Skip to content

Commit 7376831

Browse files
committed
Support saving backup and autosave files in configurable directories
1 parent cefb768 commit 7376831

File tree

5 files changed

+527
-36
lines changed

5 files changed

+527
-36
lines changed

gui-lib/framework/main.rkt

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -535,17 +535,105 @@
535535
@{Opens a dialog that queries the user about exiting. Returns the user's
536536
decision.})
537537

538-
(proc-doc/names
539-
path-utils:generate-autosave-name
540-
(-> (or/c #f path-string? path-for-some-system?) path?)
541-
(filename)
542-
@{Generates a name for an autosave file from @racket[filename].})
543-
544538
(proc-doc/names
545539
path-utils:generate-backup-name
546540
(path? . -> . path?)
547541
(filename)
548-
@{Generates a name for an backup file from @racket[filename].})
542+
@{
543+
Generates a path for a backup file based on @racket[filename].
544+
545+
@index{'path-utils:backup-dir}
546+
The value of @racket[(preferences:get 'path-utils:backup-dir)]
547+
determines the directory of the resulting path.
548+
The value of this preference must be either a path
549+
satisfying @racket[complete-path?],
550+
in which case it is additionally checked to ensure that the
551+
directory exists and is writable,
552+
or @racket[#f] (the default).
553+
When the preference contains a valid path,
554+
the resulting path will use that directory;
555+
otherwise, and by default,
556+
the result will use the same directory as @racket[filename].
557+
558+
The final element of the resulting path is derived from @racket[filename].
559+
When @racket[(preferences:get 'path-utils:backup-dir)] does not specify
560+
a valid directory, the final element of @racket[filename] is
561+
used directly as the base for the new element.
562+
Otherwise, the base is formed by transforming the complete path to @racket[filename]
563+
(resolved, if necessary, relative to @racket[(current-directory)])
564+
according to the following @deftech{encoding scheme}:
565+
@itemlist[#:style 'ordered
566+
@item{A @racket[separator-byte] is selected: @litchar{!} by default,
567+
but a list of visually-appealing one-byte characters are
568+
tried if @litchar{!} occurs in the complete path to
569+
@racket[filename].}
570+
@item{Every seperator between path elements is replaced with
571+
@racket[separator-byte], as are any other occurances of the
572+
reserved separator character (@litchar{\} on Windows or
573+
@litchar{/} on Unix or Mac OS), e.g. in the name of the root directory.}
574+
@item{A single @racket[separator-byte] is added at the beginning
575+
so that the seperator can be unambiguously identified.}
576+
@item{If the result of the previous step is excessively long, it
577+
may be shortened by replacing some bytes in the middle with
578+
@racket[(bytes-append separator-byte #"..." separator-byte)]
579+
(i.e. @litchar{!...!} with @litchar{!} as the @racket[separator-byte]).}]
580+
581+
@margin-note{
582+
Currently, "excessively long" is defined as 255 bytes.
583+
This is based on @hyperlink["https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath"]{
584+
a common value} for the maximum length of an individual element of an extended-length path
585+
on Windows, but the limit is currently enforced consistently on all platforms.
586+
}
587+
588+
In either case, the final path element is formed from the base
589+
in a platform-specific manner:
590+
@itemlist[
591+
@item{On Unix and Mac OS, a @litchar{~} is added to the end.}
592+
@item{On Windows, the extension (in the sense of @racket[path-replace-extension])
593+
is replaced with @litchar{.bak}.}]
594+
})
595+
596+
(proc-doc/names
597+
path-utils:generate-autosave-name
598+
(-> (or/c #f path-string? path-for-some-system?) path?)
599+
(filename)
600+
@{
601+
Generates a path for an autosave file based on @racket[filename].
602+
603+
@index{'path-utils:autosave-dir}
604+
The value of @racket[(preferences:get 'path-utils:autosave-dir)]
605+
determines the directory of the resulting path
606+
in much the same way as @racket[path-utils:generate-backup-name]
607+
with @racket['path-utils:backup-dir].
608+
When the preference value specifies a directory that exists and
609+
is writable, the resulting path will use that directory.
610+
Otherwise, and by default, the
611+
result will use the same directory as @racket[filename]
612+
(or, when @racket[filename] is @racket[#f], the directory determined by
613+
@racket[(find-system-path 'doc-dir)]).
614+
615+
When @racket[filename] is @racket[#f], the final element of the
616+
resulting path will be an automatically-generated unique name.
617+
618+
Otherwise, the final path element will be derived from @racket[filename].
619+
When @racket[(preferences:get 'path-utils:autosave-dir)] does not return
620+
a valid directory, the last element of @racket[filename] will be
621+
used directly as the base for the new element.
622+
When a valid autosave directory is specified, the base will be
623+
the complete path to @racket[filename],
624+
transformed according to the same @tech{encoding scheme} as
625+
with @racket[path-utils:generate-backup-name].
626+
In either case, the final path element is formed from the base
627+
in a platform-specific manner:
628+
@itemlist[
629+
@item{On Unix and Mac OS, a @litchar{#} is added to the start
630+
and end, then a number is added after the
631+
ending @litchar{#}, and then one more @litchar{#} is appended
632+
after the number.
633+
The number is selected to make the autosave filename unique.}
634+
@item{On Windows, the file’s extension (in the sense of @racket[path-replace-extension])
635+
is replaced with a number to make the autosave filename unique.}
636+
]})
549637

550638
(parameter-doc
551639
finder:dialog-parent-parameter

gui-lib/framework/private/main.rkt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,32 @@
599599
(update-style-list
600600
(color-prefs:lookup-in-color-scheme 'framework:misspelled-text-color)))
601601

602+
;; for path-utils
603+
604+
(define (valid-maybe-path-value? v)
605+
(or (not v)
606+
(and (path? v)
607+
(complete-path? v)
608+
(directory-exists? v)
609+
(memq 'write (file-or-directory-permissions v)))))
610+
611+
(define (marshall:maybe-path->bytes v)
612+
(and (path? v) (path->bytes v)))
613+
614+
(define (unmarshall:maybe-bytes->path v)
615+
(with-handlers ([exn:fail? (λ (e) #f)])
616+
(and v (bytes->path v))))
617+
618+
(define (initialize-backup/autosave-preference sym)
619+
(preferences:set-default sym #f valid-maybe-path-value?)
620+
(preferences:set-un/marshall sym
621+
marshall:maybe-path->bytes
622+
unmarshall:maybe-bytes->path))
623+
624+
(initialize-backup/autosave-preference 'path-utils:backup-dir)
625+
626+
(initialize-backup/autosave-preference 'path-utils:autosave-dir)
627+
602628
;; groups
603629

604630
(preferences:set-default 'framework:exit-when-no-frames #t boolean?)
Lines changed: 154 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,58 @@
11
#lang scheme/unit
2-
(require "sig.rkt")
2+
(require "sig.rkt"
3+
racket/list
4+
"../preferences.rkt")
35

46
(import)
57
(export framework:path-utils^)
6-
7-
(define (generate-autosave-name name)
8-
(let-values ([(base name dir?)
9-
(if name
10-
(split-path name)
11-
(values (find-system-path 'doc-dir)
12-
(bytes->path-element #"mredauto")
13-
#f))])
14-
(let* ([base (if (path? base)
15-
base
16-
(current-directory))]
17-
[path (if (relative-path? base)
18-
(build-path (current-directory) base)
19-
base)])
8+
9+
;; preferences initialized in main.rkt
10+
11+
(define (make-getter/ensure-exists pref-sym)
12+
(λ ()
13+
(let ([maybe-dir (preferences:get pref-sym)])
14+
(and maybe-dir
15+
(directory-exists? maybe-dir)
16+
(memq 'write (file-or-directory-permissions maybe-dir))
17+
maybe-dir))))
18+
19+
(define current-backup-dir
20+
(make-getter/ensure-exists 'path-utils:backup-dir))
21+
22+
(define current-autosave-dir
23+
(make-getter/ensure-exists 'path-utils:autosave-dir))
24+
25+
; generate-autosave-name : (or/c #f path-string? path-for-some-system?) -> path?
26+
(define (generate-autosave-name maybe-old-path)
27+
(cond
28+
[maybe-old-path
29+
(let*-values ([(base name dir?) (split-path maybe-old-path)]
30+
[(base) (cond
31+
[(not (path? base))
32+
(current-directory)]
33+
[(relative-path? base)
34+
(build-path (current-directory) base)]
35+
[else
36+
base])])
37+
(cond
38+
[(current-autosave-dir)
39+
=>
40+
(λ (dir)
41+
(make-unique-autosave-name dir (encode-as-path-element base name)))]
42+
[else
43+
(make-unique-autosave-name base name)]))]
44+
[else
45+
(make-unique-autosave-name (or (current-autosave-dir)
46+
(find-system-path 'doc-dir))
47+
(bytes->path-element #"mredauto"))]))
48+
49+
50+
; make-unique-autosave-name : dir-path path-element -> path?
51+
(define (make-unique-autosave-name dir name)
2052
(let loop ([n 1])
2153
(let* ([numb (string->bytes/utf-8 (number->string n))]
2254
[new-name
23-
(build-path path
55+
(build-path dir
2456
(if (eq? (system-type) 'windows)
2557
(bytes->path-element
2658
(bytes-append (regexp-replace #rx#"\\..*$"
@@ -36,22 +68,115 @@
3668
#"#"))))])
3769
(if (file-exists? new-name)
3870
(loop (add1 n))
39-
new-name))))))
40-
71+
new-name))))
72+
73+
;; generate-backup-name : path? -> path?
4174
(define (generate-backup-name full-name)
4275
(let-values ([(pre-base name dir?) (split-path full-name)])
4376
(let ([base (if (path? pre-base)
4477
pre-base
4578
(current-directory))])
46-
(let ([name-bytes (path-element->bytes name)])
47-
(cond
48-
[(and (eq? (system-type) 'windows)
49-
(regexp-match #rx#"(.*)\\.[^.]*" name-bytes))
50-
=>
51-
(λ (m)
52-
(build-path base (bytes->path-element (bytes-append (cadr m) #".bak"))))]
53-
[(eq? (system-type) 'windows)
54-
(build-path base (bytes->path-element (bytes-append name-bytes #".bak")))]
55-
[else
56-
(build-path base (bytes->path-element (bytes-append name-bytes #"~")))])))))
79+
(define name-element
80+
(let ([name-bytes (path-element->bytes name)])
81+
(bytes->path-element
82+
(cond
83+
[(and (eq? (system-type) 'windows)
84+
(regexp-match #rx#"(.*)\\.[^.]*" name-bytes))
85+
=>
86+
(λ (m)
87+
(bytes-append (cadr m) #".bak"))]
88+
[(eq? (system-type) 'windows)
89+
(bytes-append name-bytes #".bak")]
90+
[else
91+
(bytes-append name-bytes #"~")]))))
92+
(cond
93+
[(current-backup-dir)
94+
=>
95+
(λ (dir)
96+
(build-path dir (encode-as-path-element base name-element)))]
97+
[else
98+
(build-path base name-element)]))))
99+
100+
101+
102+
(define candidate-separators
103+
`(#"!" #"%" #"_" #"|" #":" #">" #"^" #"$" #"@" #"*" #"?"))
104+
105+
(define separator-regexps
106+
(map (compose1 byte-regexp regexp-quote) candidate-separators))
107+
108+
; encode-as-path-element : dir-path path-element -> path-element
109+
; N.B. generate-backup-name may supply a relative directory, but
110+
; we should always use a complete one.
111+
; That is handled by simplify+explode-path->bytes.
112+
; Windows has limitations on path lengths. Racket handles MAX_PATH
113+
; by using "\\?\" paths when necessary, but individual elements must
114+
; be shorter than lpMaximumComponentLength.
115+
; We respect this limit (on all platforms, for consistency)
116+
; by replacing some bytes from the middle if necessary.
117+
(define (encode-as-path-element base-maybe-relative name)
118+
(define illegal-rx
119+
(case (system-path-convention-type)
120+
[(windows) #rx#"\\\\"]
121+
[else #rx#"/"]))
122+
(define l-bytes
123+
(simplify+explode-path->bytes (build-path base-maybe-relative name)))
124+
(define separator-byte
125+
(or (let ([all-components (apply bytes-append l-bytes)])
126+
(for/first ([sep (in-list candidate-separators)]
127+
[rx (in-list separator-regexps)]
128+
#:unless (regexp-match? rx all-components))
129+
sep))
130+
#"!"))
131+
(define legible-name-bytes
132+
(apply
133+
bytes-append
134+
separator-byte
135+
(add-between
136+
(for/list ([elem (in-list l-bytes)])
137+
(regexp-replace* illegal-rx elem separator-byte))
138+
separator-byte)))
139+
(define num-legible-bytes
140+
(bytes-length legible-name-bytes))
141+
(bytes->path-element
142+
(cond
143+
[(< num-legible-bytes
144+
(lpMaximumComponentLength))
145+
legible-name-bytes]
146+
[else
147+
(define replacement
148+
(bytes-append separator-byte #"..." separator-byte))
149+
(define num-excess-bytes
150+
(+ (- num-legible-bytes
151+
(lpMaximumComponentLength))
152+
5 ; extra margin of safety
153+
(bytes-length replacement)))
154+
(define num-bytes-to-keep-per-side
155+
(floor (/ (- num-legible-bytes num-excess-bytes)
156+
2)))
157+
(bytes-append
158+
(subbytes legible-name-bytes 0 num-bytes-to-keep-per-side)
159+
replacement
160+
(subbytes legible-name-bytes (- num-legible-bytes
161+
num-bytes-to-keep-per-side)))])))
162+
163+
164+
;; simplify+explode-path->bytes : path? -> (listof bytes?)
165+
;; Useful because path-element->bytes doesn't work on root paths.
166+
;; Using simplify-path ensures no 'up or 'same.
167+
(define (simplify+explode-path->bytes pth)
168+
(define elems
169+
(explode-path (simplify-path pth)))
170+
(cons (path->bytes (car elems))
171+
(map path-element->bytes (cdr elems))))
172+
173+
;; lpMaximumComponentLength : -> real?
174+
;; Returns the maximum length of an element of a "\\?\" path on Windows.
175+
;; For now, assuming 255, but really this should be
176+
;; "the value returned in the lpMaximumComponentLength parameter
177+
;; of the GetVolumeInformation function".
178+
;; See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
179+
(define (lpMaximumComponentLength)
180+
255)
181+
57182

gui-test/framework/tests/README

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ signal failures when there aren't any.
4141
| This tests that preferences are saved and restored correctly, both
4242
| immediately and across reboots of gracket.
4343

44+
- path-utils: path-utils.rkt -- runs directly via raco test
45+
46+
| This tests that paths for autosave and backup files are
47+
| generated correctly and respond correctly to preferences.
4448

4549
- individual object tests:
4650

0 commit comments

Comments
 (0)