chiark / gitweb /
sys-base: Only use the extensions package from CMUCL.
[lisp] / safely.lisp
1 ;;; -*-lisp-*-
2 ;;;
3 ;;; $Id$
4 ;;;
5 ;;; Safely modify collections of files
6 ;;;
7 ;;; (c) 2005 Straylight/Edgeware
8 ;;;
9
10 ;;;----- Licensing notice ---------------------------------------------------
11 ;;;
12 ;;; This program is free software; you can redistribute it and/or modify
13 ;;; it under the terms of the GNU General Public License as published by
14 ;;; the Free Software Foundation; either version 2 of the License, or
15 ;;; (at your option) any later version.
16 ;;; 
17 ;;; This program is distributed in the hope that it will be useful,
18 ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 ;;; GNU General Public License for more details.
21 ;;; 
22 ;;; You should have received a copy of the GNU General Public License
23 ;;; along with this program; if not, write to the Free Software Foundation,
24 ;;; Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25
26 (defpackage #:safely
27   (:use #:common-lisp #:mdw.base)
28   (:export #:safely #:safely-close #:safely-delete-file
29            #:safely-open-output-stream #:safely-bail #:safely-commit
30            #:safely-writing))
31 (in-package #:safely)
32
33 (defstruct (safely (:predicate safelyp))
34   "Stores information about how to commit or undo safe writes."
35   (streams nil)
36   (trail nil))
37
38 (defun safely-close (safe stream)
39   "Make sure that STREAM is closed when SAFE is finished."
40   (push stream (safely-streams safe)))
41
42 (defun safely-delete-file (safe file)
43   "Delete FILE when SAFE is committed."
44   (push `(:delete ,file) (safely-trail safe)))
45
46 (defun generate-fresh-file-name (base tag &optional func)
47   "Return a fresh file name constructed from BASE (a filespec) and TAG (some
48    short descriptive string).  The generated name has the same directory and
49    type as the BASE name, but a different name.
50
51    If FUNC is non-nil, then it is a function to call on the generated file
52    name: generate-fresh-file-name runs in a loop, calling FUNC with generated
53    file names until FUNC returns non-nil, at which point generate-fresh-
54    file-name returns two values: the generated name, and the result of FUNC.
55    generate-fresh-file-name catches errors of type file-error from FUNC, and
56    just tries again with a new name.
57
58    If FUNC is nil, it's treated the same as a function which always returns
59    t.
60
61    This is inspired by a similar facility in scsh."
62   (let ((base (pathname base)))
63     (dotimes (i 256
64               (error "Gave up trying to find a temporary ~A file for ~S."
65                      tag base))
66       (let* ((new (merge-pathnames
67                    (make-pathname
68                     :name (format nil "~A-~A-~X"
69                                   (pathname-name base)
70                                   tag
71                                   (random most-positive-fixnum)))
72                    base))
73              (ret (and (not (probe-file new))
74                        (if func
75                            (handler-case (funcall func new)
76                              (file-error (cond)
77                                (unless (pathname-match-p
78                                         (file-error-pathname cond)
79                                         new)
80                                  (error cond))
81                                nil))
82                            t))))
83         (when ret
84           (return (values new ret)))))))
85
86 (defun safely-open-output-stream (safe file &rest open-args)
87   "Create an output stream which will be named FILE when SAFE is committed.
88    Other OPEN-ARGS are passed to open."
89   (multiple-value-bind
90       (name stream)
91       (generate-fresh-file-name file "new"
92                                 (lambda (name)
93                                   (apply #'open name
94                                          :direction :output
95                                          :if-exists nil
96                                          open-args)))
97     (safely-close safe stream)
98     (push `(:shunt ,name ,file)
99           (safely-trail safe))
100     stream))
101
102 (defun delete-file-without-moaning (file)
103   "Delete the FILE, ignoring errors."
104   (handler-case (delete-file file)
105     (file-error () nil)))
106
107 (defun rename-file-without-moaning (old new)
108   "Rename OLD to NEW, ignoring errors, and without doing any stupid name
109    mangling."
110   (handler-case (rename-file old new)
111     (file-error () nil)))
112
113 (defun safely-unwind (trail)
114   "Roll back the TRAIL of operations."
115   (dolist (job trail)
116     (ecase (car job)
117       (:shunt (destructuring-bind (new file) (cdr job)
118                 (declare (ignore file))
119                 (delete-file-without-moaning new)))
120       (:delete)
121       (:rmtmp (destructuring-bind (file) (cdr job)
122                 (delete-file-without-moaning file)))
123       (:revert (destructuring-bind (old new) (cdr job)
124                  (rename-file-without-moaning old new))))))
125
126 (defun safely-reset (safe)
127   "Reset SAFE to its initial state."
128   (setf (safely-streams safe) nil)
129   (setf (safely-trail safe) nil))
130
131 (defun safely-bail (safe)
132   "Abort the operations in SAFE, unwinding all the things that have been
133    done.  Streams are closed, new files are removed."
134   (dolist (stream (safely-streams safe))
135     (close stream :abort t))
136   (safely-unwind (safely-trail safe))
137   (safely-reset safe))
138
139 (defun safe-copy (file tag)
140   "Make a copy of the FILE.  Return the new name."
141
142   #+cmu
143   ;; Use link(2) where available.
144   (generate-fresh-file-name file tag
145                             (lambda (name)
146                               (let ((from (ext:unix-namestring file t))
147                                     (to (ext:unix-namestring name nil)))
148                                 (and from to
149                                      (unix:unix-link from to)))))
150
151   #-cmu
152   ;; Otherwise just copy the file contents and hope for the best.
153   (with-open-file (input file :element-type :default)
154     (multiple-value-bind
155         (copy output)
156         (generate-fresh-file-name file tag
157                                   (lambda (name)
158                                     (open name
159                                           :direction :output
160                                           :if-exists nil
161                                           :element-type :default)))
162       (unwind-protect
163            (progn
164              (let ((buffer (make-array 8192
165                                        :element-type (stream-element-type
166                                                       input))))
167                (loop
168                  (let ((read (read-sequence buffer input)))
169                    (when (plusp read)
170                      (write-sequence buffer output :end read))
171                    (when (< read (length buffer))
172                      (return copy))))))
173         (close output)))))
174                                               
175 (defun safely-commit (safe)
176   "Commit SAFE.  The files deleted by safely-delete-file are deleted; the
177    files created by safely-open-output-stream are renamed over the old
178    versions, if any.  If a problem occurs during this stage, everything is
179    rewound and no changes are made."
180   (let ((trail (safely-trail safe))
181         (revert nil)
182         (cleanup nil))
183     (unwind-protect
184         (progn
185           (dolist (stream (safely-streams safe))
186             (close stream))
187           (loop
188             (unless trail
189               (return))
190               (let ((job (pop trail)))
191                 (ecase (car job)
192                   (:shunt (destructuring-bind (tag new file) job
193                             (declare (ignore tag))
194                             (push `(:rmtmp ,new) revert)
195                             (if (probe-file file)
196                                 (let ((old (safe-copy file "old")))
197                                   (push `(:rmtmp ,old) cleanup)
198                                   (push `(:revert ,old ,file) revert))
199                                 (push `(:rmtmp ,file) revert))
200                             (rename-file new file)))
201                   (:delete (destructuring-bind (tag file) job
202                              (declare (ignore tag))
203                              (let ((old (safe-copy file "delete")))
204                                (push `(:revert ,old ,file) revert)
205                                (push `(:rmtmp ,old) cleanup)
206                                (delete-file file)))))))
207           (setf revert nil))
208       (safely-unwind trail)
209       (safely-unwind revert)
210       (safely-unwind cleanup)
211       (safely-reset safe))))
212
213 (defmacro safely ((safe &key) &body body)
214   "Do stuff within the BODY safely.  If BODY completes without errors, the
215    SAFE is committed; otherwise it's bailed."
216   `(let ((,safe (make-safely)))
217      (unwind-protect
218          (progn
219            ,@body
220            (safely-commit ,safe)
221            (setf ,safe nil))
222        (when ,safe
223          (safely-bail ,safe)))))
224
225 (defmacro safely-writing ((stream file &rest open-args) &body body)
226   "Simple macro for writing a single file safely.  STREAM is opened onto a
227    temporary file, and if BODY completes, it is renamed to FILE."
228   (with-gensyms safe
229     `(safely (,safe)
230        (let ((,stream (apply #'safely-open-output-stream
231                              ,safe ,file ,open-args)))
232          ,@body))))
233
234 ;;;----- That's all, folks --------------------------------------------------