Go · 1616 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package storage
4
5 import (
6 "crypto/rand"
7 "encoding/hex"
8 "fmt"
9 "io"
10 "os"
11 "path/filepath"
12 )
13
14 // WriteAtomic writes src to path via a tempfile in the same directory,
15 // fsyncs, and renames. A crash between write and rename leaves the temp
16 // file behind (callers may sweep these on startup) but never a partial
17 // file at path.
18 //
19 // The temp file MUST live on the same mount as path so the rename is
20 // atomic — callers should not pass paths that cross mount points.
21 func WriteAtomic(path string, src io.Reader) error {
22 dir := filepath.Dir(path)
23 suffix, err := randomSuffix()
24 if err != nil {
25 return fmt.Errorf("storage: atomic: random suffix: %w", err)
26 }
27 tmp := filepath.Join(dir, "."+filepath.Base(path)+".tmp."+suffix)
28
29 f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
30 if err != nil {
31 return fmt.Errorf("storage: atomic: open temp: %w", err)
32 }
33 cleanup := func() { _ = os.Remove(tmp) }
34
35 if _, err := io.Copy(f, src); err != nil {
36 _ = f.Close()
37 cleanup()
38 return fmt.Errorf("storage: atomic: copy: %w", err)
39 }
40 if err := f.Sync(); err != nil {
41 _ = f.Close()
42 cleanup()
43 return fmt.Errorf("storage: atomic: fsync: %w", err)
44 }
45 if err := f.Close(); err != nil {
46 cleanup()
47 return fmt.Errorf("storage: atomic: close: %w", err)
48 }
49 if err := os.Rename(tmp, path); err != nil {
50 cleanup()
51 return fmt.Errorf("storage: atomic: rename: %w", err)
52 }
53 return nil
54 }
55
56 func randomSuffix() (string, error) {
57 var b [8]byte
58 if _, err := rand.Read(b[:]); err != nil {
59 return "", err
60 }
61 return hex.EncodeToString(b[:]), nil
62 }
63