Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

111 lines
2.8 KiB
Go

package remotesync
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"sort"
)
// ManifestEntry describes one regular file available for remote sync.
// MtimeNS is Unix nanoseconds from the server's stat; combined with
// PAX tar headers it gives the client's mirror diff the same
// (size, mtime) change signal local sync uses.
type ManifestEntry struct {
Path string `json:"path"`
Size int64 `json:"size"`
MtimeNS int64 `json:"mtime_ns"`
}
// Manifest lists every syncable regular file under a TargetSet.
type Manifest struct {
Files []ManifestEntry `json:"files"`
}
// BuildManifest walks the target dirs and extra files and returns a
// manifest of regular files, sorted by path. Symlinks and special
// files are excluded, matching WriteArchive. Missing roots and extra
// files are tolerated: sync races against live agents deleting files.
//
// File-scoped agents (Windsurf) are rejected: their raw directory tree
// differs from the sanitized subset WriteArchive streams, so a manifest
// of the raw walk would advertise files the full archive never exports.
// Callers must route such targets through the full-archive flow.
func BuildManifest(targets TargetSet) (Manifest, error) {
if targets.HasFileScopedAgents() {
return Manifest{}, fmt.Errorf(
"manifest not supported for file-scoped agents")
}
m := Manifest{Files: []ManifestEntry{}}
add := func(path string, info os.FileInfo) {
m.Files = append(m.Files, ManifestEntry{
Path: path,
Size: info.Size(),
MtimeNS: info.ModTime().UnixNano(),
})
}
for _, dirs := range targets.Dirs {
for _, root := range dirs {
if err := manifestWalk(root, add); err != nil {
return Manifest{}, err
}
}
}
for _, path := range targets.ExtraFiles {
info, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
continue
}
return Manifest{}, fmt.Errorf("stat manifest file %q: %w", path, err)
}
if info.Mode().IsRegular() {
add(path, info)
}
}
sort.Slice(m.Files, func(i, j int) bool {
return m.Files[i].Path < m.Files[j].Path
})
return m, nil
}
func manifestWalk(root string, add func(string, os.FileInfo)) error {
info, err := os.Lstat(root)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("stat manifest root %q: %w", root, err)
}
if info.Mode()&os.ModeSymlink != 0 {
return nil
}
if !info.IsDir() {
if info.Mode().IsRegular() {
add(root, info)
}
return nil
}
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
info, err := entry.Info()
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
return nil
}
add(path, info)
return nil
})
}