package fs import ( "fmt" "io" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "sync" "sync/atomic" "time" "github.com/VictoriaMetrics/VictoriaMetrics/lib/fasttime" "github.com/VictoriaMetrics/VictoriaMetrics/lib/filestream" "github.com/VictoriaMetrics/VictoriaMetrics/lib/logger" ) var tmpFileNum uint64 // MustSyncPath syncs contents of the given path. func MustSyncPath(path string) { mustSyncPath(path) } // WriteFileAndSync writes data to the file at path and then calls fsync on the created file. // // The fsync guarantees that the written data survives hardware reset after successful call. // // This function may leave the file at the path in inconsistent state on app crash // in the middle of the write. // Use WriteFileAtomically if the file at the path must be either written in full // or not written at all on app crash in the middle of the write. func WriteFileAndSync(path string, data []byte) error { f, err := filestream.Create(path, false) if err != nil { return err } if _, err := f.Write(data); err != nil { f.MustClose() // Do not call MustRemoveAll(path), so the user could inpsect // the file contents during investigation of the issue. return fmt.Errorf("cannot write %d bytes to %q: %w", len(data), path, err) } // Sync and close the file. f.MustClose() return nil } // WriteFileAtomically atomically writes data to the given file path. // // This function returns only after the file is fully written and synced // to the underlying storage. // // This function guarantees that the file at path either fully written or not written at all on app crash // in the middle of the write. // // If the file at path already exists, then the file is overwritten atomically if canOverwrite is true. // Otherwise error is returned. func WriteFileAtomically(path string, data []byte, canOverwrite bool) error { // Check for the existing file. It is expected that // the WriteFileAtomically function cannot be called concurrently // with the same `path`. if IsPathExist(path) && !canOverwrite { return fmt.Errorf("cannot create file %q, since it already exists", path) } // Write data to a temporary file. n := atomic.AddUint64(&tmpFileNum, 1) tmpPath := fmt.Sprintf("%s.tmp.%d", path, n) if err := WriteFileAndSync(tmpPath, data); err != nil { return fmt.Errorf("cannot write data to temporary file: %w", err) } // Atomically move the temporary file from tmpPath to path. if err := os.Rename(tmpPath, path); err != nil { // do not call MustRemoveAll(tmpPath) here, so the user could inspect // the file contents during investigation of the issue. return fmt.Errorf("cannot move temporary file %q to %q: %w", tmpPath, path, err) } // Sync the containing directory, so the file is guaranteed to appear in the directory. // See https://www.quora.com/When-should-you-fsync-the-containing-directory-in-addition-to-the-file-itself absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("cannot obtain absolute path to %q: %w", path, err) } parentDirPath := filepath.Dir(absPath) MustSyncPath(parentDirPath) return nil } // IsTemporaryFileName returns true if fn matches temporary file name pattern // from WriteFileAtomically. func IsTemporaryFileName(fn string) bool { return tmpFileNameRe.MatchString(fn) } // tmpFileNameRe is regexp for temporary file name - see WriteFileAtomically for details. var tmpFileNameRe = regexp.MustCompile(`\.tmp\.\d+$`) // MkdirAllIfNotExist creates the given path dir if it isn't exist. func MkdirAllIfNotExist(path string) error { if IsPathExist(path) { return nil } return mkdirSync(path) } // MkdirAllFailIfExist creates the given path dir if it isn't exist. // // Returns error if path already exists. func MkdirAllFailIfExist(path string) error { if IsPathExist(path) { return fmt.Errorf("the %q already exists", path) } return mkdirSync(path) } func mkdirSync(path string) error { if err := os.MkdirAll(path, 0755); err != nil { return err } // Sync the parent directory, so the created directory becomes visible // in the fs after power loss. parentDirPath := filepath.Dir(path) MustSyncPath(parentDirPath) return nil } // RemoveDirContents removes all the contents of the given dir if it exists. // // It doesn't remove the dir itself, so the dir may be mounted // to a separate partition. func RemoveDirContents(dir string) { if !IsPathExist(dir) { // The path doesn't exist, so nothing to remove. return } d, err := os.Open(dir) if err != nil { logger.Panicf("FATAL: cannot open dir: %s", err) } defer MustClose(d) names, err := d.Readdirnames(-1) if err != nil { logger.Panicf("FATAL: cannot read contents of the dir %q: %s", dir, err) } for _, name := range names { if name == "." || name == ".." || name == "lost+found" { // Skip special dirs. continue } fullPath := dir + "/" + name MustRemoveAll(fullPath) } MustSyncPath(dir) } // MustClose must close the given file f. func MustClose(f *os.File) { fname := f.Name() if err := f.Close(); err != nil { logger.Panicf("FATAL: cannot close %q: %s", fname, err) } } // MustFileSize returns file size for the given path. func MustFileSize(path string) uint64 { fi, err := os.Stat(path) if err != nil { logger.Panicf("FATAL: cannot stat %q: %s", path, err) } if fi.IsDir() { logger.Panicf("FATAL: %q must be a file, not a directory", path) } return uint64(fi.Size()) } // IsPathExist returns whether the given path exists. func IsPathExist(path string) bool { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return false } logger.Panicf("FATAL: cannot stat %q: %s", path, err) } return true } func mustSyncParentDirIfExists(path string) { parentDirPath := filepath.Dir(path) if !IsPathExist(parentDirPath) { return } MustSyncPath(parentDirPath) } // IsEmptyDir returns true if path points to empty directory. func IsEmptyDir(path string) bool { // See https://stackoverflow.com/a/30708914/274937 f, err := os.Open(path) if err != nil { logger.Panicf("FATAL: cannot open dir: %s", err) } _, err = f.Readdirnames(1) MustClose(f) if err != nil { if err == io.EOF { return true } logger.Panicf("FATAL: unexpected error when reading directory %q: %s", path, err) } return false } // MustRemoveDirAtomic removes the given dir atomically. // // It uses the following algorithm: // // 1. Atomically rename the "