-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackup.go
More file actions
151 lines (133 loc) · 4.15 KB
/
Copy pathbackup.go
File metadata and controls
151 lines (133 loc) · 4.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Package app implements siphon's verbs over the domain layer.
// CLI and TUI both call into this package; neither cares about the other.
package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"io"
"os"
"path/filepath"
"time"
"github.com/oklog/ulid/v2"
"github.com/nixrajput/siphon/internal/driver"
"github.com/nixrajput/siphon/internal/dumps"
"github.com/nixrajput/siphon/internal/errs"
"github.com/nixrajput/siphon/internal/jobs"
"github.com/nixrajput/siphon/internal/profile"
)
// Deps bundles every dependency the app verbs need. CLI and TUI build
// one Deps at startup and pass it to every verb. Makes mocking trivial.
type Deps struct {
Profiles *profile.Store
Dumps *dumps.Catalog
Runner *jobs.Runner
Drivers DriverGetter
}
// DriverGetter is satisfied by internal/driver.Get. Wrapped to allow mocking.
type DriverGetter interface {
Get(name string) (driver.Driver, error)
}
// BackupOpts configures the Backup verb.
type BackupOpts struct {
Profile string
IncludeTables []string
ExcludeTables []string
ExcludeDataFrom []string
SchemaOnly bool
DataOnly bool
CompressionLevel int
Parallel int
}
// Backup dumps the source profile to a new entry in the catalog.
// Returns the running job's Event channel and ID.
func Backup(parent context.Context, d Deps, opt BackupOpts) (<-chan jobs.Event, string, error) {
resolved, err := d.Profiles.Resolve(opt.Profile)
if err != nil {
return nil, "", err
}
drv, err := d.Drivers.Get(resolved.Driver)
if err != nil {
return nil, "", err
}
return d.Runner.Run(parent, jobs.Job{
Stage: "backup",
Func: func(ctx context.Context, emit func(jobs.Event)) error {
conn, err := drv.Connect(ctx, resolved)
if err != nil {
return err
}
defer func() { _ = conn.Close() }()
id := ulid.Make().String()
tmpPath := filepath.Join(d.Dumps.Root(), id+".dump.tmp")
finalPath := d.Dumps.Path(id)
f, err := os.Create(tmpPath)
if err != nil {
return err
}
h := sha256.New()
tee := io.MultiWriter(f, h)
env := &dumps.Envelope{
Type: dumps.EnvelopeBase,
Driver: resolved.Driver,
Created: time.Now().UTC(),
}
if _, err := dumps.WriteEnvelope(tee, env); err != nil {
_ = f.Close()
_ = os.Remove(tmpPath)
return err
}
emit(jobs.Event{Phase: jobs.PhaseProgress, Message: "dumping"})
backupErr := conn.Backup(ctx, driver.BackupOpts{
IncludeTables: opt.IncludeTables,
ExcludeTables: opt.ExcludeTables,
ExcludeDataFrom: opt.ExcludeDataFrom,
SchemaOnly: opt.SchemaOnly,
DataOnly: opt.DataOnly,
CompressionLevel: opt.CompressionLevel,
Parallel: opt.Parallel,
}, tee)
// Close the dump file explicitly and check the error: for a file
// being WRITTEN, Close() is where buffered data is flushed and where
// late I/O failures (ENOSPC, quota, disk error) surface. Ignoring it
// could rename a truncated dump into the catalog with a checksum that
// matches the truncated bytes — corrupt-but-looks-valid. The pg_dump
// error takes precedence if both occurred.
closeErr := f.Close()
if backupErr != nil {
_ = os.Remove(tmpPath)
return backupErr
}
if closeErr != nil {
_ = os.Remove(tmpPath)
return &errs.Error{Op: "app.backup", Code: errs.CodeSystem, Cause: closeErr, Hint: "failed to flush dump to disk (out of space?)"}
}
if err := os.Rename(tmpPath, finalPath); err != nil {
_ = os.Remove(tmpPath)
return err
}
st, _ := os.Stat(finalPath)
size := int64(0)
if st != nil {
size = st.Size()
}
meta := &dumps.Meta{
ID: id,
Profile: opt.Profile,
Driver: resolved.Driver,
SizeBytes: size,
Checksum: "sha256:" + hex.EncodeToString(h.Sum(nil)),
Created: time.Now(),
DumpFormat: "custom",
}
if writeErr := d.Dumps.WriteMeta(meta); writeErr != nil {
// The catalog enumerates by sidecar metadata, so a dump without
// its meta would be an invisible orphan that never gets pruned.
_ = os.Remove(finalPath)
return writeErr
}
emit(jobs.Event{Phase: jobs.PhaseProgress, Message: "wrote " + finalPath})
return nil
},
})
}