You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Golang的AWS SDK仅上传修改后的文件到S3存储桶?

实现AWS SDK Go的S3增量备份(类似aws s3 sync)

要实现增量备份,核心是对比本地文件与S3对象的元数据,只上传新增或修改过的文件。AWS SDK Go没有直接提供s3 sync的封装,但可以通过以下步骤手动实现:

核心思路

  1. 遍历本地目标目录,收集所有文件的路径、修改时间、大小信息
  2. 列出S3存储桶中对应前缀下的所有对象,记录它们的键、最后修改时间、大小
  3. 对比两者,筛选出需要上传的文件:
    • 本地存在但S3中没有的文件
    • 本地文件修改时间晚于S3对象最后修改时间的文件
    • 本地文件大小与S3对象大小不一致的文件(应对修改时间未更新但内容变更的极端情况)

代码实现示例

package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
)

// LocalFile 存储本地文件的元数据
type LocalFile struct {
	Path         string
	LastModified time.Time
	Size         int64
}

// S3Object 存储S3对象的元数据
type S3Object struct {
	Key          string
	LastModified time.Time
	Size         int64
}

func main() {
	// 配置参数
	localDir := "./your-local-directory"
	s3Bucket := "your-bucket-name"
	s3Prefix := "backup/" // S3中存储备份的前缀,可留空

	// 初始化AWS会话
	sess, err := session.NewSession(&aws.Config{
		Region: aws.String("us-east-1"), // 替换为你的区域
	})
	if err != nil {
		fmt.Printf("Failed to create session: %v\n", err)
		return
	}

	// 1. 收集本地文件元数据
	localFiles, err := collectLocalFiles(localDir)
	if err != nil {
		fmt.Printf("Failed to collect local files: %v\n", err)
		return
	}

	// 2. 收集S3对象元数据
	s3Objects, err := collectS3Objects(sess, s3Bucket, s3Prefix)
	if err != nil {
		fmt.Printf("Failed to collect S3 objects: %v\n", err)
		return
	}

	// 3. 筛选需要上传的文件
	filesToUpload := filterFilesToUpload(localDir, s3Prefix, localFiles, s3Objects)

	// 4. 上传增量文件
	uploader := s3manager.NewUploader(sess)
	for _, file := range filesToUpload {
		fmt.Printf("Uploading %s to s3://%s/%s\n", file.Path, s3Bucket, file.S3Key)

		f, err := os.Open(file.Path)
		if err != nil {
			fmt.Printf("Failed to open file %s: %v\n", file.Path, err)
			continue
		}
		defer f.Close()

		_, err = uploader.Upload(&s3manager.UploadInput{
			Bucket: aws.String(s3Bucket),
			Key:    aws.String(file.S3Key),
			Body:   f,
		})
		if err != nil {
			fmt.Printf("Failed to upload %s: %v\n", file.Path, err)
		}
	}

	fmt.Println("Incremental backup completed")
}

// collectLocalFiles 遍历本地目录,返回所有文件的元数据
func collectLocalFiles(rootDir string) (map[string]LocalFile, error) {
	files := make(map[string]LocalFile)

	err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}

		// 获取相对路径,用于映射到S3键
		relPath, err := filepath.Rel(rootDir, path)
		if err != nil {
			return err
		}

		files[relPath] = LocalFile{
			Path:         path,
			LastModified: info.ModTime().UTC(), // 转UTC和S3统一时区
			Size:         info.Size(),
		}
		return nil
	})

	return files, err
}

// collectS3Objects 列出S3桶中指定前缀的所有对象元数据
func collectS3Objects(sess *session.Session, bucket, prefix string) (map[string]S3Object, error) {
	objects := make(map[string]S3Object)
	svc := s3.New(sess)

	input := &s3.ListObjectsV2Input{
		Bucket: aws.String(bucket),
		Prefix: aws.String(prefix),
	}

	for {
		result, err := svc.ListObjectsV2(input)
		if err != nil {
			return nil, err
		}

		for _, obj := range result.Contents {
			// 去掉前缀,得到相对路径,和本地文件的relPath对应
			relKey := *obj.Key[len(prefix):]
			if relKey == "" {
				continue // 跳过前缀目录本身
			}

			objects[relKey] = S3Object{
				Key:          *obj.Key,
				LastModified: *obj.LastModified,
				Size:         *obj.Size,
			}
		}

		if !*result.IsTruncated {
			break
		}
		input.ContinuationToken = result.NextContinuationToken
	}

	return objects, nil
}

// 需要上传的文件信息
type FileToUpload struct {
	Path   string
	S3Key  string
}

// filterFilesToUpload 对比本地文件和S3对象,筛选需要上传的文件
func filterFilesToUpload(localDir, s3Prefix string, localFiles map[string]LocalFile, s3Objects map[string]S3Object) []FileToUpload {
	var toUpload []FileToUpload

	for relPath, localFile := range localFiles {
		s3Obj, exists := s3Objects[relPath]
		if !exists {
			// S3中不存在该文件,需要上传
			toUpload = append(toUpload, FileToUpload{
				Path:  localFile.Path,
				S3Key: filepath.Join(s3Prefix, relPath), // 拼接S3前缀和相对路径
			})
			continue
		}

		// 对比修改时间和大小
		if localFile.LastModified.After(s3Obj.LastModified) || localFile.Size != s3Obj.Size {
			toUpload = append(toUpload, FileToUpload{
				Path:  localFile.Path,
				S3Key: s3Obj.Key,
			})
		}
	}

	return toUpload
}

关键细节说明

  • 时区统一:将本地文件的修改时间转换为UTC,和S3对象的LastModified(默认UTC)保持一致,避免时区差异导致的错误对比
  • 路径映射:用本地文件的相对路径对应S3对象的键(去掉前缀后的部分),确保路径结构一致
  • 大小校验:除了修改时间,额外对比文件大小,应对修改时间未更新但内容已变更的极端场景
  • 分页处理:S3的ListObjectsV2会分页返回结果,代码中处理了分页逻辑,确保获取所有对象

内容的提问来源于stack exchange,提问作者Wolf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 23:30:55