1// Package capturefile reserves capture output paths and removes only the file
2// that was reserved, never a different file that later took its place.
3package capturefile
4
5import (
6 "errors"
7 "fmt"
8 "io/fs"
9 "os"
10
11 "github.com/bnema/nefercap/internal/ports"
12)
13
14// Reservation identifies the file created by Create.
15type Reservation struct {
16 path string
17 info fs.FileInfo
18}
19
20// Create makes a new private (0600) file at path with O_EXCL, so an existing
21// file or symlink is never opened or overwritten. An existing path yields an
22// error matching both ports.ErrPathExists and fs.ErrExist. The returned
23// Reservation records the file's identity for a later Remove.
24func Create(path string) (*os.File, *Reservation, error) {
25 return create(path, os.O_WRONLY)
26}
27
28// CreateReadable reserves a file whose original descriptor can be reused for copying.
29func CreateReadable(path string) (*os.File, *Reservation, error) {
30 return create(path, os.O_RDWR)
31}
32
33func create(path string, access int) (*os.File, *Reservation, error) {
34 f, err := os.OpenFile(path, access|os.O_CREATE|os.O_EXCL, 0o600)
35 if err != nil {
36 if errors.Is(err, fs.ErrExist) {
37 return nil, nil, fmt.Errorf("%w: %w", ports.ErrPathExists, err)
38 }
39 return nil, nil, fmt.Errorf("create output: %w", err)
40 }
41 info, err := f.Stat()
42 if err != nil {
43 _ = f.Close()
44 _ = os.Remove(path) // just created by us; identity is unknown, so remove now
45 return nil, nil, fmt.Errorf("stat output: %w", err)
46 }
47 return f, &Reservation{path: path, info: info}, nil
48}
49
50// Owns reports whether path still names the reserved file, so a moved and
51// replaced or deleted output is never reported as saved.
52func (r *Reservation) Owns() bool {
53 cur, err := os.Lstat(r.path)
54 return err == nil && os.SameFile(r.info, cur)
55}
56
57// Remove deletes the reserved file if path still names it. A missing path is
58// success; a path that now names a different file (the original was moved and
59// replaced) is left untouched and is not an error. There is an unavoidable
60// short window between the identity check and the unlink.
61func (r *Reservation) Remove() error {
62 cur, err := os.Lstat(r.path)
63 if err != nil {
64 if errors.Is(err, fs.ErrNotExist) {
65 return nil
66 }
67 return fmt.Errorf("inspect output: %w", err)
68 }
69 if !os.SameFile(r.info, cur) {
70 return nil
71 }
72 if err := os.Remove(r.path); err != nil && !errors.Is(err, fs.ErrNotExist) {
73 return fmt.Errorf("remove output: %w", err)
74 }
75 return nil
76}