package hostfiles import ( "context" "errors" "fmt" "io" "os" "path" "sort" ) type fileState int const ( fileOK fileState = iota fileTorn fileGone fileNotRegular fileTooLarge fileUnreadable fileRetry ) // Test seams. betweenLookAndOpen runs after something has been inspected // and before it is opened; readAll is the one place file contents are read. var ( betweenLookAndOpen = func() {} readAll = io.ReadAll ) // SyncStats reports one pass. Retry lists regular files replaced between // the look and the open: nothing was read and nothing staged for them this // pass. Torn lists files staged from a read during which the file changed, // so their staged bytes may be an inconsistent image. type SyncStats struct { Published, Deleted int TooLarge, NotRegular, Unreadable []string Retry, Torn []string } // skipped reports whether rel, or any directory above it, is excluded. func skipped(skip func(string, bool) bool, rel string, isDir bool) bool { if skip == nil { return false } if skip(rel, isDir) { return true } for dir := path.Dir(rel); dir != "."; dir = path.Dir(dir) { if skip(dir, true) { return true } } return false } // SyncDir stages every difference between dir and the manifest: changed // and new regular files, and a tombstone for each manifest path that is // no longer there. It does not commit. skip, when non-nil, excludes a // relative path and, for a directory, everything under it; an excluded // path is left exactly as the manifest has it. // // A tombstone needs proof of absence. A file that exists but could not be // read keeps its manifest entry and is reported in Unreadable. A root // that cannot be opened is an error and stages nothing: an unreadable // directory is not an empty one. func SyncDir(ctx context.Context, p *Publisher, dir string, skip func(rel string, isDir bool) bool) (SyncStats, error) { root, err := openTree(dir) if err != nil { return SyncStats{}, fmt.Errorf("hostfiles: sync %s: %w", dir, err) } defer root.Close() return SyncDirAt(ctx, p, root.f, skip) } // MaxPathBytes caps a path in a files tree, relative to its root: PATH_MAX // (4096) less its terminating NUL, as moves' archive contract has it. It is // the one cap every writer (Stage, StageSkipped, StageDelete, Commit, the // watcher's publishOne, a walk) and every reader (checkManifest: Restore, // CopyManifest, ValidateManifest) applies, so no manifest is written that // a reader refuses. Every path it admits can be opened by one openat2 // beneath the restored root (Present). It also bounds depth (about 2048 // components of one byte), and with it a walk's open directories and its // path strings. const MaxPathBytes = 4095 // ErrTooDeep is a path past MaxPathBytes: a writer refuses to stage it, // and the walk or manifest that holds it is refused whole. var ErrTooDeep = errors.New("hostfiles: a path is past the tree's length cap") // withinTreeCap is the one cap the writers and readers share. func withinTreeCap(rel string) error { if n := len(rel); n > MaxPathBytes { return fmt.Errorf("%w: %d bytes (cap %d), starting %q", ErrTooDeep, n, MaxPathBytes, rel[:64]) } return nil } // SyncDirAt is SyncDir over a directory the caller has already opened, and // does not close. SyncDir opens its root by path and follows a link there // (the path is the platform's own); a caller syncing ground someone else // can write opens the root itself without following anything // (instancefs.OpenDirBeneath) and syncs here, so the tree published is the // directory it vetted, whatever is renamed into the path afterwards. // Beneath the root nothing is followed either way. func SyncDirAt(ctx context.Context, p *Publisher, dir *os.File, skip func(rel string, isDir bool) bool) (SyncStats, error) { root, err := handleOf(dir) if err != nil { return SyncStats{}, err } return syncRoot(ctx, p, root, skip) } func syncRoot(ctx context.Context, p *Publisher, root *dirHandle, skip func(rel string, isDir bool) bool) (SyncStats, error) { var st SyncStats seen := map[string]bool{} err := syncTree(ctx, root, p, skip, func(d *dirHandle, name, rel string) error { if err := ctx.Err(); err != nil { return err } body, mode, state, err := readStable(d, name) if err != nil { return err } switch state { case fileGone: return nil case fileNotRegular: st.NotRegular = append(st.NotRegular, rel) return nil case fileUnreadable: seen[rel] = true // it exists: keep what the manifest has st.Unreadable = append(st.Unreadable, rel) return nil case fileRetry: seen[rel] = true // replaced mid-look: the next scan reads it st.Retry = append(st.Retry, rel) return nil case fileTooLarge: seen[rel] = true st.TooLarge = append(st.TooLarge, rel) return p.StageSkipped(rel, "too-large") } seen[rel] = true before := p.pending() if err := p.Stage(ctx, rel, body, mode, state == fileTorn); err != nil { return err } if p.pending() != before { st.Published++ } if state == fileTorn { st.Torn = append(st.Torn, rel) } return nil }) if err != nil { return st, fmt.Errorf("hostfiles: sync %s: %w", root.Name(), err) } for _, rel := range p.Paths() { if seen[rel] || skipped(skip, rel, false) { continue } if err := p.StageDelete(rel); err != nil { return st, err } st.Deleted++ } sort.Strings(st.TooLarge) sort.Strings(st.NotRegular) sort.Strings(st.Unreadable) sort.Strings(st.Retry) sort.Strings(st.Torn) return st, nil }