package real import ( "io" "os" "path/filepath" "strconv" "strings" "sync/atomic" "pad/internal/io/pool/types" ) // RealFileSystem implements the pool.FileSystem interface using the real OS filesystem. type RealFileSystem struct { WorkingDir string } // NewRealFileSystem creates a RealFileSystem rooted at the given working directory. // If workingDir is empty, it defaults to "/". func NewRealFileSystem(workingDir string) *RealFileSystem { if workingDir == "" { workingDir = "/" } abs, err := filepath.Abs(workingDir) if err != nil { abs = workingDir } return &RealFileSystem{WorkingDir: abs} } func (fs *RealFileSystem) ReadDir(path string) ([]types.DirEntry, error) { entries, err := os.ReadDir(filepath.Join(fs.WorkingDir, path)) if err != nil { return nil, err } var res []types.DirEntry for _, e := range entries { res = append(res, &realDirEntry{e}) } return res, nil } func (fs *RealFileSystem) DirExists(path string) bool { info, err := os.Stat(filepath.Join(fs.WorkingDir, path)) return err == nil && info.IsDir() } func (fs *RealFileSystem) FileExists(path string) bool { info, err := os.Stat(filepath.Join(fs.WorkingDir, path)) return err == nil && !info.IsDir() } func (fs *RealFileSystem) ReadFile(path string) ([]byte, error) { return os.ReadFile(filepath.Join(fs.WorkingDir, path)) } func (fs *RealFileSystem) ReadFileAt(path string, offset, size int) ([]byte, error) { fullPath := filepath.Join(fs.WorkingDir, path) f, err := os.Open(fullPath) if err != nil { return nil, err } defer f.Close() buf := make([]byte, size) n, err := f.ReadAt(buf, int64(offset)) if err != nil && err != io.EOF { return nil, err } return buf[:n], nil } func (fs *RealFileSystem) WriteFile(path string, content []byte) error { // Simple write for backward compatibility if needed, // but defer to Atomic implementation. return fs.WriteFileAtomic(path, content) } // tmpSeq numbers the per-call temp files so two writes never share a staging // path. Combined with the owner-side per-file write serialization this makes // same-file write interleaving structurally impossible. var tmpSeq atomic.Uint64 func (fs *RealFileSystem) WriteFileAtomic(path string, content []byte) error { // Atomic write implementation fullPath := filepath.Join(fs.WorkingDir, path) // Temp file in the same directory as the target to ensure same filesystem // rename. Unique per call ("..tmp.."): even a concurrent // write or a crashed process's leftover cannot interleave with this one // on the staging file, and rename promotes only this call's complete // content. base := filepath.Base(fullPath) tmpPath := filepath.Join(filepath.Dir(fullPath), "."+base+".tmp."+strconv.Itoa(os.Getpid())+"."+strconv.FormatUint(tmpSeq.Add(1), 10)) if err := os.MkdirAll(filepath.Dir(tmpPath), 0755); err != nil { return err } if err := os.WriteFile(tmpPath, content, 0644); err != nil { os.Remove(tmpPath) return err } fs.removeStaleTemps(filepath.Dir(fullPath), base, tmpPath) if err := os.Rename(tmpPath, fullPath); err != nil { os.Remove(tmpPath) return err } return nil } // removeStaleTemps best-effort removes leftover staging files for the same // file: any "..tmp.." that is not the caller's own temp, plus // the legacy deterministic name "..tmp" from older app versions. A // live write never owns either: same-file writes are serialized by the logic // owner, and a live temp always carries the caller's own pid/seq (excluded // via keepTmp). Errors are ignored. func (fs *RealFileSystem) removeStaleTemps(dir, base, keepTmp string) { entries, err := os.ReadDir(dir) if err != nil { return } keep := filepath.Base(keepTmp) prefix := "." + base + ".tmp." legacy := "." + base + ".tmp" for _, e := range entries { name := e.Name() if name == keep { continue // this call's own live temp file } if name == legacy || strings.HasPrefix(name, prefix) { os.Remove(filepath.Join(dir, name)) } } } func (fs *RealFileSystem) DeleteFile(path string) error { return os.Remove(filepath.Join(fs.WorkingDir, path)) } func (fs *RealFileSystem) CreateDir(path string) error { return os.MkdirAll(filepath.Join(fs.WorkingDir, path), 0755) } // realDirEntry wraps os.DirEntry type realDirEntry struct { entry os.DirEntry } func (e *realDirEntry) Name() string { return e.entry.Name() } func (e *realDirEntry) IsDir() bool { return e.entry.IsDir() } func (e *realDirEntry) Info() (os.FileInfo, error) { return e.entry.Info() }