距离第一次写这个文章已经很久了。

这段时间里基于常用的应用场景,进一步丰富了文件服务器的功能。主要做了如下工作:

  • 现在可以自动检查重名文件是否重复,并自动重命名。
  • 现在可以提供三种缩放方式获取缩略图
  • 优化了代码结构

很惭愧,就做了这三个微小的工作。

###文件去重

基于现有的应用场景,首先要求便于部署,其次对存储效率及性能要求不高。在此前提下不适用适用数据库的方式管理文件。所以去重工作仅限于重名相同文件的去重与重命名。

最常用的文件比较方式莫过于比较MD5码。在golang中获取文件的MD5也是一件很简单的事情:

//计算MD5值
func calMd5(input io.Reader) string {
	md5h := md5.New()
	io.Copy(md5h, input)
	return string(md5h.Sum([]byte(""))) //md5
}
复制代码

为了实现查重功能,我在存储文件信息的结构体中保存了一个重复次数字段用以计算及重命名。

具体实现步骤如下:

1、检查文件是否存在,不存在直接保存

2、若文件存在,检查MD5码是否相同,相同则记为已保存

3、若MD5码不同,重名计数加一,以添加后缀的方式进行重命名,然后重复步骤1.

在代码上,以下三个函数来实现查重:


/**
对文件进行重命名。
 */
func (upload *UploadFile) rename() {
	path := upload.fsroot+upload.name;
	ex := exist(path,upload.md5)
	if ex==1{
		upload.dupcount = upload.dupcount+1 //重复次数+1
		fileext := filepath.Ext(upload.name) //获取扩展名
		filename := strings.TrimSuffix(upload.name,fileext) // 获取文件名
		if(upload.dupcount>1){
			filename = strings.TrimSuffix(upload.name,"_"+strconv.Itoa(upload.dupcount-1)+fileext) // 获取文件名
		}
		filename+="_"+strconv.Itoa(upload.dupcount) //文件名加上重复次数
		upload.name = filename+fileext //重命名
		upload.rename()
	}
	//记录为已保存过
	if ex==2 {
		upload.issaved=1
	}
	return
}



// 检查文件是否存在
// 根据MD5判断重名文件是否重复,若重复则删除原文件
// 如果由 filename 指定的文件或目录存在则返回 1,否则返回0,若文件已存在且MD5相等,返回2
func exist(filename string,md5 string) int {
	_, err := os.Stat(filename)
	//文件不存在
	if err != nil {
		return 0
	}
	file, ferr := os.Open(filename)
	//读取文件失败=不存在
	if ferr!=nil {
		return 0
	}
	//若MD5相等,保存原文件,算作已存在
	if calMd5(file) == md5{
		return 2;
	}

	//文件存在
	return 1;
}
复制代码

预计可能存在的问题:

  • 对于不重名的相同文件没有处理能力。
  • 对于大文件读取MD5效率比较低。
  • 脱离于业务,在为多个项目同时提供服务,则很难保证文件删除功能的安全性。

获取缩略图

说实话……golang的图片库并没有看明白。不过感谢伟大的github,使用了imaging之后,很愉快滴就实现了这个功能。

基于最常用的场景,实现了三种缩略方式:

  • 按比例缩放,保证全图信息
  • 按给定尺寸缩放,保证比例及全图信息
  • 按给定尺寸拉伸裁剪,保证尺寸及比例,裁剪掉多于信息。

为了从磁盘读取文件,我创建了一个新的结构体LocalFile……说起来应该是LocalImage才对。不过下次再说吧。

/**
本地图片文件
 */
type LocalFile struct {
	uri string
	data image.Image//图片数据
	out string //输出文件路径
	scalaTo float32 //缩放比例
	scalaAsStr string//缩放尺寸字符串
	scalaWidth int
	scalaHeight int
	cutStr string //裁剪属性字符串
	cutStartX int
	cutStartY int
	cutWidth  int
	cutHeight int

}

//将本地文件加载到内存,若读取文件出错则返回错误
func (file *LocalFile) load () error{
	path := "upload/"+file.uri
	logs.Info(path)
	f,err := os.Open(path)
	if(err!=nil){
		return err
	}
	defer f.Close()

	img,ie := imaging.Decode(f)

	if(ie!=nil){
		return ie
	}

	file.data = img

	return nil

}
复制代码

下面仅以按尺寸缩放为例,说明缩放方式,更多的缩放方式可以参考imaging库的github

func (file *LocalFile) scalaAs(x,y int){
	fmt.Printf("scala image to %d * %d\n",x,y)
	file.scalaWidth = x
	file.scalaHeight = y
	dst := imaging.Fit(file.data, file.scalaWidth, file.scalaHeight, imaging.Lanczos)
	file.data = dst
}
复制代码

###代码的修补

为了便于日后的工作,我对原始的代码进行了一些整理和封装。可以说是从java带来的不封装会死病。

1、 切实的调用了初始化函数

没毛病……之前虽然写了一个init但是并没有使用。现在会在执行时调用初始化函数,并且当初始化失败时会报错并退出。

2、 整理网络请求处理方法

将网络请求的处理方法单独扔到了httpservice.go文件里。在xofileserver.go里面只要与服务启动相关的东西,不要业务相关的代码。

然后对ajax的处理进行了一下转移。将跨域调用的header移动到UpdateResponse结构体的SendJsonp方法中。对基本的ajax请求进行了一下简单的封装。目的在于统一json返回的格式。虽然现在只有一个上传接口。

3、多打日志,多写注释

增加可读性,出错也更容易及时发现。

4、及时关闭文件

恩,第一次写的时候并没有注意到这个细节。现在补齐了。

主要解决了一个bug:

原本采用将图片的Reader直接放在结构体中的方式来传送图片。

当加入MD5查重之后,此处出现了严重的问题。计算MD5会读取流中的所有数据,导致在保存文件时就只剩一个空文件。

经过检查确认问题之后,改用bytes.Buffer来保存数据。解决问题。

###小结

在本次的改进中,对于golang的语言逻辑和思路有了更深入一点的理解。但是切实上并没有使用什么特别高深的技术。其实由此也可见golang对于常用功能的封装做得非常好。只要简单的代码就可以实现很多的功能。

基于业务驱动,导致现在的文件服务主要为图片服务。下一步将会加入对于更多文件类型的特殊处理,提供压缩打包批量下载等功能。也可能添加几个简单的页面便于进行管理操作。

新的2017,加油吧。