Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
93 lines
2.6 KiB
Go
93 lines
2.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
|
|
|
package store
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// atomicWriteIn replaces name inside the root: a temporary file in the
|
|
// same directory, fsync, rename, and a directory fsync.
|
|
func atomicWriteIn(root *os.Root, name string, data []byte) error {
|
|
dir := path.Dir(name)
|
|
base := path.Base(name)
|
|
handle, err := root.OpenFile(path.Join(dir, "."+base+".tmp"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("create temp file: %w", err)
|
|
}
|
|
if _, err := handle.Write(data); err != nil {
|
|
handle.Close()
|
|
root.Remove(path.Join(dir, "."+base+".tmp"))
|
|
return fmt.Errorf("write: %w", err)
|
|
}
|
|
if err := handle.Sync(); err != nil {
|
|
handle.Close()
|
|
root.Remove(path.Join(dir, "."+base+".tmp"))
|
|
return fmt.Errorf("sync: %w", err)
|
|
}
|
|
if err := handle.Close(); err != nil {
|
|
root.Remove(path.Join(dir, "."+base+".tmp"))
|
|
return fmt.Errorf("close: %w", err)
|
|
}
|
|
if err := root.Rename(path.Join(dir, "."+base+".tmp"), name); err != nil {
|
|
root.Remove(path.Join(dir, "."+base+".tmp"))
|
|
return fmt.Errorf("replace: %w", err)
|
|
}
|
|
syncDirIn(root, dir)
|
|
return nil
|
|
}
|
|
|
|
// syncDirIn flushes a directory entry inside the root, so the rename is
|
|
// durable.
|
|
func syncDirIn(root *os.Root, dir string) {
|
|
handle, err := root.Open(dir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer handle.Close()
|
|
_ = handle.Sync()
|
|
}
|
|
|
|
func absPath(path string) string {
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return path
|
|
}
|
|
return abs
|
|
}
|
|
|
|
// within reports whether path is inside directory, comparing resolved
|
|
// paths so that a symlinked content directory is not mistaken for an
|
|
// escape. A path that does not exist yet is resolved through its
|
|
// longest existing ancestor, which is what a new post file is.
|
|
func within(directory, path string) bool {
|
|
dirResolved := resolveExisting(directory)
|
|
pathResolved := resolveExisting(path)
|
|
if pathResolved == dirResolved {
|
|
return true
|
|
}
|
|
rel, err := filepath.Rel(dirResolved, pathResolved)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
}
|
|
|
|
// resolveExisting resolves symlinks in the longest existing prefix of
|
|
// path and appends the remainder unchanged.
|
|
func resolveExisting(path string) string {
|
|
if resolved, err := filepath.EvalSymlinks(path); err == nil {
|
|
return resolved
|
|
}
|
|
parent := filepath.Dir(path)
|
|
if parent == path || parent == "." {
|
|
return path
|
|
}
|
|
return filepath.Join(resolveExisting(parent), filepath.Base(path))
|
|
}
|