X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/3bf0f670487e571a0525844a6c00de495b8a465a..9346d20fe4297dce1aa16171749a7ef3e41ce7bd:/backup_archive.go diff --git a/backup_archive.go b/backup_archive.go index bd6e003..f68cb70 100644 --- a/backup_archive.go +++ b/backup_archive.go @@ -13,6 +13,25 @@ import ( "strings" ) +// BackupReader checks cancellation between reads of an archive or file. +type BackupReader struct { + ctx context.Context + reader io.Reader +} + +func (r BackupReader) Read(data []byte) (int, error) { + if r.ctx.Err() != nil { + return 0, r.ctx.Err() + } + + count, err := r.reader.Read(data) + if err != nil { + return count, fmt.Errorf("read backup data: %w", err) + } + + return count, nil +} + // backupArchive writes the files in root to path through a private temporary // file and a rename. Only directories and regular files enter the archive. func backupArchive(ctx context.Context, root, path string) (size int64, err error) { @@ -31,9 +50,9 @@ func backupArchive(ctx context.Context, root, path string) (size int64, err erro compressed := gzip.NewWriter(file) archive := tar.NewWriter(compressed) - err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error { - if walkErr != nil { - return fmt.Errorf("walk backup: %w", walkErr) + err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("walk backup: %w", err) } if ctx.Err() != nil { @@ -44,39 +63,45 @@ func backupArchive(ctx context.Context, root, path string) (size int64, err erro return nil } - name, nameErr := filepath.Rel(root, path) - if nameErr != nil { - return fmt.Errorf("name archive entry: %w", nameErr) + name, err := filepath.Rel(root, path) + if err != nil { + return fmt.Errorf("name archive entry: %w", err) } - info, infoErr := entry.Info() - if infoErr != nil { - return fmt.Errorf("read backup entry: %w", infoErr) + info, err := entry.Info() + if err != nil { + return fmt.Errorf("read backup entry: %w", err) } if !info.IsDir() && !info.Mode().IsRegular() { return fmt.Errorf("unsupported backup entry %q", name) } - header, headerErr := tar.FileInfoHeader(info, "") - if headerErr != nil { - return fmt.Errorf("make archive header: %w", headerErr) + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return fmt.Errorf("make archive header: %w", err) } header.Name = filepath.ToSlash(name) header.Uid, header.Gid = 0, 0 header.Uname, header.Gname = "", "" - headerErr = archive.WriteHeader(header) - if headerErr != nil { - return fmt.Errorf("write archive header: %w", headerErr) + err = archive.WriteHeader(header) + if err != nil { + return fmt.Errorf("write archive header: %w", err) } if info.IsDir() { return nil } - return backupCopyFile(ctx, archive, path, info.Size()) + err = backupCopyFile(ctx, archive, path, info.Size()) + if err != nil { + return fmt.Errorf("archive file: %w", err) + } + + return nil }) + err = errors.Join(err, archive.Close(), compressed.Close()) if err != nil { return 0, fmt.Errorf("write archive: %w", err) @@ -100,6 +125,29 @@ func backupArchive(ctx context.Context, root, path string) (size int64, err erro return info.Size(), nil } +// backupCopyFile copies the size fixed at the snapshot of path. Growth after +// that point is not part of the copy, and a shorter file is an error. +func backupCopyFile(ctx context.Context, out io.Writer, path string, size int64) (err error) { + file, err := os.Open(path) + if err != nil { + return fmt.Errorf("open backup file: %w", err) + } + + defer func() { + err = errors.Join(err, file.Close()) + }() + + _, err = io.CopyN(out, BackupReader{ + ctx: ctx, + reader: file, + }, size) + if err != nil { + return fmt.Errorf("copy backup file: %w", err) + } + + return nil +} + // backupExtract restores regular files and directories in an empty staging // directory. Links, duplicate entries, unsafe names, and special modes fail. // Directory modes apply last so a read-only directory can hold files. @@ -122,19 +170,25 @@ func backupExtract(ctx context.Context, path, root string) (err error) { err = errors.Join(err, compressed.Close()) }() - archive := tar.NewReader(BackupReader{ctx: ctx, reader: compressed}) + archive := tar.NewReader(BackupReader{ + ctx: ctx, + reader: compressed, + }) + seen := make(map[string]bool) var directories []*tar.Header for { - header, readErr := archive.Next() - if errors.Is(readErr, io.EOF) { + var header *tar.Header + + header, err = archive.Next() + if errors.Is(err, io.EOF) { break } - if readErr != nil { - return fmt.Errorf("read archive entry: %w", readErr) + if err != nil { + return fmt.Errorf("read archive entry: %w", err) } name := strings.TrimSuffix(header.Name, "/") @@ -187,43 +241,19 @@ func backupExtract(ctx context.Context, path, root string) (err error) { return nil } -// BackupReader checks cancellation between reads of an archive or file. -type BackupReader struct { - ctx context.Context - reader io.Reader -} - -func (r BackupReader) Read(data []byte) (int, error) { - if r.ctx.Err() != nil { - return 0, r.ctx.Err() - } - - count, err := r.reader.Read(data) - if err != nil { - return count, fmt.Errorf("read backup data: %w", err) - } - - return count, nil -} - -// backupCopyFile copies the size fixed at the snapshot of path. Growth after -// that point is not part of the copy, and a shorter file is an error. -func backupCopyFile(ctx context.Context, out io.Writer, path string, size int64) (err error) { - file, err := os.Open(path) - if err != nil { - return fmt.Errorf("open backup file: %w", err) +// backupExcluded keeps runtime locks, sockets, probe files, and import copies +// out of the snapshot. +func backupExcluded(name string) bool { + first, _, _ := strings.Cut(filepath.ToSlash(name), "/") + if first == socketFileName || first == tokenLockFileName || first == "probe" || strings.HasPrefix(first, "agent-backup-") { + return true } - defer func() { - err = errors.Join(err, file.Close()) - }() - - _, err = io.CopyN(out, BackupReader{ctx: ctx, reader: file}, size) - if err != nil { - return fmt.Errorf("copy backup file: %w", err) + if filepath.Base(name) == "lock" || (first == imageDirName && strings.HasPrefix(filepath.Base(name), ".image-")) || (first == voiceDirName && (strings.HasPrefix(filepath.Base(name), ".voice-") || strings.HasPrefix(filepath.Base(name), ".transcript-"))) { + return true } - return nil + return false } func backupExtractFile(in io.Reader, path string, mode os.FileMode) (err error) { @@ -248,18 +278,3 @@ func backupExtractFile(in io.Reader, path string, mode os.FileMode) (err error) return nil } - -// backupExcluded keeps runtime locks, sockets, probe files, and import copies -// out of the snapshot. -func backupExcluded(name string) bool { - first, _, _ := strings.Cut(filepath.ToSlash(name), "/") - if first == socketFileName || first == tokenLockFileName || first == "probe" || strings.HasPrefix(first, "agent-backup-") { - return true - } - - if filepath.Base(name) == "lock" { - return true - } - - return false -}