transaction.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. package backup
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. )
  10. type restoreFile struct {
  11. path string
  12. before []byte
  13. after []byte
  14. mode os.FileMode
  15. existed bool
  16. }
  17. type restoreJournal struct {
  18. DebtikDir string `json:"debtik_dir"`
  19. NetworkDir string `json:"network_dir"`
  20. NewDir string `json:"new_dir,omitempty"`
  21. OldDir string `json:"old_dir,omitempty"`
  22. OldExists bool `json:"old_exists"`
  23. HasDebtik bool `json:"has_debtik"`
  24. Committed bool `json:"committed"`
  25. Files []journalFile `json:"files"`
  26. }
  27. type journalFile struct {
  28. Path string `json:"path"`
  29. Data []byte `json:"data,omitempty"`
  30. Mode os.FileMode `json:"mode"`
  31. Existed bool `json:"existed"`
  32. }
  33. func (m *Manager) journalPath() string {
  34. return filepath.Join(m.backupStore, "restore-transaction.json")
  35. }
  36. func (m *Manager) writeJournal(j *restoreJournal) error {
  37. if err := os.MkdirAll(m.backupStore, 0700); err != nil {
  38. return err
  39. }
  40. data, err := json.Marshal(j)
  41. if err != nil {
  42. return err
  43. }
  44. return restoreWrite(m.journalPath(), data, 0600)
  45. }
  46. func syncRestoreDir(path string) error {
  47. d, err := os.Open(path)
  48. if err != nil {
  49. return err
  50. }
  51. defer d.Close()
  52. return d.Sync()
  53. }
  54. // RecoverInterruptedRestore must be called before managers load configuration.
  55. func (m *Manager) RecoverInterruptedRestore() (bool, error) {
  56. m.mu.Lock()
  57. defer m.mu.Unlock()
  58. return m.recoverInterruptedRestoreLocked()
  59. }
  60. func (m *Manager) recoverInterruptedRestoreLocked() (bool, error) {
  61. data, err := os.ReadFile(m.journalPath())
  62. if os.IsNotExist(err) {
  63. return false, nil
  64. }
  65. if err != nil {
  66. return false, err
  67. }
  68. var j restoreJournal
  69. if err = json.Unmarshal(data, &j); err != nil {
  70. return false, err
  71. }
  72. if j.DebtikDir != m.debtikDir || j.NetworkDir != m.networkDir {
  73. return false, errors.New("restore journal paths do not match configured paths")
  74. }
  75. parent := filepath.Dir(m.debtikDir)
  76. if j.HasDebtik && (j.NewDir == "" || (j.OldExists && j.OldDir == "") || (!j.OldExists && j.OldDir != "")) {
  77. return false, errors.New("invalid restore journal directory state")
  78. }
  79. if !j.HasDebtik && (j.NewDir != "" || j.OldDir != "" || j.OldExists) {
  80. return false, errors.New("invalid restore journal directory state")
  81. }
  82. for _, p := range []string{j.NewDir, j.OldDir} {
  83. if p != "" && (filepath.Dir(p) != parent || !strings.HasPrefix(filepath.Base(p), ".debtik-restore-")) {
  84. return false, errors.New("invalid restore journal staging path")
  85. }
  86. }
  87. for _, f := range j.Files {
  88. if filepath.Dir(f.Path) != m.networkDir || !strings.HasSuffix(f.Path, ".cfg") {
  89. return false, errors.New("invalid restore journal network path")
  90. }
  91. }
  92. if !j.Committed {
  93. for _, f := range j.Files {
  94. if f.Existed {
  95. if err = os.MkdirAll(m.networkDir, 0755); err == nil {
  96. err = restoreWrite(f.Path, f.Data, f.Mode)
  97. }
  98. } else {
  99. err = os.Remove(f.Path)
  100. if os.IsNotExist(err) {
  101. err = nil
  102. }
  103. }
  104. if err != nil {
  105. return false, fmt.Errorf("recovering %s: %w", f.Path, err)
  106. }
  107. }
  108. if j.HasDebtik {
  109. if j.OldExists {
  110. if _, e := os.Lstat(j.OldDir); e == nil {
  111. if err = os.RemoveAll(m.debtikDir); err != nil {
  112. return false, err
  113. }
  114. if err = os.Rename(j.OldDir, m.debtikDir); err != nil {
  115. return false, err
  116. }
  117. if err = syncRestoreDir(parent); err != nil {
  118. return false, err
  119. }
  120. } else if !os.IsNotExist(e) {
  121. return false, e
  122. } else if _, e = os.Lstat(m.debtikDir); e != nil {
  123. return false, fmt.Errorf("original configuration unavailable: %w", e)
  124. }
  125. } else if _, e := os.Lstat(j.NewDir); os.IsNotExist(e) {
  126. if err = os.RemoveAll(m.debtikDir); err != nil {
  127. return false, err
  128. }
  129. if err = syncRestoreDir(parent); err != nil {
  130. return false, err
  131. }
  132. } else if e != nil {
  133. return false, e
  134. }
  135. }
  136. }
  137. if j.NewDir != "" {
  138. if err = os.RemoveAll(j.NewDir); err != nil {
  139. return false, err
  140. }
  141. }
  142. if j.OldDir != "" {
  143. if err = os.RemoveAll(j.OldDir); err != nil {
  144. return false, err
  145. }
  146. }
  147. if j.HasDebtik {
  148. if err = syncRestoreDir(parent); err != nil {
  149. return false, err
  150. }
  151. }
  152. if err = os.Remove(m.journalPath()); err != nil {
  153. return false, err
  154. }
  155. if err = syncRestoreDir(m.backupStore); err != nil {
  156. return false, err
  157. }
  158. return true, nil
  159. }
  160. // applyStagedRestore restores the original on-disk files if an apply step fails.
  161. // The optional step hook enables deterministic failure tests.
  162. func (m *Manager) applyStagedRestore(stage string, step func(string) error) (err error) {
  163. runStep := func(name string) error {
  164. if step != nil {
  165. return step(name)
  166. }
  167. return nil
  168. }
  169. stagedDebtik := filepath.Join(stage, "debtik")
  170. stagedNetwork := filepath.Join(stage, "network")
  171. var files []restoreFile
  172. entries, readErr := os.ReadDir(stagedNetwork)
  173. if readErr != nil && !os.IsNotExist(readErr) {
  174. return fmt.Errorf("failed listing staged network files: %w", readErr)
  175. }
  176. for _, entry := range entries {
  177. if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".cfg") {
  178. continue
  179. }
  180. target := filepath.Join(m.networkDir, entry.Name())
  181. f := restoreFile{path: target, mode: 0644}
  182. f.after, err = os.ReadFile(filepath.Join(stagedNetwork, entry.Name()))
  183. if err != nil {
  184. return fmt.Errorf("failed reading staged network file %s: %w", entry.Name(), err)
  185. }
  186. info, statErr := os.Lstat(target)
  187. if statErr == nil {
  188. if !info.Mode().IsRegular() {
  189. return fmt.Errorf("failed restoring network file %s: destination is not a regular file", entry.Name())
  190. }
  191. f.existed = true
  192. f.mode = info.Mode().Perm()
  193. f.before, err = os.ReadFile(target)
  194. if err != nil {
  195. return fmt.Errorf("failed reading current network file %s: %w", entry.Name(), err)
  196. }
  197. } else if !os.IsNotExist(statErr) {
  198. return statErr
  199. }
  200. files = append(files, f)
  201. }
  202. hasDebtik := false
  203. if info, e := os.Stat(stagedDebtik); e == nil {
  204. if !info.IsDir() {
  205. return errors.New("invalid staged configuration")
  206. }
  207. hasDebtik = true
  208. } else if !os.IsNotExist(e) {
  209. return e
  210. }
  211. var newDir, oldDir string
  212. oldExists := false
  213. if hasDebtik {
  214. parent := filepath.Dir(m.debtikDir)
  215. if err = os.MkdirAll(parent, 0755); err != nil {
  216. return err
  217. }
  218. newDir, err = os.MkdirTemp(parent, ".debtik-restore-new-*")
  219. if err != nil {
  220. return err
  221. }
  222. defer func() {
  223. if _, e := os.Stat(m.journalPath()); os.IsNotExist(e) {
  224. _ = os.RemoveAll(newDir)
  225. }
  226. }()
  227. if err = m.syncDirectory(stagedDebtik, newDir); err != nil {
  228. return fmt.Errorf("failed staging debtik configuration: %w", err)
  229. }
  230. info, e := os.Lstat(m.debtikDir)
  231. if e == nil {
  232. if !info.IsDir() {
  233. return errors.New("debtik configuration path is not a directory")
  234. }
  235. oldExists = true
  236. } else if !os.IsNotExist(e) {
  237. return e
  238. }
  239. if oldExists {
  240. oldDir, err = os.MkdirTemp(parent, ".debtik-restore-old-*")
  241. if err != nil {
  242. return err
  243. }
  244. if err = os.Remove(oldDir); err != nil {
  245. return err
  246. }
  247. }
  248. }
  249. if _, err := os.Stat(m.journalPath()); err == nil {
  250. return errors.New("an interrupted restore must be recovered first")
  251. } else if !os.IsNotExist(err) {
  252. return err
  253. }
  254. journal := &restoreJournal{DebtikDir: m.debtikDir, NetworkDir: m.networkDir, NewDir: newDir, OldDir: oldDir, OldExists: oldExists, HasDebtik: hasDebtik}
  255. for _, f := range files {
  256. journal.Files = append(journal.Files, journalFile{Path: f.path, Data: f.before, Mode: f.mode, Existed: f.existed})
  257. }
  258. if err = m.writeJournal(journal); err != nil {
  259. return fmt.Errorf("failed recording restore transaction: %w", err)
  260. }
  261. rollback := func(cause error) error {
  262. if _, err := m.recoverInterruptedRestoreLocked(); err != nil {
  263. return fmt.Errorf("restore failed and rollback incomplete: %w", errors.Join(cause, err))
  264. }
  265. return fmt.Errorf("restore failed; previous files restored: %w", cause)
  266. }
  267. if hasDebtik {
  268. if oldExists {
  269. if err = os.Rename(m.debtikDir, oldDir); err != nil {
  270. return rollback(err)
  271. }
  272. if err = syncRestoreDir(filepath.Dir(m.debtikDir)); err != nil {
  273. return rollback(err)
  274. }
  275. if err = runStep("old-renamed"); err != nil {
  276. return rollback(err)
  277. }
  278. }
  279. if err = os.Rename(newDir, m.debtikDir); err != nil {
  280. return rollback(err)
  281. }
  282. if err = syncRestoreDir(filepath.Dir(m.debtikDir)); err != nil {
  283. return rollback(err)
  284. }
  285. if err = runStep("debtik"); err != nil {
  286. return rollback(err)
  287. }
  288. }
  289. if len(files) > 0 {
  290. if err = os.MkdirAll(m.networkDir, 0755); err != nil {
  291. return rollback(err)
  292. }
  293. }
  294. for _, f := range files {
  295. if err = restoreWrite(f.path, f.after, 0644); err != nil {
  296. return rollback(fmt.Errorf("failed restoring network file %s: %w", filepath.Base(f.path), err))
  297. }
  298. if err = runStep("network"); err != nil {
  299. return rollback(err)
  300. }
  301. }
  302. journal.Committed = true
  303. if err = m.writeJournal(journal); err != nil {
  304. return fmt.Errorf("could not commit restore journal; recovery required: %w", err)
  305. }
  306. if err = runStep("committed"); err != nil {
  307. return fmt.Errorf("restore committed; cleanup pending: %w", err)
  308. }
  309. if _, err = m.recoverInterruptedRestoreLocked(); err != nil {
  310. return fmt.Errorf("configuration restored but transaction cleanup failed: %w", err)
  311. }
  312. return nil
  313. }
  314. func restoreWrite(path string, data []byte, mode os.FileMode) error {
  315. f, err := os.CreateTemp(filepath.Dir(path), ".debtik-restore-*")
  316. if err != nil {
  317. return err
  318. }
  319. name := f.Name()
  320. defer os.Remove(name)
  321. if err = f.Chmod(mode); err == nil {
  322. _, err = f.Write(data)
  323. }
  324. if err == nil {
  325. err = f.Sync()
  326. }
  327. if closeErr := f.Close(); err == nil {
  328. err = closeErr
  329. }
  330. if err != nil {
  331. return err
  332. }
  333. if err = os.Rename(name, path); err != nil {
  334. return err
  335. }
  336. return syncRestoreDir(filepath.Dir(path))
  337. }