如何为static-aws-deploy实现S3 Delta-Upload

223 阅读4分钟

delta-upload只是一种花哨的说法,即只上传有变化的或新的文件--所以我不想在每次发布文章时更新我博客上的所有文件,我只想上传新文件和有变化的文件(例如:RSS feed),这将节省一点带宽,对API的大量请求,并将使该工具对小的变化更加灵活,例如在发布后修复错字。

让我们来看看我是怎么想的!

方法

有几种方法可以找出哪些文件是新的或改变过的。在这种情况下,我决定使用文件的lastModifiedDate ,以及它们的md5 hash。如果本地文件的lastModifiedDate 是在已经上传的文件之后,并且如果md5 的哈希值不一致,那么该文件将被标记为新的或更改的。

我之所以使用md5 ,只是因为S3 REST API有一种方法来获取一个桶内的所有文件,这里有记录,其中也包括一个ETag 属性,这是文件的md5 哈希值。

从架构的角度来看,static-aws-deploy已经走了所有的文件,以便收集它们并生成它们的Headers ,以便上传,所以在这里挂钩似乎是一个好主意。

实施

首先,我需要添加一个标志来启用delta-uploading以保持向后兼容。

var (
    ...
    delta      bool
)

flag.BoolVar(&delta, "delta", false, "only upload changed files")
flag.BoolVar(&delta, "d", false, "only upload changed files (shorthand)")

下一步是将这个标志传递给upload.ParseFiles 函数。

files, err := upload.ParseFiles(&config.S3, delta)

在这个函数中,如果设置了delta 标志,就需要向AWS S3发出请求,以获取桶中所有文件的信息,并将它们放入一个漂亮的数据结构中供以后使用。该数据结构看起来像这样。

type Delta map[string]*DeltaProperties

type DeltaProperties struct {
    LastModified time.Time
    ETag         string
}

基本上,它只是从一个文件映射到它的属性,我们需要确定它是否已经被改变。为了创建和填充这个地图,我写了getDeltaMap 函数,它请求AWS S3,用etree解析XML,返回一个Delta ,并处理所有错误。

它看起来像这样(为简洁起见省略了错误处理)。

func getDeltaMap(config *Config) (Delta, error) {
    // make authenticated AWS S3 request
    client := &http.Client{}
    req, err := http.NewRequest("GET", fmt.Sprintf("https://s3.amazonaws.com/%s/?list-type=2", config.Bucket.Name), nil)
    awsauth.Sign(req, awsauth.Credentials{
        AccessKeyID:     config.Bucket.Accesskey,
        SecretAccessKey: config.Bucket.Key,
    })
    resp, err := client.Do(req)

    // parse xml
    doc := etree.NewDocument()
    doc.ReadFrom(resp.Body);
    root := doc.SelectElement("ListBucketResult")
    contents := root.SelectElements("Contents")

    // build map
    deltaMap := make(Delta)
    for _, file := range contents {
        lastModified := file.SelectElement("LastModified")
        etag := file.SelectElement("ETag")
        key := file.SelectElement("Key")
        parsedLastModified, err := time.Parse(time.RFC3339Nano, lastModified.Text())
        deltaProp := DeltaProperties{
            ETag:           strings.Trim(etag.Text(), "\""),
            LastModified:   parsedLastModified,
        }
        deltaMap[key.Text()] = &deltaProp
    }
    return deltaMap, nil
}

ETag这里没有什么太令人惊讶的,除了我们需要从Trim 一些""s ,因为XML是这样的。有了这个,我们已经有了delta-upload的关键组件--一种检查我们计划上传的文件是否与服务器上已有的文件不同的方法。

下一步是找出我们本地文件的lastModifiedDatemd5 散列值。为此目的,我创建了hasFileChanged 函数,如下所示。

func hasFileChanged(info os.FileInfo, deltaMap Delta, uploadPath, filePath string) (bool, error) {
    etag, err := calculateETag(filePath)
    if err != nil {
        return false, err
    }
    deltaProps := deltaMap[uploadPath]
    if deltaProps != nil {
        return etag != deltaProps.ETag && info.ModTime().After(deltaProps.LastModified), nil
    }
    return true, nil
}

这个函数将在行走所有要上传的文件时被调用。它得到一个os.FileInfodeltaMap 和两个路径。其中一个路径是正常的本地文件路径,我们需要计算md5 哈希值,另一个是清理过的路径,它与已经上传的文件的Key 匹配,这些文件被映射在deltaMap

首先,计算本地文件的ETag

func calculateETag(path string) (string, error) {
    bytes, err := ioutil.ReadFile(path)
    if err != nil {
        return "", fmt.Errorf("could not read file: %s while calculating it's ETag, %v", path, err)
    }
    return fmt.Sprintf("%x", md5.Sum(bytes)), nil
}

然后,对于deltaMap 内的每个文件,我们检查ETags是否匹配,以及该文件在上传后是否被修改。这样,我们将同时标记已修改的和新的文件。

有了这些函数,扩展文件行走的逻辑就变得微不足道了。

// in ParseFiles(...)
...
deltaMap := make(Delta)
if delta {
    deltaMap, err = getDeltaMap(config)
    if err != nil {
        return nil, err
    }
}
err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
    if !info.IsDir() && !re.MatchString(path) {
        hasChanged := true
        if delta {
            hasChanged, err = hasFileChanged(info, deltaMap, getUploadPath(config, path), path)
            if err != nil {
                return err
            }
        }
        if hasChanged {
            result[path] = []Header{}
        }
    }
    return nil
})

好了,就这样了。现在在我的博客源上运行static-aws-deploy --dr -d ,可以看到以下情况。

0 Files to upload (4 concurrently)...
Delta Dry Run finished.

而在手动改变RSS-feed(index.xml)之后。

1 Files to upload (4 concurrently)...
www/index.xml...Done.
Delta Dry Run finished.

完成了!