使用Go語言完成文件夾的MD5計算

案例需求:我們的監測系統會定期的檢查配置文件的變動,這些配置文件放置在一個獨立的文件夾下面,我們可以通過對於整個的文件夾內所有文件進行md5的計算來完成監測,本文就通過Go語言實現了一個命令行工具,完成上述的需求。

1. 單一文件的md5計算

我們首先將需求任務進行分解,既然需要計算文件夾下的所有文件md5值,我們必須先考慮如何實現單一文件的md5值計算。

下面就是一個簡單的md5求值程序,這裏我們通過參數傳遞進去需要計算的文件,然後調用go語言提供的內置的crypto包中的函數來完成取值,計算得出的結果使用16進制的方式打印出來。

package main

import (
    "crypto/md5"
    "fmt"
    "io/ioutil"
    "os"
)

func Md5SumFile(file string) (value [md5.Size]byte, err error) {
    data, err := ioutil.ReadFile(file)
    if err != nil {
        return
    }
    value = md5.Sum(data)
    return
}
func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: ./md5file yourfile")
        return
    }
    md5Value, err := Md5SumFile(os.Args[1])
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    fmt.Printf("%x %s\n", md5Value, os.Args[1])
}

執行程序輸出如下面所示:

$ go run src/md5_files.go src/test.txt 
fc3ff98e8c6a0d3087d515c0473f8677 src/test.txt

2 文件的遍歷與計算

2.1 參數處理

如果我們傳遞進去的是一個文件夾則如何處理呢?這裏我們先通過改造原始的程序使得程序可以接收不同的參數類型比如-d代表了後面的爲一個目錄文件,-f代表了傳遞的是一個文件,這裏我們需要藉助於golang的flags包來完成參數的解析,解析完畢後我們就可以按照對應的方式處理參數了。

var directory, file *string

func init() {
    directory = flag.String("d", "", "The directory contains all the files that need to calculate the md5 value")
    file = flag.String("f", "", "The file that need to caclulate the md5 value")
}
func main() {
    flag.Parse()
    if *directory == "" && *file == "" {
        flag.Usage()
        return
    }
    if *file != "" {
        md5Value, err := Md5SumFile(*file)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        fmt.Printf("%x %s\n", md5Value, *file)
        return
    }
    if *directory != "" {

        result, err := Md5SumFolder(*directory)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        var paths []string
        for path := range result {
            paths = append(paths, path)
        }
        sort.Strings(paths)
        for _, path := range paths {
            fmt.Printf("%x %s\n", result[path], path)
        }
    }
}

這裏我們根據不同的參數類型進行處理,對於文件的話,我們按照第一個例子中的處理方式調用Md5SumFile()函數獲得文件相應的md5值,而如果是文件夾類型的話,我們則需要新建一個處理函數,並且這個處理函數的返回值應該包含文件夾中所有文件和對應的md5值。

2.2 文件夾函數處理

整個的文件夾的處理函數如下面所示,包含了文件的遍歷和調用md5取值的過程,最終的結果保存在一個map類型中返回。

func Md5SumFolder(folder string) (map[string][md5.Size]byte, error) {
    result := make(map[string][md5.Size]byte)
    err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }
        if !info.Mode().IsRegular() {
            return nil
        }
        data, err := ioutil.ReadFile(path)
        if err != nil {
            return err
        }
        result[path] = md5.Sum(data)
        return nil
    })
    if err != nil {
        return nil, err
    }
    return result, nil
}
2.3 彙總處理結果

我們還需要對於所有的文件md5值進行合併並且計算一個最終的md5值,這樣我們只需要監測這個md5值是否發生變化,就能知道這個文件夾是否發生了變化。這裏我們新加入一個-merge參數代表了輸出單一的合併後的值,否則則輸出整個文件夾下所有的md5值。

//init函數
merge = flag.Bool("merge", false, "Merging all md5 values to one (Folder type only)")
...
//main函數
if *merge == true {
        var md5value string
        for _, path := range paths {
            md5value += fmt.Sprintf("%x", result[path])
        }
        fmt.Printf("%x %s\n", md5.Sum([]byte(md5value)), *directory)
    } else {
        for _, path := range paths {
            fmt.Printf("%x %s\n", result[path], path)
        }
    }

