package busarchive import ( "errors" "fmt" "io/fs" "os" "path/filepath" "strings" "time" ) // StreamReport is one stream of a verified snapshot. type StreamReport struct { Name string `json:"name"` LastSeq uint64 `json:"last_seq"` } // Report is the answer of VerifyFile: which file was held to its manifest, and whether it passed. type Report struct { File string `json:"file"` Modified time.Time `json:"modified"` Size int64 `json:"size"` Taken *time.Time `json:"taken,omitempty"` Streams []StreamReport `json:"streams"` Skipped []Skipped `json:"skipped,omitempty"` Passed bool `json:"passed"` // Fault is the first thing wrong, empty when the snapshot passed. Fault string `json:"fault,omitempty"` } // VerifyFile verifies one snapshot inside dir: the file called name, or the newest *.tar when name is // empty. Nothing outside dir is read and nothing is written there; a snapshot that fails is an answer // (Passed false, the first Fault), and only a directory without a snapshot is an error. func VerifyFile(dir, name string) (*Report, error) { path, info, err := pick(dir, name) if err != nil { return nil, err } r := &Report{File: filepath.Base(path), Modified: info.ModTime().UTC(), Size: info.Size(), Streams: []StreamReport{}} f, err := os.Open(path) if err != nil { r.Fault = err.Error() return r, nil } defer f.Close() a, err := ReadArchive(f, "") if err != nil { r.Fault = err.Error() return r, nil } defer a.Close() taken := a.Manifest.Finished r.Taken = &taken r.Skipped = a.Manifest.Skipped for _, s := range a.Manifest.Streams { r.Streams = append(r.Streams, StreamReport{Name: s.Name, LastSeq: s.LastSeq}) } if err := a.Verify(); err != nil { r.Fault = err.Error() return r, nil } r.Passed = true return r, nil } // pick names the regular file to verify, and nothing outside dir: a name is one path component, and a // symbolic link is not followed out of the directory. func pick(dir, name string) (string, fs.FileInfo, error) { if dir == "" { return "", nil, errors.New("the snapshots directory is not set") } if name != "" { if name != filepath.Base(name) || name == "." || name == ".." || strings.ContainsRune(name, 0) { return "", nil, fmt.Errorf("%q is not a file name inside the snapshots directory", name) } p := filepath.Join(dir, name) info, err := os.Lstat(p) if err != nil { return "", nil, fmt.Errorf("no snapshot %s in the snapshots directory: %w", name, err) } if !info.Mode().IsRegular() { return "", nil, fmt.Errorf("%s is not a regular file", name) } return p, info, nil } entries, err := os.ReadDir(dir) if err != nil { return "", nil, fmt.Errorf("the snapshots directory does not read: %w", err) } var best string var bestInfo fs.FileInfo for _, e := range entries { if !strings.HasSuffix(e.Name(), ".tar") { continue } info, err := e.Info() if err != nil || !info.Mode().IsRegular() { continue } if bestInfo == nil || info.ModTime().After(bestInfo.ModTime()) { best, bestInfo = e.Name(), info } } if bestInfo == nil { return "", nil, errors.New("the snapshots directory holds no .tar snapshot") } return filepath.Join(dir, best), bestInfo, nil }