Retry transient uploads and continue backups
All checks were successful
Build / Test and build (push) Successful in 5m53s
All checks were successful
Build / Test and build (push) Successful in 5m53s
This commit is contained in:
@@ -276,12 +276,13 @@ func (a *application) uploadCommand() *cobra.Command {
|
||||
}
|
||||
stderr := cmd.ErrOrStderr()
|
||||
lineOpen := false
|
||||
summary, err := uploadLocalPath(cmd.Context(), client, args[0], args[1], uploadPathOptions{Excludes: excludes, Progress: func(localPath, remotePath string, done, total int64) {
|
||||
summary, err := uploadLocalPath(cmd.Context(), client, args[0], args[1], uploadPathOptions{Excludes: excludes, Progress: func(_, remotePath string, done, total int64) {
|
||||
displayPath := uploadProgressPath(args[1], remotePath)
|
||||
percent := int64(0)
|
||||
if total > 0 {
|
||||
percent = done * 100 / total
|
||||
}
|
||||
fmt.Fprintf(stderr, "\r上传 %s -> %s: %d/%d bytes (%d%%)", localPath, remotePath, done, total, percent)
|
||||
fmt.Fprintf(stderr, "\r上传 %s: %d/%d bytes (%d%%)", displayPath, done, total, percent)
|
||||
lineOpen = done < total
|
||||
if !lineOpen {
|
||||
fmt.Fprintln(stderr)
|
||||
@@ -292,15 +293,20 @@ func (a *application) uploadCommand() *cobra.Command {
|
||||
lineOpen = false
|
||||
}
|
||||
fmt.Fprintf(stderr, "跳过 %s: %s\n", localPath, reason)
|
||||
}, Failed: func(_, _ string, uploadErr error) {
|
||||
if lineOpen {
|
||||
fmt.Fprintln(stderr)
|
||||
lineOpen = false
|
||||
}
|
||||
fmt.Fprintf(stderr, "失败: %v\n", uploadErr)
|
||||
}})
|
||||
if lineOpen {
|
||||
fmt.Fprintln(stderr)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
if summary.Directories > 0 || summary.Files > 0 || summary.Skipped > 0 || summary.Failed > 0 {
|
||||
fmt.Fprintf(stderr, "完成:成功上传 %d 个文件(%s),处理 %d 个目录,跳过 %d 项,失败 %d 个文件\n", summary.Files, formatBytes(summary.Bytes), summary.Directories, summary.Skipped, summary.Failed)
|
||||
}
|
||||
fmt.Fprintf(stderr, "完成:上传 %d 个文件(%s),处理 %d 个目录,跳过 %d 项\n", summary.Files, formatBytes(summary.Bytes), summary.Directories, summary.Skipped)
|
||||
return nil
|
||||
return err
|
||||
}}
|
||||
cmd.Flags().StringArrayVar(&excludes, "exclude", nil, "排除源目录中的 glob(可重复,支持 **)")
|
||||
return cmd
|
||||
|
||||
@@ -23,16 +23,19 @@ type uploadPathSummary struct {
|
||||
Files int
|
||||
Directories int
|
||||
Skipped int
|
||||
Failed int
|
||||
Bytes int64
|
||||
}
|
||||
|
||||
type uploadPathProgress func(localPath, remotePath string, uploaded, total int64)
|
||||
type uploadPathSkipped func(localPath, reason string)
|
||||
type uploadPathFailed func(localPath, remotePath string, err error)
|
||||
|
||||
type uploadPathOptions struct {
|
||||
Excludes []string
|
||||
Progress uploadPathProgress
|
||||
Skipped uploadPathSkipped
|
||||
Failed uploadPathFailed
|
||||
}
|
||||
|
||||
type uploadWalkDir func(string, fs.WalkDirFunc) error
|
||||
@@ -77,6 +80,7 @@ func uploadLocalDirectory(ctx context.Context, client uploadPathClient, root, re
|
||||
return uploadPathSummary{}, err
|
||||
}
|
||||
summary := uploadPathSummary{Directories: 1}
|
||||
var firstUploadErr error
|
||||
err := walk(root, func(localPath string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -131,11 +135,23 @@ func uploadLocalDirectory(ctx context.Context, client uploadPathClient, root, re
|
||||
reportUploadSkip(&summary, options, localPath, "百度开放 API 不支持空文件")
|
||||
return nil
|
||||
}
|
||||
return uploadRegularFile(ctx, client, localPath, remotePath, info, options.Progress, &summary)
|
||||
if err := uploadRegularFile(ctx, client, localPath, remotePath, info, options.Progress, &summary); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if firstUploadErr == nil {
|
||||
firstUploadErr = err
|
||||
}
|
||||
reportUploadFailure(&summary, options, localPath, remotePath, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return summary, err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return summary, fmt.Errorf("%d files failed to upload; first failure: %w", summary.Failed, firstUploadErr)
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
@@ -146,6 +162,13 @@ func reportUploadSkip(summary *uploadPathSummary, options uploadPathOptions, loc
|
||||
}
|
||||
}
|
||||
|
||||
func reportUploadFailure(summary *uploadPathSummary, options uploadPathOptions, localPath, remotePath string, err error) {
|
||||
summary.Failed++
|
||||
if options.Failed != nil {
|
||||
options.Failed(localPath, remotePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func uploadRegularFile(ctx context.Context, client uploadPathClient, localPath, remotePath string, info fs.FileInfo, progress uploadPathProgress, summary *uploadPathSummary) error {
|
||||
completeReported := false
|
||||
if progress != nil && info.Size() > 0 {
|
||||
@@ -172,6 +195,14 @@ func cleanUploadRemotePath(name string) string {
|
||||
return pathpkg.Clean("/" + strings.TrimPrefix(name, "/"))
|
||||
}
|
||||
|
||||
func uploadProgressPath(remoteDestination, remotePath string) string {
|
||||
root := cleanUploadRemotePath(remoteDestination)
|
||||
if relative := strings.TrimPrefix(remotePath, root+"/"); relative != remotePath {
|
||||
return relative
|
||||
}
|
||||
return pathpkg.Base(remotePath)
|
||||
}
|
||||
|
||||
func normalizeUploadExcludes(patterns []string) ([]string, error) {
|
||||
result := make([]string, 0, len(patterns))
|
||||
for _, pattern := range patterns {
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
type recordingUploadClient struct {
|
||||
directories []string
|
||||
files map[string][]byte
|
||||
failures map[string]error
|
||||
}
|
||||
|
||||
func (c *recordingUploadClient) MkdirAll(_ context.Context, remotePath string) error {
|
||||
@@ -36,6 +38,9 @@ func (c *recordingUploadClient) UploadFile(_ context.Context, localPath, remoteP
|
||||
if c.files == nil {
|
||||
c.files = make(map[string][]byte)
|
||||
}
|
||||
if err := c.failures[remotePath]; err != nil {
|
||||
return baidu.File{}, err
|
||||
}
|
||||
c.files[remotePath] = content
|
||||
if progress != nil {
|
||||
progress(int64(len(content)), int64(len(content)))
|
||||
@@ -197,6 +202,35 @@ func TestUploadLocalDirectoryContinuesAfterReadError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadLocalDirectoryContinuesAfterUploadFailure(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(root, "a-fails.txt"), []byte("failed"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "b-succeeds.txt"), []byte("success"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := &recordingUploadClient{failures: map[string]error{
|
||||
"/backup/mac/a-fails.txt": errors.New("gateway timeout"),
|
||||
}}
|
||||
var failed []string
|
||||
summary, err := uploadLocalPath(context.Background(), client, root, "/backup/mac", uploadPathOptions{Failed: func(localPath, remotePath string, err error) {
|
||||
failed = append(failed, filepath.Base(localPath)+":"+remotePath+":"+err.Error())
|
||||
}})
|
||||
if err == nil || !strings.Contains(err.Error(), "1 files failed to upload") {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
if summary.Files != 1 || summary.Failed != 1 || summary.Bytes != 7 {
|
||||
t.Fatalf("summary = %+v", summary)
|
||||
}
|
||||
if len(failed) != 1 || !strings.Contains(failed[0], "a-fails.txt:/backup/mac/a-fails.txt:") || !strings.Contains(failed[0], "gateway timeout") {
|
||||
t.Fatalf("failed = %q", failed)
|
||||
}
|
||||
if !bytes.Equal(client.files["/backup/mac/b-succeeds.txt"], []byte("success")) {
|
||||
t.Fatalf("b-succeeds.txt was not uploaded: %v", client.files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadExcludePatternMatching(t *testing.T) {
|
||||
patterns, err := normalizeUploadExcludes([]string{".Trash/", "Library/Caches/**", "**/*.tmp"})
|
||||
if err != nil {
|
||||
@@ -220,3 +254,12 @@ func TestUploadExcludePatternMatching(t *testing.T) {
|
||||
t.Fatal("invalid exclude pattern was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadProgressPath(t *testing.T) {
|
||||
if got := uploadProgressPath("/backup/mac/", "/backup/mac/Library/file.txt"); got != "Library/file.txt" {
|
||||
t.Fatalf("progress path = %q", got)
|
||||
}
|
||||
if got := uploadProgressPath("/backup/file.txt", "/backup/file.txt"); got != "file.txt" {
|
||||
t.Fatalf("single-file progress path = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user