commit 9a894fbe1c966dbd6a613bbad720b98fe334cb13
parent cf177446e9bca4c62fa76a27ad803b454f83d818
Author: Tomas Hlavaty <tom@logand.com>
Date:   Sun, 27 Oct 2013 20:04:49 +0100
content addressable storage code added
Diffstat:
| A | cas.lisp |  |  | 178 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| M | cl-rw.asd |  |  | 3 | ++- | 
2 files changed, 180 insertions(+), 1 deletion(-)
diff --git a/cas.lisp b/cas.lisp
@@ -0,0 +1,178 @@
+;;; Copyright (C) 2013 Tomas Hlavaty <tom@logand.com>
+;;;
+;;; Permission is hereby granted, free of charge, to any person
+;;; obtaining a copy of this software and associated documentation
+;;; files (the "Software"), to deal in the Sofe without
+;;; restriction, irncluding without limitation the rights to use, copy,
+;;; modify, merge, publish, distribute, sublicense, and/or sell copies
+;;; of the Software, and to permit persons to whom the Software is
+;;; furnished to do so, subject to the following conditions:
+;;;
+;;; The above copyright notice and this permission notice shall be
+;;; included in all copies or substantial portions of the Software.
+;;;
+;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+;;; EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+;;; MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+;;; NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+;;; HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+;;; WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+;;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+;;; DEALINGS IN THE SOFTWARE.
+
+(defpackage :rw.cas
+  (:use :cl)
+  (:export :*db-pathname*
+           :defrecord
+           :defreference
+           :load-record
+           :reference-keys
+           :with-record
+           :with-record-cache))
+
+(in-package :rw.cas)
+
+(defvar *db-pathname*)
+(defvar *record-cache*)
+
+(defun object-pathname (oid)
+  (format nil "~a/objects/~a/~a" *db-pathname* (subseq oid 0 2) oid))
+
+(defun reference-pathname (kind key)
+  (format nil "~a/refs/~(~a~)/~a" *db-pathname* kind key))
+
+(defun reference-keys (kind)
+  (mapcar #'pathname-name (directory (format nil "~a/refs/~(~a~)/*" *db-pathname* kind))))
+
+(defun object-exists-p (oid)
+  (probe-file (object-pathname oid)))
+
+(defun store-object (pathname)
+  ;; cant use rename-file, errno cross device link on ccl
+  (let* ((oid (rw.os:sha1sum pathname))
+         (f (object-pathname oid)))
+    (ensure-directories-exist f)
+    ;; TODO atomic probe and move
+    (when (probe-file f)
+      (error "object ~s already exists" oid))
+    (rw.os:run-command "mv" (list "-n" (namestring pathname) (namestring f)))
+    oid))
+
+(defun store-record (record)
+  (let ((f (rw.os:make-temporary-file :template "/tmp/cafsXXXXXX")))
+    (with-open-file (s f
+                       :direction :output
+                       :if-exists :supersede
+                       :if-does-not-exist :error)
+      (write record :stream s))
+    (store-object f)))
+
+(defun load-record (oid)
+  (or (gethash oid *record-cache*)
+      (setf (gethash oid *record-cache*)
+            (with-open-file (s (object-pathname oid))
+              (read s)))))
+
+(defmacro with-record-cache (() &body body)
+  `(let ((*record-cache* (make-hash-table :test #'equal)))
+     ,@body))
+
+(defun check-ptype (type value) ;; TODO
+  (assert type)
+  #+nil
+  (if (atom type)
+      (case type
+        (boolean '(q:boolean-type))
+        (integer '(q:integer-type))
+        (string '(q:varchar-type))
+        (pdate '(q:date-type))
+        (ptime '(q:time-type))
+        (ptimestamp-tz '(q:timestamp-with-timezone-type))
+        (universal-time '(q:timestamp-with-timezone-type))
+        (octet-vector '(q:blob-type))
+        (t (if (subtypep type 'persistent-type)
+               (expand-ptype-to-db (persistent-type-pkey-type type))
+               (or (get type 'db-type)
+                   (expand-ptype-to-db (ptype-specifier type))))))
+      (ecase (car type)
+        (or
+          (destructuring-bind (a b) (cdr type)
+            (assert (eq 'null a))
+            (check-ptype b)))
+        (integer `(q:integer-type ,(cadr type)))
+        (string `(q:char-type ,(cadr type)))
+        (text `(q:varchar-type ,(cadr type)))))
+  value)
+
+(defun make-record (x)
+  (let ((oid (store-record x)))
+    (load-record oid) ;; TODO optimize, simply put into cache, but for now check storing works
+    oid))
+
+(defmacro defrecord (name super &body slots)
+  (let ((package (symbol-package name)))
+    `(progn
+       (defun ,(intern (format nil "MAKE-~a" name) package)
+           (&key ,@(loop
+                      for slot in (car slots)
+                      collect (destructuring-bind (name &key initform &allow-other-keys)
+                                  slot
+                                (if initform
+                                    (list name initform)
+                                    name))))
+         (make-record (list ',name
+                            ,@(loop
+                                 for slot in (car slots)
+                                 appending (destructuring-bind (name &key type initform)
+                                               slot
+                                             `(',name (check-ptype ',type ,name)))))))))
+  #+nil
+  `(progn
+     ,(build-defrecord name body)
+     (eval-when (:compile-toplevel :load-toplevel :execute)
+       (setf (get ',name 'defrecord-slots) ',(car body)
+             (get ',name 'defrecord-specs) ',(cdr body)))))
+
+(defmacro with-record (slots oid &body body)
+  (let ((r (gensym)))
+    `(let ((,r (load-record ,oid)))
+       (let ,(loop ;; TODO optimize, like destructuring-bind but with custom names
+                for (var slot) in slots
+                collect `(,var (getf (cdr ,r) ',slot)))
+         ,@body))))
+
+(defun load-reference (kind key)
+  (with-open-file (s (reference-pathname kind key))
+    (read s)))
+
+(defun store-reference (kind key oid how)
+  (let ((f (reference-pathname kind key)))
+    (multiple-value-bind (yes no) (ecase how
+                                    (:create (values :create :error))
+                                    (:update (values :error :supersede)))
+      (with-open-file (s f
+                         :direction :output
+                         :if-does-not-exist no
+                         :if-exists yes)
+        (write oid :stream s)))))
+
+(defun make-reference (kind key oid)
+  (store-reference kind key oid :create))
+
+(defun update-reference (kind key oid)
+  (store-reference kind key oid :update))
+
+(defun dereference (kind key)
+  (let ((oid (load-reference kind key)))
+    (load-record oid)
+    oid))
+
+(defmacro defreference (name kind ptype) ;; TODO check ptype + sequence
+  (let ((package (symbol-package name)))
+    `(progn
+       (defun ,(intern (format nil "MAKE-~a" name) package) (key oid)
+         (make-reference ',kind key oid))
+       (defun ,(intern (format nil "UPDATE-~a" name) package) (key oid)
+         (update-reference ',kind key oid))
+       (defun ,(intern (format nil "FOLLOW-~a" name) package) (key)
+         (dereference ',kind key)))))
diff --git a/cl-rw.asd b/cl-rw.asd
@@ -49,4 +49,5 @@
                (:file "http")
                (:file "net")
                (:file "calendar")
-               (:file "ui")))
+               (:file "ui")
+               (:file "cas")))