這樣我們可以測試函數執行情況如下,分別對於不同的類型參數進行取值:

$ go run src/md5_folder_support.go -f src/test.txt
01f73475c6d686cc2efd74a7c2a96df5 src/test.txt

$ go run src/md5_folder_support.go -d src
4adea23a39f58672f0258980d6c4208 src/md5_folder_support.go
fc3ff98e8c6a0d3087d515c0473f8677 src/test.txt
c7a96525778ec2daefb3e57c99b8c6c2 src/test/hello
7b9e14080b072115f287a7608d3b4aff src/test/test_sub/hello
de8ce01552ee663717249956f664c1f6 src/version/md5_files.go

$ go run src/md5_folder_support.go -d src -merge
b38c907db04043c7508b464378a5f632 src

3並行化取值

3.1 串行測試

如果只是使用上述的執行程序其實與直接編寫一個shell腳本沒什麼太多區別,我們實測一個大小500Mb的文件夾的執行時間:

$ ./md5_folder_support -d /home/mike/Calibre\ 書庫 -merge
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
1.36595528s

如果使用shell來直接執行的話,測試腳本如下:

$ start=$(date +'%s%N');find /home/mike/Calibre\ 書庫 -type f -exec md5sum {} \;|sort -k 2|md5sum ;eclipe=$((($(date +'%s%N')-$start)));echo "$eclipe/1000000000"|bc -l
1.40261921400000000000

由於shell傳遞的是整個的sort產生的結果到md5所以顯示的會不一致,但是流程基本上都涉及了,對於單個文件的取值,對於結果的排序,和彙總輸出。我們可以看到基本上輸出的花費時間是差不多的。

3.2 並行程序執行

我們將上面的串行執行程序,利用go的並行處理方式進行改寫,改寫後的文件夾處理函數代碼如下:

func Md5SumFolder(folder string) (map[string][md5.Size]byte, error) {
    returnValue := make(map[string][md5.Size]byte)
    done := make(chan struct{})
    defer close(done)
    c := make(chan result)
    errc := make(chan error, 1)
    var wg sync.WaitGroup
    go func() {
        err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.Mode().IsRegular() {
                return nil
            }
            wg.Add(1)
            go func() {
                data, err := ioutil.ReadFile(path)
                select {
                case c <- result{path: path, md5Sum: md5.Sum(data), err: err}:
                case <-done:
                }
                wg.Done()
            }()
            select {
            case <-done:
                return errors.New("Canceled")
            default:
                return nil
            }
        })
        errc <- err
        go func() {
            wg.Wait()
            close(c)
        }()
    }()
    for r := range c {
        if r.err != nil {
            return nil, r.err
        }
        returnValue[r.path] = r.md5Sum
    }
    if err := <-errc; err != nil {
        return nil, err
    }
    return returnValue, nil
}

我們利用了sync包中的函數來完成等待過程,防止並行程序的提前退出。利用了一個名爲done的channel來管理程序的退出,不管任何情況下,該通道的都會隨着函數的退出而執行關閉操作,不再接收任何的輸入進來。程序如下片段中,我們對於每一個新的文件,利用wg.Add()添加一個佔位,這樣如果我們不去手動的執行一個wg.Done()則主程序就會停止在wg.Wait()處。

wg.Add(1)
go func() {
    data, err := ioutil.ReadFile(path)
    select {
    case c <- result{path: path, md5Sum: md5.Sum(data), err: err}:
    case <-done:
    }
    wg.Done()
}()

對於程序執行一次跟之前相同的運算可以看出,新的並行程序的執行速度已經明顯得到提升

go run src/main.go -d /home/mike/Calibre\ 書庫 -merge
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
279.154729ms
3.3 並行限制

上述的例子中我們並未限制打開的文件數量,比如我們文件夾下有上萬個文件,這樣讀入內存中的數據可能會非常的大,計算起來也要耗盡所有的內存,因此我們限制我們最大的讀取數量比如限制最大打開10個文件進行計算,這樣我們的程序需要重新設計一下。

