| 965 | } |
| 966 | |
| 967 | func (local *localFS) WriteFile(ctx context.Context, expectedChangeKind ChangeKind, path string, upperStat *types.Stat, upperFS ReadFS) (CachedChange, int64, error) { |
| 968 | var writtenBytes int64 |
| 969 | appliedChange, err := local.changeCache.getOrInit(ctx, local.cacheKey(path), func(ctx context.Context) (*ChangeWithStat, error) { |
| 970 | reader, err := upperFS.ReadFile(ctx, path) |
| 971 | if err != nil { |
| 972 | return nil, fmt.Errorf("failed to read file %q: %w", path, err) |
| 973 | } |
| 974 | defer reader.Close() |
| 975 | |
| 976 | fullPath := local.toFullPath(path) |
| 977 | |
| 978 | lowerStat, err := os.Lstat(fullPath) |
| 979 | if err != nil && !os.IsNotExist(err) { |
| 980 | return nil, fmt.Errorf("failed to stat existing path: %w", err) |
| 981 | } |
| 982 | |
| 983 | replacesExisting := lowerStat != nil |
| 984 | |
| 985 | if replacesExisting { |
| 986 | if err := os.RemoveAll(fullPath); err != nil { |
| 987 | return nil, fmt.Errorf("failed to remove existing file: %w", err) |
| 988 | } |
| 989 | } |
| 990 | |
| 991 | f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(upperStat.Mode)&os.ModePerm) |
| 992 | if err != nil { |
| 993 | return nil, err |
| 994 | } |
| 995 | defer f.Close() |
| 996 | |
| 997 | h := newHashFromStat(upperStat) |
| 998 | |
| 999 | copyBuf := copyBufferPool.Get().(*[]byte) |
| 1000 | written, err := io.CopyBuffer(io.MultiWriter(f, h), reader, *copyBuf) |
| 1001 | writtenBytes = written |
| 1002 | copyBufferPool.Put(copyBuf) |
| 1003 | if err != nil { |
| 1004 | return nil, fmt.Errorf("failed to copy contents: %w", err) |
| 1005 | } |
| 1006 | if err := f.Close(); err != nil { |
| 1007 | return nil, fmt.Errorf("failed to close file: %w", err) |
| 1008 | } |
| 1009 | |
| 1010 | if err := rewriteMetadata(fullPath, upperStat); err != nil { |
| 1011 | return nil, fmt.Errorf("failed to rewrite file metadata: %w", err) |
| 1012 | } |
| 1013 | |
| 1014 | // store the hash in an xattr so GetPreviousChange above can use that instead of re-hashing the file |
| 1015 | dgst := digest.NewDigest(hashutil.XXH3, h) |
| 1016 | if err := sysx.Setxattr(fullPath, hashXattrKey, []byte(dgst.String()), 0); err != nil { |
| 1017 | return nil, fmt.Errorf("failed to set content hash xattr: %w", err) |
| 1018 | } |
| 1019 | |
| 1020 | return &ChangeWithStat{ |
| 1021 | kind: expectedChangeKind, |
| 1022 | stat: &HashedStatInfo{ |
| 1023 | StatInfo: StatInfo{upperStat}, |
| 1024 | dgst: dgst, |