// Package archive пишет и читает архивы бэкапа: tar (PAX) поверх zstd. package archive import ( "archive/tar" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "time" "github.com/klauspost/compress/zstd" ) type Writer struct { zw *zstd.Encoder tw *tar.Writer seen map[string]bool Files int Bytes int64 Warnings []string } func NewWriter(w io.Writer) (*Writer, error) { zw, err := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedDefault)) if err != nil { return nil, err } return &Writer{zw: zw, tw: tar.NewWriter(zw), seen: map[string]bool{}}, nil } func (w *Writer) warn(format string, a ...any) { w.Warnings = append(w.Warnings, fmt.Sprintf(format, a...)) } // AddBytes пишет в архив файл из памяти. func (w *Writer) AddBytes(name string, data []byte, mode int64) error { hdr := &tar.Header{ Name: name, Mode: mode, Size: int64(len(data)), ModTime: time.Now(), Typeflag: tar.TypeReg, Format: tar.FormatPAX, } if err := w.tw.WriteHeader(hdr); err != nil { return err } _, err := w.tw.Write(data) w.seen[name] = true w.Files++ w.Bytes += hdr.Size return err } // AddFiles добавляет абсолютный путь src (файл или каталог) под именами prefix+<путь без ведущего />. func (w *Writer) AddFiles(prefix, src string, skip *Matcher) error { return w.AddTree(src, func(p string) string { return prefix + strings.TrimPrefix(filepath.ToSlash(p), "/") }, skip) } // AddTree обходит src (не следуя симлинкам) и пишет каждую запись под именем name(path). // Ошибки отдельных файлов (нет доступа, исчез во время обхода) становятся предупреждениями. func (w *Writer) AddTree(src string, name func(p string) string, skip *Matcher) error { return filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error { if err != nil { if errors.Is(err, fs.ErrNotExist) && p != src { return nil } w.warn("%s: %v", p, err) if d != nil && d.IsDir() { return fs.SkipDir } return nil } if skip.Match(filepath.ToSlash(p)) { if d.IsDir() { return fs.SkipDir } return nil } return w.addEntry(p, name(p)) }) } func (w *Writer) addEntry(p, name string) error { fi, err := os.Lstat(p) if err != nil { w.warn("%s: %v", p, err) return nil } mode := fi.Mode() if mode&(fs.ModeSocket|fs.ModeDevice|fs.ModeCharDevice|fs.ModeNamedPipe|fs.ModeIrregular) != 0 { return nil } if fi.IsDir() { name = strings.TrimSuffix(name, "/") + "/" } if w.seen[name] { return nil } var link string if mode&fs.ModeSymlink != 0 { if link, err = os.Readlink(p); err != nil { w.warn("%s: %v", p, err) return nil } } hdr, err := tar.FileInfoHeader(fi, link) if err != nil { w.warn("%s: %v", p, err) return nil } hdr.Name = name hdr.Format = tar.FormatPAX var f *os.File if hdr.Typeflag == tar.TypeReg { // открываем до заголовка: недоступный файл просто пропускаем if f, err = os.Open(p); err != nil { w.warn("%s: %v", p, err) return nil } defer f.Close() } if err := w.tw.WriteHeader(hdr); err != nil { return err } w.seen[name] = true w.Files++ if f == nil { return nil } n, err := io.CopyN(w.tw, f, hdr.Size) w.Bytes += n if err != nil { if !errors.Is(err, io.EOF) { return fmt.Errorf("%s: %w", p, err) } // файл уменьшился во время чтения — добиваем нулями до размера из заголовка w.warn("%s: файл изменился во время чтения (%d из %d байт)", p, n, hdr.Size) if _, err := io.CopyN(w.tw, zeroReader{}, hdr.Size-n); err != nil { return err } } return nil } func (w *Writer) Close() error { if err := w.tw.Close(); err != nil { w.zw.Close() return err } return w.zw.Close() } type zeroReader struct{} func (zeroReader) Read(p []byte) (int, error) { clear(p) return len(p), nil }