git.fiddlerwoaroof.com
Browse code

Refactoring to a more testable storage engine

fiddlerwoaroof authored on 07/08/2017 05:01:15
Showing 6 changed files
... ...
@@ -3,19 +3,22 @@
3 3
   :author "Fiddlerwoaroof <fiddlerwoaroof@howit.is>"
4 4
   :license "MIT"
5 5
   :depends-on (#:alexandria
6
-	       #:alimenta
7
-	       #:fwoar.lisputils
8
-	       #:ironclad
9
-	       #:local-time
10
-	       #:serapeum
11
-	       #:ubiquitous
12
-	       #:uiop
13
-	       #:yason)
6
+               #:alimenta
7
+               #:fwoar.lisputils
8
+               #:ironclad
9
+               #:local-time
10
+               #:serapeum
11
+               #:stream-provider
12
+               #:trivia
13
+               #:ubiquitous
14
+               #:uiop
15
+               #:vector-update-stream
16
+               #:yason)
14 17
   :serial t
15 18
   :components ((:file "package")
16
-	       (:file "tools")
17
-	       (:file "yason-encoders")
18
-	       (:file "encoders")
19
-	       (:file "feed-index-utils")
20
-	       (:file "feed-archive")))
19
+               (:file "tools")
20
+               (:file "yason-encoders")
21
+               (:file "encoders")
22
+               (:file "feed-index-utils")
23
+               (:file "feed-archive")))
21 24
 
... ...
@@ -285,9 +285,9 @@ pre {
285 285
     padding: 1em;
286 286
 }
287 287
 
288
-.code .code, code code, pre pre,
289 288
 .code pre, code .code, pre code,
290
-.code code, code pre, pre .code,
289
+.code .code, code code, pre pre,
290
+.code code, code pre, pre .code
291 291
 {
292 292
     background: transparent;
293 293
     padding: 0;
... ...
@@ -14,98 +14,163 @@
14 14
 
15 15
 (defun wrap-condition (condition feed)
16 16
   (error 'feed-error
17
-	 :feed feed
18
-	 :condition condition))
17
+         :feed feed
18
+         :condition condition))
19 19
 
