master
go 112 lines 3.29 KB
Raw
1 package common
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11
12 "github.com/ipfs/kubo/repo/fsrepo/migrations/atomicfile"
13 )
14
15 // CheckVersion verifies the repo is at the expected version
16 func CheckVersion(repoPath string, expectedVersion string) error {
17 versionPath := filepath.Join(repoPath, "version")
18 versionBytes, err := os.ReadFile(versionPath)
19 if err != nil {
20 return fmt.Errorf("could not read version file: %w", err)
21 }
22 version := strings.TrimSpace(string(versionBytes))
23 if version != expectedVersion {
24 return fmt.Errorf("expected version %s, got %s", expectedVersion, version)
25 }
26 return nil
27 }
28
29 // WriteVersion writes the version to the repo
30 func WriteVersion(repoPath string, version string) error {
31 versionPath := filepath.Join(repoPath, "version")
32 return os.WriteFile(versionPath, []byte(version), 0644)
33 }
34
35 // Must panics if the error is not nil. Use only for errors that cannot be handled gracefully.
36 func Must(err error) {
37 if err != nil {
38 panic(fmt.Errorf("error can't be dealt with transactionally: %w", err))
39 }
40 }
41
42 // WithBackup performs a config file operation with automatic backup and rollback on error
43 func WithBackup(configPath string, backupSuffix string, fn func(in io.ReadSeeker, out io.Writer) error) error {
44 // Read the entire file into memory first
45 // This allows us to close the file before doing atomic operations,
46 // which is necessary on Windows where open files can't be renamed
47 data, err := os.ReadFile(configPath)
48 if err != nil {
49 return fmt.Errorf("failed to read config file %s: %w", configPath, err)
50 }
51
52 // Create an in-memory reader for the data
53 in := bytes.NewReader(data)
54
55 // Create backup atomically to prevent partial backup on interruption
56 backupPath := configPath + backupSuffix
57 backup, err := atomicfile.New(backupPath, 0600)
58 if err != nil {
59 return fmt.Errorf("failed to create backup file for %s: %w", backupPath, err)
60 }
61 if _, err := backup.Write(data); err != nil {
62 Must(backup.Abort())
63 return fmt.Errorf("failed to write backup data: %w", err)
64 }
65 if err := backup.Close(); err != nil {
66 Must(backup.Abort())
67 return fmt.Errorf("failed to finalize backup: %w", err)
68 }
69
70 // Create output file atomically
71 out, err := atomicfile.New(configPath, 0600)
72 if err != nil {
73 // Clean up backup on error
74 os.Remove(backupPath)
75 return fmt.Errorf("failed to create atomic file for %s: %w", configPath, err)
76 }
77
78 // Run the conversion function
79 if err := fn(in, out); err != nil {
80 Must(out.Abort())
81 // Clean up backup on error
82 os.Remove(backupPath)
83 return fmt.Errorf("config conversion failed: %w", err)
84 }
85
86 // Close the output file atomically
87 Must(out.Close())
88 // Backup remains for potential revert
89
90 return nil
91 }
92
93 // RevertBackup restores a backup file
94 func RevertBackup(configPath string, backupSuffix string) error {
95 return os.Rename(configPath+backupSuffix, configPath)
96 }
97
98 // ReadConfig reads and unmarshals a JSON config file into a map
99 func ReadConfig(r io.Reader) (map[string]any, error) {
100 confMap := make(map[string]any)
101 if err := json.NewDecoder(r).Decode(&confMap); err != nil {
102 return nil, err
103 }
104 return confMap, nil
105 }
106
107 // WriteConfig marshals and writes a config map as indented JSON
108 func WriteConfig(w io.Writer, config map[string]any) error {
109 enc := json.NewEncoder(w)
110 enc.SetIndent("", " ")
111 return enc.Encode(config)
112 }