-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfback.rkt
436 lines (368 loc) · 11.8 KB
/
fback.rkt
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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
#! /usr/local/bin/racket
#lang racket/base
(define (display-help)
(displayln #<<EOF
Command-line syntax:
fback
- Show this help screen
fback <directory>
- Backup <directory> using default configuration
fback revisions <files>
- List dates, checksums and file sizes of all revisions of each of <files>
fback restore <datestring> <file>
- Restore <file> to the revision at <datestring>
EOF
))
;; Procedures:
;; If file changed from previous file version or snapshot:
;; Copy new file version
;; If time since last snapshot > threshold
;; Make new snapshot
;; Restore subfolder at destination to date:
;; For each file in subfolder:
;; Find newest version older than or equal to date
;; Copy that version to destination
;; If free space < threshold OR snapshot failed OR file version copy failed
;; # Merge snapshots
;; While number of snapshots > snapthreshold
;; OverwriteCopy second oldest snapshot to oldest snapshot
;; # Remove file versions
;; For each file:
;; occupation=size*(revisions - revthreshold)
;; While files with positive occupation exist AND (free space < threshold OR snapshot failed OR file version copy failed)
;; Remove the oldest revision of the file with highest positive occupation
;; While free space < threshold OR snapshot failed OR file version copy failed
;; Remove a revision that is older than the newest snapshot from the file with highest occupation
;;
;;
;;
;; Pseudo-Predicates:
;; * file changed from previous file version or snapshot
;; Compare size
;; If equal size
;; Compare checksum
;; * time since last snapshot
;; Parse folder name of last folder in snapshot top folder
;; * Find newest version older than or equal to date
;; Traverse file revisions
;; If revision older than or equal to date
;; Save path of file
;; Traverse file revisions in snapshots
;; If revision older than or equal to date
;; Save path of file
;; Return the path that is newer of the two
;; * Copy that version to destination
;; FileCopy with force flag
;; * free space
;; DriveSpaceFree or procedure during testing
;; * snapshot failed
;; ErrorLevel during FileCopy
;; * file version copy failed
;; ErrorLevel during FileCopy
;; * number of snapshots
;; Traverse snapshot top folder
;; Increment a counter
;; * OverwriteCopy second oldest snapshot to oldest snapshot
;; Traverse snapshot top folder to find the snapshots
;; * files with positive occupation exist
;; Traverse file list
;; Traverse file revisions
;; Sum file size
;; Write sum to file list
;; * Remove the oldest revision of the file with highest positive occupation
;; * Remove a revision that is older than the newest snapshot from the file with highest occupation
;;
;;
;;
;; File structures:
;; U:\Snapshots\20150830111559\D\<path on source drive>
;; U:\FileRevisions\20150830111559\D\<path on source drive>
;; Data structures:
;; Map : file path -> occupation
(define (string->boolean str)
(case str
[("#t") #t]
[("#f") #f]
[else (raise-argument-error 'sting->boolean "boolean?" str)]))
(require "options.rkt")
(read-options "default.conf")
(define-options BACKUP-BASEPATH)
(define-options minimum-number-of-revisions)
(define-options DB-FILE)
(define-options do-use-file-checksum)
(define MINIMUM-NUMBER-OF-REVISIONS (string->number minimum-number-of-revisions))
(define DO-USE-FILE-CHECKSUM (string->boolean do-use-file-checksum))
(when (not (directory-exists? BACKUP-BASEPATH))
(make-directory* BACKUP-BASEPATH))
(define (tee val)
(printf "~a\n" val)
val)
;; DB:
;; #hash "filename" -> ( #hash "000datestring" -> (date checksum size pathstring))
(define (print-db)
(if (equal? DB-sym 'not-init)
(displayln DB-sym)
(hash-for-each
DB-sym
(lambda (key val)
(printf "~a\n" key)
(hash-for-each
val
(lambda (rev-key rev-val)
(printf " ~a\n"
rev-key)))))))
(define (export-db db)
;;(print-db)
(unless (equal? DB-sym 'not-init)
(define outfile (open-output-file DB-FILE #:exists 'replace))
(write db outfile)
(close-output-port outfile)))
(define (empty-db)
(make-hash))
(define (make-mutable db)
(define mutable-db (make-hash))
(hash-for-each
db
(lambda (key value)
(hash-set! mutable-db
key
(hash-copy value))))
mutable-db)
(define (import-db)
(with-handlers ([exn:fail:filesystem? ;; Database file does not exist; return empty
(lambda (exception) (empty-db))])
(define infile (open-input-file DB-FILE))
(define db (read infile))
(close-input-port infile)
(make-mutable db)))
;; Memoize DB to minimize file I/O
(define DB-sym 'not-init)
(define (DB)
(when (equal? DB-sym 'not-init)
(set! DB-sym (import-db)))
(immutable? DB-sym)
DB-sym)
(define (DB-list-revisions filepath)
;;(displayln (hash-ref (DB) filepath #f))
;;(if (hash-has-key? (DB) (path->string filepath))
;; (printf ".")
;; (printf "-"))
(hash-ref (DB) (path->string filepath) #f))
(define (DB-remove-revision filepath datestring-to-remove)
;;(displayln (DB))
(let ((revision-tbl
(DB-list-revisions filepath)))
(unless (equal? revision-tbl #f)
(hash-remove! revision-tbl datestring-to-remove))))
(define (DB-add-revision filepath rev-tuple)
;(displayln (hash? (DB)))
;(displayln (immutable? (DB)))
;(displayln filepath)
;(displayln rev-tuple)
(when (not (hash-has-key? (DB) (path->string filepath)))
(hash-set! (DB) (path->string filepath) (make-hash)))
;;(displayln (immutable? (hash-ref (DB) (path->string filepath))))
(hash-set! (hash-ref (DB) (path->string filepath))
(revision-tuple-datestring rev-tuple)
rev-tuple))
(require racket/list)
(define (get-drive-letter filepath)
(substring (path->string (first (explode-path filepath))) 0 1))
(require racket/string)
(define (filepath-to-rev-regex fp)
(pregexp
(string-append
"(?i:^"
(string-replace
(path->string
(reroot-path fp
(string-append
BACKUP-BASEPATH
"(Snapshots|FileRevisions)\\"
"[0-9]+")))
"\\" "\\\\")
"$)")))
(require racket/path)
(define (original-filepath rev-path)
(let ((orig-fp-list (rest (rest (explode-path (find-relative-path BACKUP-BASEPATH rev-path))))))
(set! orig-fp-list
(cons (string->path
(string-append
(path->string (first orig-fp-list))
":")) ;; Add a : after the drive letter
(rest orig-fp-list)))
(apply build-path orig-fp-list)))
(define (revision-datestring rev-path)
(path->string (second (explode-path (find-relative-path BACKUP-BASEPATH rev-path)))))
(define (file-checksum filepath)
(sha1 (open-input-file filepath)))
(define (revision-sequence fp)
(let ((rev-tbl (DB-list-revisions fp)))
(if (equal? rev-tbl #f)
;; Need to fill in the gaps in the DB by reading from file system
(sequence-map
(lambda (rev-path)
(let ((new-rev-tuple (make-revision-tuple rev-path)))
(DB-add-revision fp new-rev-tuple)
new-rev-tuple))
(sequence-filter
(lambda (rev-path)
(regexp-match
(filepath-to-rev-regex fp)
(path->string rev-path)))
(in-directory BACKUP-BASEPATH)))
;; Just return what is in DB
(in-hash-values rev-tbl))))
(define (newest-revision-tuple fp)
(let* ((sorted-revs
(sort (sequence->list (revision-sequence fp))
#:key car
string>?)))
(if (null? sorted-revs)
null
(first sorted-revs))))
(define (revision-tuple-datestring rev-tuple)
(if (null? rev-tuple)
null
(first rev-tuple)))
(define (revision-tuple-checksum rev-tuple)
(if (null? rev-tuple)
null
(second rev-tuple)))
(define (revision-tuple-filesize rev-tuple)
(if (null? rev-tuple)
null
(third rev-tuple)))
(define (revision-tuple-path rev-tuple)
(if (null? rev-tuple)
null
(string->path (fourth rev-tuple))))
(require file/sha1)
(define (file-changed? fp)
(let ((newest (revision-tuple-path (newest-revision-tuple fp))))
(cond [(null? fp)
#f]
[(null? newest)
#t]
[(not (= (file-size newest)
(file-size fp)))
#t]
[(and DO-USE-FILE-CHECKSUM
(not (string=? (sha1 (open-input-file fp))
(sha1 (open-input-file fp)))))
#t]
[#t #f])))
(require srfi/19)
(define (new-timestamp-string)
(date->string (current-date) "~Y~m~d~k~M~S"))
(define (revision-filepath fp timestamp)
(reroot-path fp
(string-append
BACKUP-BASEPATH
"FileRevisions\\"
timestamp)))
(define (make-revision-tuple rev-path)
(list (revision-datestring rev-path)
(file-checksum rev-path)
(file-size rev-path)
(path->string rev-path)))
;; Get amount of free space (overrides built-in for testing purposes)
(require math/base)
(require racket/sequence)
(define (get-free-space)
(- 40000
(sequence-fold
+ 0
(sequence-map
file-size
(in-directory (current-directory))))))
(define (list-revisions filepaths)
(for-each
(lambda (file)
(displayln file)
(for-each
(lambda (revision-tuple)
(printf "~a\t ~a\t ~a\n"
(revision-tuple-datestring revision-tuple)
(revision-tuple-checksum revision-tuple)
(revision-tuple-filesize revision-tuple)))
(sort
(sequence->list
(revision-sequence file))
#:key car
string<?)))
filepaths))
(define (get-files-with-many-revisions)
(sequence-filter
(lambda (rev-path)
(>
(sequence-length
(revision-sequence
(original-filepath
(path->string rev-path))))
MINIMUM-NUMBER-OF-REVISIONS))
(sequence-filter
file-exists? ;; Is not directory
(in-directory BACKUP-BASEPATH))))
(define (do-cleanup size)
(when (> size 0)
(let* ((files-to-remove (get-files-with-many-revisions))
(removed-size 0))
(for ([file-to-remove (get-files-with-many-revisions)])
#:break (> removed-size size)
(DB-remove-revision (original-filepath file-to-remove)
(revision-datestring file-to-remove))
;;(delete-file file-to-remove)
(set! removed-size (+ removed-size (file-size file-to-remove)))
(printf "Removing ~a, ~a bytes removed total \n" file-to-remove removed-size)))))
;;(do-cleanup 20000)
(define (drive-full-error? e)
(and (exn:fail:filesystem:errno? e)
(equal? (exn:fail:filesystem:errno-errno e)
'(112 . windows))))
(define (safe-copy-file . args)
(with-handlers ([drive-full-error? (lambda (e)
(do-cleanup) ;; Clear up some space on backup drive
(apply safe-copy-file args))]) ;; Try to back up file again
(apply copy-file args)))
(define (restore-revision datestring filepath)
(let ((copy-src (revision-filepath filepath datestring))
(copy-dest filepath))
(printf "Restoring from ~a\n" copy-src)
(copy-file copy-src copy-dest #t)))
(require racket/file)
(define (backup-directory directorypath)
(let ((ts (new-timestamp-string)))
(for ([f (in-directory (simplify-path directorypath))])
(let ((rev-path (revision-filepath f ts)))
(if (file-exists? f) ; If not directory
(if (file-changed? f)
(begin
(printf " changed: ~a\n" rev-path)
(make-directory* (path-only rev-path))
(safe-copy-file f rev-path)
(DB-add-revision f (make-revision-tuple rev-path))
)
;;(printf "unchanged: ~a\n" f)
(void))
(void))))))
(require racket/cmdline)
(require profile)
;(profile
(let ((args
(vector->list (current-command-line-arguments))))
(if (null? args)
(display-help)
(case (first args)
[("revisions")
(list-revisions (map
(lambda (path)
(path->complete-path (string->path path)))
(rest args)))]
[("restore")
(restore-revision (second args)
(path->complete-path (string->path (third args))))]
[else
(backup-directory (path->complete-path (string->path (first args))))]))
(export-db (DB)))
;)