20 20
 (defmacro unwrap-feed-errors (() &body body)
21 21
   `(handler-bind ((feed-error (op (error (the-condition _)))))
22 22
      ,@body))
23 23
 
24
+(defun %encode-item (root-dir item)
25
+  (destructuring-bind (title path) item
26
+    (format t "~&Encoding ~a~%" title)
27
+    (restart-once (fix-pathname () (fix-path path))
28
+      (let ((pathname (uiop:enough-pathname path root-dir)))
29
+        (yason:with-object ()
30
+          (yason:encode-object-element "title" title)
31
+          (yason:encode-object-element "path" pathname))))))
32
+
24 33
 (defun %encode-feed-as-json (feed item-storage-info root-dir &optional stream)
25 34
   (with-accessors ((description alimenta:description)
26
-		   (feed-link alimenta:feed-link)
27
-		   (items alimenta:items)
28
-		   (link alimenta:link)
29
-		   (source-type alimenta:source-type)
30
-		   (title alimenta:title)) feed)
35
+                   (feed-link alimenta:feed-link)
36
+                   (items alimenta:items)
37
+                   (link alimenta:link)
38
+                   (source-type alimenta:source-type)
39
+                   (title alimenta:title)) feed)
31 40
   (yason:with-output (stream :indent t)
32 41
     (yason:with-object ()
33 42
       (yason:encode-object-element "metadata" feed)
34 43
       (yason:with-object-element ("items")
35
-	(yason:with-array ()
36
-	  (dolist (item item-storage-info)
37
-            
38
-	    (with-simple-restart (skip-item "Skip item ~s" (car item))
44
+        (yason:with-array ()
45
+          (dolist (item item-storage-info)
46
+            (with-simple-restart (skip-item "Skip item ~s" (car item))
39 47
               ;; (format t "~&I Store Info: ~a~%~4t~a~%" (uiop:unix-namestring (cadr item)) root-dir)
40
-	      (%encode-item root-dir item)
41
-	      #+null
48
+              (%encode-item root-dir item)
49
+              #+null
42 50
               (yason:encode-array-element (uiop:unix-namestring (uiop:enough-pathname root-dir (cadr item))))
43
-	      )))))))
51
+              )))))))
44 52
 
45
-(defmethod store ((items sequence) (directory pathname))
46
-  (map 'list (op (store _ directory))
53
+(defmethod store ((items sequence) storage)
54
+  (when (next-method-p)
55
+    (format t "calling next...~%")
56
+    (call-next-method))
57
+  (map 'list (op (store _ storage))
47 58
        (stable-sort (sort items #'string-lessp
48
-			  :key #'alimenta:title)
49
-		    #'local-time:timestamp>
50
-		    :key #'alimenta:date)))
59
+                          :key #'alimenta:title)
60
+                    #'local-time:timestamp>
61
+                    :key #'alimenta:date)))
51 62
 
52 63
 (defmethod store ((feed alimenta:feed) (directory pathname))
53 64
   (flet ((get-feed-store-name (feed directory)
54
-	   (merge-pathnames (get-id feed)
55
-			    directory)))
65
+           (merge-pathnames (get-id feed)
66
+                            directory)))
56 67
 
57 68
     (with-accessors ((description alimenta:description)
58
-		     (feed-link alimenta:feed-link)
59
-		     (items alimenta:items)
60
-		     (link alimenta:link)
61
-		     (source-type alimenta:source-type)
62
-		     (title alimenta:title)) feed
63
-      ; We wrap all errors with our own condition
69
+                     (feed-link alimenta:feed-link)
70
+                     (items alimenta:items)
71
+                     (link alimenta:link)
72
+                     (source-type alimenta:source-type)
73
+                     (title alimenta:title)) feed
74
+                                        ; We wrap all errors with our own condition
64 75
       (handler-bind ((error (lambda (c) (error 'feed-error :feed feed :condition c))))
65
-	(values (prog1-let ((feed-title title)
66
-			    (feed-store (get-feed-store-name feed directory)))
67
-		  (ensure-directories-exist feed-store)
68
-		  (with-open-file (index (merge-pathnames "index.json" feed-store) :direction :output)
69
-		    (%encode-feed-as-json feed
70
-					  (store items feed-store)
71
-					  feed-store
72
-					  index)))
73
-		feed-link)))))
76
+        (values (prog1-let ((feed-title title)
77
+                            (feed-store (get-feed-store-name feed directory)))
78
+                  (ensure-directories-exist feed-store)
79
+                  (with-open-file (index (merge-pathnames "index.json" feed-store) :direction :output)
80
+                    (%encode-feed-as-json feed
81
+                                          (store (copy-seq items) feed-store)
82
+                                          feed-store
83
+                                          index)))
84
+                feed-link)))))
74 85
 
75 86
 (defmethod store ((feed alimenta:feed) (stream stream))
76 87
   (handler-bind ((error (lambda (c)
77
-			  (typecase c
78
-			    (feed-error c)
79
-			    (t (wrap-condition c feed))))))
88
+                          (typecase c
89
+                            (feed-error c)
90
+                            (t (wrap-condition c feed))))))
80 91
     (yason:with-output (stream :indent t)
81 92
       (yason:with-object ()
82
-	(yason:with-object-element ("metadata")
83
-	  (yason:encode-object feed))
84
-	(yason:with-object-element ("items")
85
-	  (yason:with-array ()
86
-	    (for:for ((item over feed))
87
-	      (store item stream))))))))
88
-
89
-(defun %encode-item (root-dir item)
90
-  (destructuring-bind (title path) item
91
-    (format t "~&Encoding ~a~%" title)
92
-    (restart-once (fix-pathname () (fix-path path))
93
-      (let ((pathname (uiop:enough-pathname path root-dir)))
94
-	(yason:with-object ()
95
-	  (yason:encode-object-element "title" title)
96
-	  (yason:encode-object-element "path" pathname))))))
93
+        (yason:with-object-element ("metadata")
94
+          (yason:encode-object feed))
95
+        (yason:with-object-element ("items")
96
+          (yason:with-array ()
97
+            (for:for ((item over feed))
98
+              (store item stream))))))
99
+    (list (alimenta:title feed)
100
+          stream)))
97 101
 
98 102
 (defmethod store ((item alimenta:item) (directory pathname))
99 103
   (flet ((get-item-store-name (item directory)
100
-	   (let ((id (get-id item)))
101
-	     (merge-pathnames (make-pathname :name id :version nil :type "json") directory))))
104
+           (let ((id (get-id item)))
105
+             (merge-pathnames (make-pathname :name id :version nil :type "json") directory))))
102 106
 
103 107
     (prog1-let ((item-title (alimenta:title item))
104
-		(fn (get-item-store-name item directory)))
108
+                (fn (get-item-store-name item directory)))
105 109
       (with-open-file (item-f fn :direction :output)
106
-	(yason:encode item item-f)))))
110
+        (yason:encode item item-f)))))
107 111
 
108 112
 (defmethod store ((item alimenta:item) (stream stream))
109 113
   (yason:with-output (stream :indent t)
110
-    (yason:encode-object item)))
114
+    (yason:encode-object item))
115
+  (list (alimenta:title item)
116
+        stream))
117
+
118
+;; The feed is always index.json
119
+(defmethod stream-provider:stream-key (provider (feed alimenta:feed))
120
+  (pathname
121
+   (string-join
122
+    (list (get-id feed)
123
+          "index.json")
124
+    "/")))
125
+
126
+(defmethod stream-provider:stream-key :around ((provider stream-provider:file-provider)
127
+                                               (feed alimenta:feed))
128
+  (prog1-bind (result (call-next-method))
129
+    (ensure-directories-exist (merge-pathnames result
130
+                                               (stream-provider:root provider)))))
131
+
132
+(defmethod stream-provider:stream-key (provider (item alimenta:item))
133
+  (let ((id (get-id item)))
134
+    (make-pathname :name id :version nil :type "json")))
111 135
 
136
+(defclass feed-stream-provider (stream-provider:file-provider)
137
+  ((%item-providers :accessor item-providers :initform (make-hash-table :test 'equal))))
138
+
139
+(defmethod stream-provider:get-nested-provider ((provider stream-provider:stream-provider) (streamable alimenta:feed))
140
+  (with (items-root (uiop:merge-pathnames* (uiop:pathname-directory-pathname (stream-provider:stream-key provider streamable))
141
+                                           (stream-provider:root provider)))
142
+    (format t "~&items-root: ~a   @#%@#$^#$&^&%$~%" items-root) (terpri)
143
+    (ensure-gethash items-root
144
+                    (item-providers provider)
145
+                    (make-instance 'stream-provider:file-provider :root items-root))))
146
+
147
+(defmethod store :around ((item alimenta:feed-entity) (stream-provider stream-provider:stream-provider))
148
+  (call-next-method)
149
+  (list (alimenta:title item)
150
+        (stream-provider:stream-key stream-provider item)))
151
+
152
+(defmethod store ((item alimenta:item) (stream-provider stream-provider:stream-provider))
153
+  (stream-provider:with-storage-stream (s item stream-provider)
154
+    (store item s)))
155
+
156
+(defmethod store ((feed alimenta:feed) (stream-provider stream-provider:stream-provider))
157
+  (stream-provider:with-storage-stream (s feed stream-provider)
158
+    (with-accessors ((description alimenta:description)
159
+                     (feed-link alimenta:feed-link)
160
+                     (items alimenta:items)
161
+                     (link alimenta:link)
162
+                     (source-type alimenta:source-type)
163
+                     (title alimenta:title)) feed
164
+      (let* ((item-provider (stream-provider:get-nested-provider stream-provider feed))
165
+             (item-storage-info (map 'list (op (store _ item-provider))
166
+                                                        items)))
167
+        (yason:with-output (s :indent t)
168
+          (yason:with-object ()
169
+            (yason:encode-object-element "metadata" feed)
170
+            (yason:with-object-element ("items")
171
+              (yason:with-array ()
172
+                (dolist (item item-storage-info)
173
+                  (destructuring-bind (title path) item
174
+                    (yason:with-object ()
175
+                      (yason:encode-object-elements "title" title
176
+                                                    "path" path))))))))))))
... ...
@@ -8,59 +8,59 @@
8 8
 
9 9
 (defun get-store-directory-name (timestamp)
10 10
   (flet ((make-dirname (timestamp)
11
-	   (-> (local-time:format-timestring nil (local-time:timestamp-minimize-part timestamp :sec)
12
-					     :format +dirname-format+)
13
-	       (merge-pathnames *feed-base*))))
11
+           (-> (local-time:format-timestring nil (local-time:timestamp-minimize-part timestamp :sec)
12
+                                             :format +dirname-format+)
13
+               (merge-pathnames *feed-base*))))
14 14
     (-> (prog1-let ((result (make-dirname timestamp)))
15
-	  (ensure-directories-exist result))
16
-	(car))))
15
+          (ensure-directories-exist result))
16
+        (car))))
17 17
 
18 18
 (defun test-feed-list ()
19 19
   (values '("http://feeds.feedburner.com/GamasutraFeatureArticles/"
20
-	    "https://www.codinghorror.com/blog/index.xml"
21
-	    "https://sancrucensis.wordpress.com/feed/")
22
-	  #p"/tmp/feed-archive/"))
20
+            "https://www.codinghorror.com/blog/index.xml"
21
+            "https://sancrucensis.wordpress.com/feed/")
22
+          #p"/tmp/feed-archive/"))
23 23
 
24 24
 (defun init-feeds (&key feed-list archive-root)
25 25
   (ubiquitous:restore 'alimenta.feed-archiver)
26 26
   (let ((default-root (or archive-root
27
-			  (merge-pathnames ".feed-archive/"
28
-					   (truename "~/")))))
27
+                          (merge-pathnames ".feed-archive/"
28
+                                           (truename "~/")))))
29 29
     (values (ubiquitous:defaulted-value feed-list :feeds)
30
-	    (ubiquitous:defaulted-value default-root :archive :root))))
30
+            (ubiquitous:defaulted-value default-root :archive :root))))
31 31
 
32 32
 (defun add-feed (feed)
33 33
   (init-feeds)
34 34
   (pushnew feed
35
-	   (ubiquitous:value :feeds)
36
-	   :test #'equalp))
35
+           (ubiquitous:value :feeds)
36
+           :test #'equalp))
37 37
 
38 38
 (defun safe-pull-feed (feed-url &aux (pop-times 0))
39 39
   "Handles date parsing errors in the feed: chronicity won't parse
40 40
    certain date formats, this catches the error and modifies the
41 41
    format to something chronicity can handle."
42 42
   (flet ((pop-50-tokens (c)
43
-	   (declare (ignore c))
44
-	   (when (find-restart 'alimenta:pop-token) 
45
-	     (if (< pop-times 50)
46
-		 (progn (incf pop-times)
47
-			(format t "~&Processing error, trying to pop a token (popped ~d times)~%"
48
-				pop-times)
49
-			(alimenta:pop-token))
50
-		 (continue)))))
43
+           (declare (ignore c))
44
+           (when (find-restart 'alimenta:pop-token) 
45
+             (if (< pop-times 50)
46
+                 (progn (incf pop-times)
47
+                        (format t "~&Processing error, trying to pop a token (popped ~d times)~%"
48
+                                pop-times)
49
+                        (alimenta:pop-token))
50
+                 (continue)))))
51 51
     (handler-bind ((warning #'muffle-warning)
52
-		   (error #'pop-50-tokens))
52
+                   (error #'pop-50-tokens))
53 53
       (prog1 (alimenta.pull-feed:pull-feed feed-url)
54
-	;; Why am I decf-ing here?
55
-	(decf pop-times)))))
54
+        ;; Why am I decf-ing here?
55
+        (decf pop-times)))))
56 56
 
57 57
 (defmacro with-progress-message ((stream before after &optional (error-msg " ERROR~%~4t~a~%")) &body body)
58 58
   (once-only (before after stream)
59 59
     `(handler-bind ((error (op (format ,stream ,error-msg _))))
60 60
        (format ,stream "~&~a . . ." ,before)
61 61
        (multiple-value-prog1 (progn
62
-			       ,@body)
63
-	 (format ,stream " ~a~%" ,after)))))
62
+                               ,@body)
63
+         (format ,stream " ~a~%" ,after)))))
64 64
 
