This repository has been archived by the owner on Jun 18, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
duetbackup.go
241 lines (202 loc) · 5.36 KB
/
duetbackup.go
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package duetbackup
import (
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/wilriker/librfm"
)
const (
SysDir = "0:/sys"
dirMarker = ".duetbackup"
)
var multiSlashRegex = regexp.MustCompile(`/{2,}`)
// CleanPath will reduce multiple consecutive slashes into one and
// then remove a trailing slash if any.
func CleanPath(path string) string {
cleanedPath := multiSlashRegex.ReplaceAllString(path, "/")
cleanedPath = strings.TrimSuffix(cleanedPath, "/")
return cleanedPath
}
type Backup interface {
// SyncFolder will syncrhonize the contents of a remote folder to a local directory.
// The boolean flag removeLocal decides whether or not files that have been remove
// remote should also be deleted locally
SyncFolder(remoteFolder, outDir string, excls Excludes, removeLocal bool) error
}
type backup struct {
rfm librfm.RRFFileManager
verbose bool
}
func New(rfm librfm.RRFFileManager, verbose bool) Backup {
return &backup{
rfm: rfm,
verbose: verbose,
}
}
// ensureOutDirExists will create the local directory if it does not exist
// and will in any case create the marker file inside it
func (b *backup) ensureOutDirExists(outDir string) error {
path, err := filepath.Abs(outDir)
if err != nil {
return err
}
// Check if the directory exists
fi, err := os.Stat(path)
if err != nil && !os.IsNotExist(err) {
return err
}
// Create the directory
if fi == nil {
if b.verbose {
log.Println(" Creating directory", path)
}
if err = os.MkdirAll(path, 0755); err != nil {
return err
}
}
// Create the marker file
markerFile, err := os.Create(filepath.Join(path, dirMarker))
if err != nil {
return err
}
markerFile.Close()
return nil
}
func (b *backup) updateLocalFiles(fl *librfm.Filelist, outDir string, excls Excludes, removeLocal bool) error {
if err := b.ensureOutDirExists(outDir); err != nil {
return err
}
for _, file := range fl.Files {
if file.IsDir() {
continue
}
remoteFilename := fmt.Sprintf("%s/%s", fl.Dir, file.Name)
// Skip files covered by an exclude pattern
if excls.Contains(remoteFilename) {
if b.verbose {
log.Println(" Excluding: ", remoteFilename)
}
continue
}
fileName := filepath.Join(outDir, file.Name)
fi, err := os.Stat(fileName)
if err != nil && !os.IsNotExist(err) {
return err
}
// File does not exist or is outdated so get it
if fi == nil || fi.ModTime().Before(file.Date()) {
// Download file
body, duration, err := b.rfm.Download(remoteFilename)
if err != nil {
return err
}
if b.verbose {
kibs := (float64(file.Size) / duration.Seconds()) / 1024
if fi != nil {
log.Printf(" Updated: %s (%.1f KiB/s)", remoteFilename, kibs)
} else {
log.Printf(" Added: %s (%.1f KiB/s)", remoteFilename, kibs)
}
}
// Open or create corresponding local file
nf, err := os.Create(fileName)
if err != nil {
return err
}
defer nf.Close()
// Write contents to local file
_, err = nf.Write(body)
if err != nil {
return err
}
// Adjust mtime
os.Chtimes(fileName, file.Date(), file.Date())
} else {
if b.verbose {
log.Println(" Up-to-date:", remoteFilename)
}
}
}
return nil
}
// isManagedDirectory checks wether the given path is a directory and
// if so if it contains the marker file. It will return false in case
// any error has occured.
func (b *backup) isManagedDirectory(basePath string, f os.FileInfo) bool {
if !f.IsDir() {
return false
}
markerFile := filepath.Join(basePath, f.Name(), dirMarker)
fi, err := os.Stat(markerFile)
if err != nil && !os.IsNotExist(err) {
return false
}
if fi == nil {
return false
}
return true
}
func (b *backup) removeDeletedFiles(fl *librfm.Filelist, outDir string) error {
// Pseudo hash-set of known remote filenames
existingFiles := make(map[string]struct{})
for _, f := range fl.Files {
existingFiles[f.Name] = struct{}{}
}
files, err := ioutil.ReadDir(outDir)
if err != nil {
return err
}
for _, f := range files {
if _, exists := existingFiles[f.Name()]; !exists {
// Skip directories not managed by us as well as our marker file
if !b.isManagedDirectory(outDir, f) || f.Name() == dirMarker {
continue
}
if err := os.RemoveAll(filepath.Join(outDir, f.Name())); err != nil {
return err
}
if b.verbose {
log.Println(" Removed: ", f.Name())
}
}
}
return nil
}
func (b *backup) SyncFolder(folder, outDir string, excls Excludes, removeLocal bool) error {
// Skip complete directories if they are covered by an exclude pattern
if excls.Contains(folder) {
log.Println("Excluding", folder)
return nil
}
log.Println("Fetching filelist for", folder)
fl, err := b.rfm.Filelist(folder)
if err != nil {
return err
}
log.Println("Downloading new/changed files from", folder, "to", outDir)
if err = b.updateLocalFiles(fl, outDir, excls, removeLocal); err != nil {
return err
}
if removeLocal {
log.Println("Removing no longer existing files in", outDir)
if err = b.removeDeletedFiles(fl, outDir); err != nil {
return err
}
}
// Traverse into subdirectories
for _, file := range fl.Files {
if !file.IsDir() {
continue
}
remoteFilename := fmt.Sprintf("%s/%s", fl.Dir, file.Name)
fileName := filepath.Join(outDir, file.Name)
if err = b.SyncFolder(remoteFilename, fileName, excls, removeLocal); err != nil {
return err
}
}
return nil
}