package filestore import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "fmt" "hash" "io" "os" "path/filepath" ) var ErrDestinationConflict = errors.New("destination already exists with different content") // Identity 是不可变文件在进入事务目录前必须满足的身份。 type Identity struct { Size int64 SHA256 string } // File 是一次原子提交的结果。 type File struct { Path string Identity Identity Reused bool } // Store 在一个 daemon 独占管理的本地根目录中保存不可变文件。 type Store struct { root string } // New 创建文件存储并固定其规范根目录。 func New(root string) (*Store, error) { if root == "" { return nil, errors.New("file store root is required") } absRoot, err := filepath.Abs(root) if err != nil { return nil, fmt.Errorf("resolve file store root: %w", err) } if err := os.MkdirAll(absRoot, 0o750); err != nil { return nil, fmt.Errorf("create file store root: %w", err) } resolvedRoot, err := filepath.EvalSymlinks(absRoot) if err != nil { return nil, fmt.Errorf("resolve file store symlinks: %w", err) } return &Store{root: resolvedRoot}, nil } // Commit 把内容写入同目录临时文件,校验后通过硬链接原子创建最终文件。 // 最终文件已经存在且身份相同时按幂等成功处理,内容不同时拒绝覆盖。 func (s *Store) Commit(relativePath string, source io.Reader, expected Identity) (File, error) { if source == nil { return File{}, errors.New("file source is required") } expectedDigest, err := validateIdentity(expected) if err != nil { return File{}, err } if !filepath.IsLocal(relativePath) || relativePath == "." { return File{}, fmt.Errorf("file store path is not a local relative path: %q", relativePath) } target := filepath.Join(s.root, filepath.Clean(relativePath)) parent := filepath.Dir(target) if err := os.MkdirAll(parent, 0o750); err != nil { return File{}, fmt.Errorf("create destination directory: %w", err) } if err := s.verifyParent(parent); err != nil { return File{}, err } if existing, found, err := verifyExisting(target, expected, expectedDigest); err != nil { return File{}, err } else if found { return existing, nil } temp, err := os.CreateTemp(parent, ".incoming-*") if err != nil { return File{}, fmt.Errorf("create temporary file: %w", err) } tempPath := temp.Name() preserveTemp := false defer func() { if !preserveTemp { _ = os.Remove(tempPath) } }() if err := temp.Chmod(0o640); err != nil { _ = temp.Close() return File{}, fmt.Errorf("set temporary file permissions: %w", err) } digest := sha256.New() size, copyErr := io.Copy(io.MultiWriter(temp, digest), source) if copyErr != nil { _ = temp.Close() return File{}, fmt.Errorf("write temporary file: %w", copyErr) } if size != expected.Size { _ = temp.Close() return File{}, fmt.Errorf("file size mismatch: got %d, want %d", size, expected.Size) } if !sameDigest(digest, expectedDigest) { _ = temp.Close() return File{}, errors.New("file SHA-256 mismatch") } if err := temp.Sync(); err != nil { _ = temp.Close() return File{}, fmt.Errorf("flush temporary file: %w", err) } if err := temp.Close(); err != nil { return File{}, fmt.Errorf("close temporary file: %w", err) } if err := os.Link(tempPath, target); err != nil { if existing, found, verifyErr := verifyExisting(target, expected, expectedDigest); verifyErr != nil { return File{}, verifyErr } else if found { return existing, nil } return File{}, fmt.Errorf("atomically create destination file: %w", err) } if err := syncDirectory(parent); err != nil { // 最终路径已经可见,保留临时硬链接,避免在目录落盘失败时继续改变现场。 preserveTemp = true return File{}, err } if err := os.Remove(tempPath); err != nil { preserveTemp = true return File{}, fmt.Errorf("remove committed temporary link: %w", err) } if err := syncDirectory(parent); err != nil { return File{}, err } return File{Path: target, Identity: expected}, nil } func (s *Store) verifyParent(parent string) error { resolvedParent, err := filepath.EvalSymlinks(parent) if err != nil { return fmt.Errorf("resolve destination directory symlinks: %w", err) } relative, err := filepath.Rel(s.root, resolvedParent) if err != nil { return fmt.Errorf("compare destination directory with store root: %w", err) } if relative != "." && !filepath.IsLocal(relative) { return fmt.Errorf("destination directory escapes file store root: %q", parent) } return nil } func validateIdentity(identity Identity) ([]byte, error) { if identity.Size < 0 { return nil, errors.New("expected file size must not be negative") } digest, err := hex.DecodeString(identity.SHA256) if err != nil || len(digest) != sha256.Size { return nil, errors.New("expected SHA-256 must be a 64-character hexadecimal value") } return digest, nil } func verifyExisting(path string, expected Identity, expectedDigest []byte) (File, bool, error) { info, err := os.Lstat(path) if errors.Is(err, os.ErrNotExist) { return File{}, false, nil } if err != nil { return File{}, false, fmt.Errorf("inspect destination file: %w", err) } if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { return File{}, false, fmt.Errorf("destination is not a regular file: %s", path) } if info.Size() != expected.Size { return File{}, false, ErrDestinationConflict } file, err := os.Open(path) if err != nil { return File{}, false, fmt.Errorf("open existing destination file: %w", err) } digest := sha256.New() _, copyErr := io.Copy(digest, file) closeErr := file.Close() if err := errors.Join(copyErr, closeErr); err != nil { return File{}, false, fmt.Errorf("hash existing destination file: %w", err) } if !sameDigest(digest, expectedDigest) { return File{}, false, ErrDestinationConflict } return File{Path: path, Identity: expected, Reused: true}, true, nil } func sameDigest(actual hash.Hash, expected []byte) bool { return subtle.ConstantTimeCompare(actual.Sum(nil), expected) == 1 } func syncDirectory(path string) error { directory, err := os.Open(path) if err != nil { return fmt.Errorf("open directory for flush: %w", err) } syncErr := directory.Sync() closeErr := directory.Close() if err := errors.Join(syncErr, closeErr); err != nil { return fmt.Errorf("flush directory: %w", err) } return nil }