65 65
 (defun skip-feed ()
66 66
   (when-let ((restart (find-restart 'skip-feed)))
... ...
@@ -72,41 +72,57 @@
72 72
 
73 73
 (defun pull-and-store-feeds (feeds pull-directory)
74 74
   (mapcar (op (pull-and-store-feed _ pull-directory))
75
-	  feeds))
75
+          feeds))
76 76
 
77
-(defun pull-and-store-feed (feed-url pull-directory)
77
+(defun log-pull (feed-puller feed-url stream)
78
+  (let ((before-message (concatenate 'string "Trying to pull: " feed-url)))
79
+    (with-progress-message (stream before-message "Success")
80
+      (funcall feed-puller feed-url))))
81
+
82
+(defun normalize-feed (feed-url feed)
83
+  (alimenta:filter-feed (coerce-feed-link feed-url feed)
84
+                        (complement #'older-than-a-month)
85
+                        :key 'alimenta:date))
86
+
87
+(defun log-serialization (feed-url stream feed path)
88
+  (with-progress-message (stream "Serializing XML" (format nil "done with ~a" feed-url))
89
+    (save-feed feed (merge-pathnames "feed.xml" path))))
90
+
91
+(defun feed-relative-pathname (path &optional (feed-base *feed-base*))
92
+  (uiop:enough-pathname path feed-base))
93
+
94
+(defun pull-and-store-feed (feed-url pull-directory &optional (feed-puller #'safe-pull-feed))
78 95
   (flet ((log-pull (stream)
79
-	   (let ((before-message (format nil "Trying to pull: ~a" feed-url)))
80
-	     (with-progress-message (stream before-message "Success")
81
-	       (prog1 (safe-pull-feed feed-url)))))
82
-	 (log-serialization (stream feed path)
83
-	   (with-progress-message (stream "Serializing XML" (format nil "done with ~a" feed-url))
84
-	     (save-feed feed (merge-pathnames "feed.xml" path)))))
96
+           (declare (inline) (dynamic-extent stream))
97
+           (log-pull feed-puller feed-url stream))
98
+         (log-serialization (stream feed path)
99
+           (declare (inline) (dynamic-extent stream))
100
+           (log-serialization feed-url stream feed path)))
85 101
 
86 102
     (with-simple-restart (skip-feed "Stop processing for ~a" feed-url)
87 103
       (let* ((feed (with-retry ("Pull feed again.")
88
-		     (alimenta:filter-feed (coerce-feed-link feed-url
89
-							     (log-pull t))
90
-					   (complement #'older-than-a-month)
91
-					   :key 'alimenta:date))))
92
-	(multiple-value-bind (result url) (store feed pull-directory)
93
-	  (destructuring-bind (title path) result
94
-	    (log-serialization t feed path)
95
-	    (make-feed-reference url :title title
96
-				 :path (uiop:enough-pathname path *feed-base*))))))))
104
+                     (normalize-feed feed-url (log-pull t)))))
105
+        (trivia:multiple-value-match (store feed pull-directory)
106
+          (((list title path) url)
107
+           (log-serialization t feed path)
108
+           (make-feed-reference url :title title :path (feed-relative-pathname path))))))))
97 109
 
98 110
 (defun feed-index (index-stream pull-time references)
99 111
   (yason:with-output (index-stream :indent t)
100 112
     (yason:encode-object
101 113
      (make-feed-index pull-time (remove-if 'null references)))))
102 114
 
103
-(defun archive-feeds ()
115
+(defun archive-feeds (pull-time pull-directory index-stream)
116
+  (let ((references (pull-and-store-feeds *feeds* pull-directory)))
117
+    (feed-index index-stream pull-time references)
118
+    references))
119
+
120
+(defun archive-feeds-nondeterm ()
104 121
   (let* ((pull-time (local-time:now))
105 122
 	 (pull-directory (get-store-directory-name pull-time)) 
106
-	 (references (pull-and-store-feeds *feeds* pull-directory))
107 123
 	 (index-path (merge-pathnames "index.json" pull-directory)))
108 124
     (with-open-file (index index-path :direction :output)
109
-      (feed-index index pull-time references))
125
+      (archive-feeds pull-time pull-directory index))
110 126
     (format t "~&!! pull-directory ~a~%" (uiop:enough-pathname pull-directory *feed-base*))))
111 127
 
112 128
 ;; This is an ungodly mess, we need to avoid funneling everything through fix-pathname-or-skip
... ...
@@ -144,4 +160,5 @@
144 160
 			  (fix-pathname-or-skip _1 :restart 'continue)))))
145 161
 	(multiple-value-bind (*feeds* *feed-base*) (funcall feed-list-initializer)
146 162
 	  (alimenta.pull-feed:with-user-agent ("Feed Archiver v0.1b")
147
-	    (archive-feeds)))))))
163
+	    (archive-feeds-nondeterm)))))))
164
+
... ...
@@ -1,18 +1,32 @@
1
-(defpackage :alimenta.feed-archive.tools
2
-  (:use :cl :alexandria :serapeum :fw.lu)
1
+(defpackage :afa-user
2
+  (:use cl)
3
+  (:export #:define-package))
4
+(in-package :afa-user)
5
+
6
+(defmacro define-package (name &body arguments)
7
+  (let ((uses (cons :use
8
+		    (union '(:cl :alexandria :serapeum)
9
+			   (cdr (assoc :use arguments))))))
10
+    `(progn
11
+       (defpackage ,name
12
+	 ,uses
13
+	 ,@(remove :use arguments :key 'car)))))
14
+
15
+(define-package :alimenta.feed-archive.tools
16
+  (:use :fw.lu)
3 17
   (:shadow :->)
4 18
   (:export :fix-pathname :sha256-string :get-id :older-than-a-week :-> :get-feed-store-name
5 19
 	   :store :get-item-store-name :restart-once :coerce-feed-link :with-retry
6 20
 	   :older-than-a-month))
7 21
 
8
-(defpackage :alimenta.feed-archive.encoders
9
-  (:use :cl :alexandria :serapeum :fw.lu :alimenta.feed-archive.tools)
22
+(define-package :alimenta.feed-archive.encoders
23
+  (:use :fw.lu :alimenta.feed-archive.tools)
10 24
   (:shadowing-import-from :alimenta.feed-archive.tools :->)
11 25
   (:export :skip-item :the-condition :the-feed :feed-error
12
-	   :unwrap-feed-errors))
26
+           :unwrap-feed-errors :feed-stream-provider))
13 27
 
14
-(defpackage :alimenta.feed-archive
15
-  (:use :cl :alexandria :serapeum :fw.lu :alimenta.feed-archive.tools)
28
+(define-package :alimenta.feed-archive
29
+  (:use :fw.lu :alimenta.feed-archive.tools)
16 30
   (:shadowing-import-from :alimenta.feed-archive.tools :->)
17 31
   (:export #:init-feeds #:archive-feeds #:command-line-main))
18 32
 
19 33
new file mode 100644
... ...
@@ -0,0 +1,68 @@
1
+(afa-user:define-package :stream-provider
2
+  (:use :fw.lu)
3
+  (:export
4
+   #:get-stream-for
5
+   #:stream-provider
6
+   #:string-provider
7
+   #:file-provider
8
+   #:stream-key
9
+   #:root
10
+   #:streams
11
+   #:with-storage-stream))
12
+(cl:in-package :stream-provider)
13
+
14
+(defclass stream-provider ()
15
+  ())
16
+
17
+(defvar *store-root* *default-pathname-defaults*)
18
+
19
+(defgeneric stream-key (provider item)
20
+  (:documentation "return a key for an item: must be able to be compared with EQUAL
21
+
22
+If you override on provider, make sure to CALL-NEXT-METHOD"))
23
+
24
+(defgeneric get-stream-for (provider streamable &rest extra-args)
25
+  (:documentation "get a stream for a given streamable object"))
26
+
27
+(defgeneric root (provider)
28
+  (:documentation "get the base path for the streams"))
29
+
30
+
31
+(defclass string-provider (stream-provider)
32
+  ((%streams :reader streams :initform (make-hash-table :test 'equal))))
33
+
34
+(defmethod root ((provider string-provider))
35
+  #p"/")
36
+
37
+(defmethod stream-key :around (provider item)
38
+  (let ((key (call-next-method)))
39
+    (check-type key (or string pathname))
40
+    (uiop:enough-pathname key (root provider))))
41
+
42
+(defmethod get-stream-for ((provider string-provider) streamable &rest extra-args)
43
+  (declare (ignore extra-args))
44
+  (with-accessors* (streams) provider
45
+    (vector-update-stream:make-update-stream 
46
+     (setf (gethash (stream-key provider streamable) streams)
47
+           (make-array 10 :adjustable t :fill-pointer 0)))))
48
+
49
+(defclass file-provider (stream-provider)
50
+  ((%root :initarg :root :initform (error "need a root for a file-provider") :reader root)
51
+   (%if-exists :initarg :if-exists :initform :supersede :reader if-exists)))
52
+
53
+(defmethod get-stream-for ((provider file-provider) streamable &rest extra-args)
54
+  (declare (ignore extra-args))
55
+  (with-accessors* (if-exists root) provider
56
+    (let ((stream-key (merge-pathnames (stream-key provider streamable)
57
+                                       root)))
58
+      (when (eql if-exists :if-exists)
59
+        (ensure-directories-exist stream-key))
60
+      (open stream-key :direction :output :if-exists if-exists
61
+            :element-type 'octet))))
62
+
63
+(defmacro with-storage-stream ((stream-sym object provider &rest extra-args) &body body)
64
+  (once-only (object)
65
+    `(let ((,stream-sym (flexi-streams:make-flexi-stream (get-stream-for ,provider ,object ,@extra-args)
66
+                                                         :external-format :utf-8)))
67
+       (unwind-protect (progn ,@body)
68
+         (close ,stream-sym)))))