我們先看一下程序最終實際的運行效果,基本上按照預期的時間處理完畢:

$ go run src/main.go -d /home/mike/Calibre\ 書庫 -merge -max 1
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
1.241248438s
$ go run src/main.go -d /home/mike/Calibre\ 書庫 -merge -max 2
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
831.018648ms
$ go run src/main.go -d /home/mike/Calibre\ 書庫 -merge -max 3
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
520.293084ms
$ go run src/main.go -d /home/mike/Calibre\ 書庫 -merge
d9c08b4a6149cb8d57740bd4482a820e /home/mike/Calibre 書庫
228.959296ms

4 最終程序清單如下

package main

import (
    "crypto/md5"
    "errors"
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
    "sort"
    "sync"
    "time"
)

var directory, file *string
var merge *bool
var limit *int

func init() {
    directory = flag.String("d", "", "The directory contains all the files that need to calculate the md5 value")
    file = flag.String("f", "", "The file that need to caclulate the md5 value")
    merge = flag.Bool("merge", false, "Merging all md5 values to one (Folder type only)")
    limit = flag.Int("max", 0, "limit the max files to caclulate.")
}
func Md5SumFile(file string) (value [md5.Size]byte, err error) {
    data, err := ioutil.ReadFile(file)
    if err != nil {
        return
    }
    value = md5.Sum(data)
    return
}

type result struct {
    path   string
    md5Sum [md5.Size]byte
    err    error
}

func Md5SumFolder(folder string, limit int) (map[string][md5.Size]byte, error) {
    returnValue := make(map[string][md5.Size]byte)
    var limitChannel chan (struct{})
    if limit != 0 {
        limitChannel = make(chan struct{}, limit)
    }

    done := make(chan struct{})
    defer close(done)

    c := make(chan result)
    errc := make(chan error, 1)
    var wg sync.WaitGroup
    go func() {
        err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if !info.Mode().IsRegular() {
                return nil
            }

            if limit != 0 {
                //如果已經滿了則阻塞在這裏
                limitChannel <- struct{}{}
            }

            wg.Add(1)
            go func() {
                data, err := ioutil.ReadFile(path)
                select {
                case c <- result{path: path, md5Sum: md5.Sum(data), err: err}:
                case <-done:
                }
                if limit != 0 {
                    //讀出數據,這樣就有新的文件可以處理
                    <-limitChannel
                }

                wg.Done()
            }()
            select {
            case <-done:
                return errors.New("Canceled")
            default:
                return nil
            }
        })
        errc <- err
        go func() {
            wg.Wait()
            close(c)
        }()
    }()
    for r := range c {
        if r.err != nil {
            return nil, r.err
        }
        returnValue[r.path] = r.md5Sum
    }
    if err := <-errc; err != nil {
        return nil, err
    }
    return returnValue, nil
}

func main() {
    timeStart := time.Now()
    flag.Parse()
    if *directory == "" && *file == "" {
        flag.Usage()
        return
    }
    if *file != "" {
        md5Value, err := Md5SumFile(*file)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        fmt.Printf("%x %s\n", md5Value, *file)
        return
    }
    if *directory != "" {

        result, err := Md5SumFolder(*directory, *limit)
        if err != nil {
            fmt.Println(err.Error())
            return
        }
        var paths []string
        for path := range result {
            paths = append(paths, path)
        }
        sort.Strings(paths)
        if *merge == true {
            var md5value string
            for _, path := range paths {
                md5value += fmt.Sprintf("%x", result[path])
            }
            fmt.Printf("%x %s\n", md5.Sum([]byte(md5value)), *directory)
        } else {
            for _, path := range paths {
                fmt.Printf("%x %s\n", result[path], path)
            }
        }

    }
    fmt.Println(time.Since(timeStart).String())
}

這就是整個的程序的執行流程,如果有任何問題,請留言告訴我或者email給我,我的個人網站jsmean.com歡迎大家訪問。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章