【Golang實現檔案伺服器】(二)圖片去重與縮圖功能

晦若晨曦發表於2017-12-14

距離第一次寫這個文章已經很久了。

這段時間裡基於常用的應用場景,進一步豐富了檔案伺服器的功能。主要做了如下工作:

  • 現在可以自動檢查重名檔案是否重複,並自動重新命名。
  • 現在可以提供三種縮放方式獲取縮圖
  • 優化了程式碼結構

很慚愧,就做了這三個微小的工作。

###檔案去重

基於現有的應用場景,首先要求便於部署,其次對儲存效率及效能要求不高。在此前提下不適用適用資料庫的方式管理檔案。所以去重工作僅限於重名相同檔案的去重與重新命名。

最常用的檔案比較方式莫過於比較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,加油吧。

相關文章