]> git.giacomolli.uk - onechat/blobdiff - backup_archive.go
Add local context to chat operation failures
[onechat] / backup_archive.go
index bd6e00367c481c34cb4e4bf3e2775e6e64f5fb46..f68cb703932f64f55e061dd0e794fb6a67220c45 100644 (file)
@@ -13,6 +13,25 @@ import (
        "strings"
 )
 
        "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) {
 // 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)
 
        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 {
                }
 
                if ctx.Err() != nil {
@@ -44,39 +63,45 @@ func backupArchive(ctx context.Context, root, path string) (size int64, err erro
                        return nil
                }
 
                        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)
                }
 
                }
 
                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 = "", ""
                }
 
                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
                }
 
                }
 
                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)
        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
 }
 
        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.
 // 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())
        }()
 
                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 {
        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
                }
 
                        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, "/")
                }
 
                name := strings.TrimSuffix(header.Name, "/")
@@ -187,43 +241,19 @@ func backupExtract(ctx context.Context, path, root string) (err error) {
        return nil
 }
 
        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) {
 }
 
 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
 }
 
        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
-}