Selaa lähdekoodia

Import album, database

Innocenty Enikeew 10 vuotta sitten
vanhempi
commit
6c2057bc9b
5 muutettua tiedostoa jossa 238 lisäystä ja 19 poistoa
  1. 25 7
      assets/site.js
  2. 96 0
      incoming.html
  3. 4 3
      index.html
  4. 1 0
      photo-store.asd
  5. 112 9
      photo-store.lisp

+ 25 - 7
assets/site.js

@@ -2,13 +2,20 @@ function setAlbums(albums) {
     var dAlbums = document.getElementById('albums');
     dAlbums.innerHTML = '';
     Array.prototype.forEach.call(albums, function(alb, i){
-        var a = appendElement(dAlbums, 'a');
-        var img = appendElement(a, 'img');
-        img.src = alb.cover.src;
-        if (alb.title) {
-            var fig = appendElement(a, 'figure');
-            fig.textContent = alb.title;
-        };
+        var a = addAlbumDom(dAlbums, alb);
+        a.addEventListener('click', function() {
+            jsonGET('/api/albums/' + alb.id + '/', function (alb) {
+                startGallery(alb.photos);
+            });
+        });
+    });
+}
+
+function setIncoming(albums) {
+    var dAlbums = document.getElementById('albums');
+    dAlbums.innerHTML = '';
+    Array.prototype.forEach.call(albums, function(alb, i){
+        var a = addAlbumDom(dAlbums, alb);
         a.addEventListener('click', function() {
             startGallery(alb.photos);
         });
@@ -29,6 +36,17 @@ function startGallery(items, index) {
     return gallery;
 }
 
+function addAlbumDom(parent, alb) {
+    var a = appendElement(parent, 'a');
+    var img = appendElement(a, 'img');
+    img.src = alb.cover.src;
+    if (alb.title) {
+        var fig = appendElement(a, 'figure');
+        fig.textContent = alb.title;
+    };
+    return a;
+}
+
   function appendElement(parent, tag, id, className, innerHTML) {
     var el = document.createElement(tag);
     if (id) el.id = id;

+ 96 - 0
incoming.html

@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<html lang="en" prefix="og: http://ogp.me/ns#" debug="true">
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">    
+    <meta charset="utf-8">
+
+    <title>Photo store: Incoming</title>
+
+    <meta name="viewport" content="width = device-width, initial-scale = 1.0"> 
+
+    <link rel="stylesheet" href="/css/site.css">
+    <script src="/js/site.js"></script>
+
+    <!-- Core CSS file -->
+    <link rel="stylesheet" href="/css/photoswipe.css"> 
+
+    <!-- Skin CSS file (styling of UI - buttons, caption, etc.)
+         In the folder of skin CSS file there are also:
+         - .png and .svg icons sprite, 
+         - preloader.gif (for browsers that do not support CSS animations) -->
+    <link rel="stylesheet" href="/css/default-skin.css"> 
+
+    <!-- Core JS file -->
+    <script src="/js/photoswipe.min.js"></script> 
+
+    <!-- UI JS file -->
+    <script src="/js/photoswipe-ui-default.min.js"></script> 
+
+    <!--[if lt IE 9]>
+        <script>
+          document.createElement('figure');
+        </script>
+    <![endif]-->
+  </head>
+
+  <body>
+	<div class="section section--head">
+	  <div class="row row--heading">
+		<a href="/"><h1>PhotoStore</h1></a>
+        <h3>Incoming</h3>
+	  </div>
+    </div>
+
+    <div class="section">
+	  <div class="row">
+        <div id="albums" class="demo-gallery"></div>
+      </div>
+    </div>
+
+    <div id="gallery" class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
+      <div class="pswp__bg"></div>
+      <div class="pswp__scroll-wrap">
+        <div class="pswp__container">
+		  <div class="pswp__item"></div>
+		  <div class="pswp__item"></div>
+		  <div class="pswp__item"></div>
+        </div>
+        <div class="pswp__ui pswp__ui--hidden">
+          <div class="pswp__top-bar">
+			<div class="pswp__counter"></div>
+			<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
+			<!--<button class="pswp__button pswp__button--share" title="Share"></button> -->
+			<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
+			<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
+			<div class="pswp__preloader">
+			  <div class="pswp__preloader__icn">
+				<div class="pswp__preloader__cut">
+				  <div class="pswp__preloader__donut"></div>
+				</div>
+			  </div>
+			</div>
+          </div>
+		  <div class="pswp__loading-indicator"><div class="pswp__loading-indicator__line"></div></div>
+          <!--<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
+	        <div class="pswp__share-tooltip">
+	        </div>
+	      </div>
+          -->
+          <button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
+          <button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>
+          <div class="pswp__caption">
+            <div class="pswp__caption__center">
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <script type="text/javascript">
+      jsonGET('/api/incoming/', function(data) {
+          if (data) 
+              setIncoming(data);
+      });
+    </script>
+  </body>
+</html>

+ 4 - 3
index.html

@@ -37,6 +37,7 @@
 	<div class="section section--head">
 	  <div class="row row--heading">
 		<h1>PhotoStore</h1>
+        <a href="/incoming/"><h3>Incoming</h3></a>
 	  </div>
     </div>
 
@@ -86,9 +87,9 @@
     </div>
 
     <script type="text/javascript">
-      jsonGET('/incoming/', function(data) {
-          setAlbums(data);
-          //startGallery(data);
+      jsonGET('/api/albums/', function(data) {
+          if (data)
+              setAlbums(data);
       });
     </script>
   </body>

+ 1 - 0
photo-store.asd

@@ -5,6 +5,7 @@
   :license "MIT"
   :depends-on (:cl-json
                :geo
+               :iterate
                :local-time
                :log4cl
                :restas

+ 112 - 9
photo-store.lisp

@@ -1,4 +1,5 @@
 (in-package #:photo-store)
+(use-package :iter)
 
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (restas::register-pkgmodule-traits 'photo-store)
@@ -8,6 +9,72 @@
 ;; photos: id | album_id | filename | size | taken | width | height | lat | lon
 ;; albums: id | parent_id | path | date | name | description | cover_id | accessibility
 
+(defvar *db-path* "db.sqlite" "SQLite database path")
+
+(defmacro with-db ((db) &body body)
+  `(sqlite:with-open-database (,db *db-path* :busy-timeout 10)
+     (sqlite:execute-non-query ,db "PRAGMA foreign_keys = ON")
+     ,@body))
+
+(defun db-init ()
+  (with-db (db)
+    (sqlite:execute-non-query db "create table if not exists photos (id INTEGER PRIMARY KEY, path, size, taken, width, height, lat, lon)")
+    (sqlite:execute-non-query db "create table if not exists albums (id INTEGER PRIMARY KEY, name, description, cover_id REFERENCES photos)")
+    (sqlite:execute-non-query db "create table if not exists album_photos (album_id REFERENCES albums NOT NULL, photo_id REFERENCES photos NOT NULL, description, idx, hidden)")))
+
+(defun db/add-photo (db info)
+  (let* ((path (namestring (uiop:subpathp (aget :path info) *photo-storage-path*)))
+         (size (aget :length info))
+         (taken (local-time:timestamp-to-unix (aget :created-at info)))
+         (dim (aget :dim info))
+         (w (first dim)) (h (second dim))
+         (loc (aget :location info))
+         (lat (and loc (geo:latitude-deg loc)))
+         (lon (and loc (geo:longitude-deg loc))))
+    (sqlite:execute-non-query
+     db "insert into photos (path, size, taken, width, height, lat, lon) values (?, ?, ?, ?, ?, ?, ?)"
+     path size taken w h lat lon)
+    (sqlite:last-insert-rowid db)))
+
+(defun db/add-album (db name &optional description cover-id)
+  (sqlite:execute-non-query
+   db "insert into albums (name, description, cover_id) values (?, ?, ?)"
+   name description cover-id)
+  (sqlite:last-insert-rowid db))
+
+(defun db/add-album-photo (db album-id photo-id &optional description idx hidden)
+  (sqlite:execute-non-query
+   db "insert into album_photos (album_id, photo_id, description, idx, hidden) values (?, ?, ?, ?, ?)"
+   album-id photo-id description idx hidden))
+
+(defvar *photo-storage-url* nil "base url for photo storage")
+(defun db-load-album (album-id)
+  (with-db (db)
+    (multiple-value-bind (name cover-id)
+        (sqlite:execute-one-row-m-v db "select name, cover_id from albums where id = ?" album-id)
+      (let ((photos (iter (for (id path title w h)
+                               in-sqlite-query "select p.id, p.path, ap.description, p.width, p.height from photos p inner join album_photos ap on ap.photo_id = p.id where ap.album_id = ? order by ap.idx"
+                               on-database db
+                               with-parameters (album-id))
+                          (collect (list (cons :id id)
+                                         (cons :src (concatenate 'string *photo-storage-url* path))
+                                         (cons :title title)
+                                         (cons :w w) (cons :h h))))))
+        (list (cons :title name)
+              (cons :cover (find cover-id photos :key (agetter :id)))
+              (cons :photos photos))))))
+
+(defun db-load-albums ()
+  (with-db (db)
+    (iter (for (id title cover-path cover-w cover-h)
+               in-sqlite-query "select a.id, a.name, c.path, c.width, c.height from albums a inner join photos c on a.cover_id=c.id order by c.taken" on-database db)
+          (collect (list (cons :id id)
+                         (cons :title title)
+                         (cons :cover (list (cons :src (concatenate 'string
+                                                                    *photo-storage-url* cover-path))
+                                            (cons :w cover-w)
+                                            (cons :h cover-h))))))))
+
 (defun dirname (directory)
   (car (last (pathname-directory directory))))
 
@@ -75,25 +142,52 @@
   (delete-duplicates (mapcar #'uiop:pathname-directory-pathname files) :test #'equal))
 
 (defun move-file-to (file dest)
-  (uiop:rename-file-overwriting-target
-   file (uiop:make-pathname* :defaults file :directory (pathname-directory dest))))
+  (let ((destpath (uiop:make-pathname* :defaults file :directory (pathname-directory dest))))
+    (when (probe-file destpath)
+      (error "File ~S exists" destpath))
+    (uiop:rename-file-overwriting-target file destpath)
+    destpath))
 
-(defun move-images-with-folders (files dest)
+(defun move-images-with-folders (files dest &optional after-move-func)
   (labels ((move (files dest)
              (dolist (f files)
-               (move-file-to f dest))))
+               (funcall after-move-func (move-file-to f dest)))))
     (let ((dirs (directories files)))
+      (unless after-move-func (setf after-move-func #'identity))
       (ensure-directories-exist dest)
       (move files dest)
-      ;; Move all non-image files from original folders if no images left there
+      ;; Move all non-image files from original folders if no images left there.
+      ;; Do not call after-move-func on them
+      (setf after-move-func #'identity)
       (dolist (dir dirs)
         (unless (other-images dir files)
           (move (uiop:directory-files dir) dest)
-          (uiop:delete-empty-directory dir))))))
+          (ignore-errors (uiop:delete-empty-directory dir)))))))
+
+(defun import-album (name dest paths)
+  (let (added-photos
+        (files (remove-if-not #'(lambda (p) (uiop:subpathp p *incoming-path*))
+                              (mapcar #'probe-file paths))))
+    (when files
+      (with-db (db)
+        (sqlite:with-transaction db
+          (labels ((handle-moved (path)
+                     (let ((info (load-photo-info path)))
+                       (push (cons (db/add-photo db info) (aget :name info)) added-photos)
+                       (log:info "Moved" (aget :path info)))))
+            (move-images-with-folders files dest #'handle-moved)
+            (let ((album-id (db/add-album db name nil (car (middle added-photos)))))
+              (loop for (photo-id . name) in (reverse added-photos)
+                 for idx from 1
+                 do (db/add-album-photo db album-id photo-id name idx))
+              album-id)))))))
 
 (restas:define-route main ("")
   (asdf:system-relative-pathname :photo-store "index.html"))
 
+(restas:define-route incoming ("incoming/")
+  (asdf:system-relative-pathname :photo-store "incoming.html"))
+
 ;; Assets file path
 (defparameter *assets-path*
   (asdf:system-relative-pathname :photo-store "assets/"))
@@ -108,9 +202,18 @@
   (json:encode-json-to-string object))
 
 (defvar *incoming-path* nil "Path for incoming photos")
-(restas:define-route incoming ("incoming/"
-                               :content-type "application/json")
-  (json (mapcar 'album-item (load-albums-from-dir *incoming-path*))))
+(restas:define-route api/incoming ("api/incoming/"
+                                   :content-type "application/json")
+  (json (mapcar #'(lambda (a) (album-item a t)) (load-albums-from-dir *incoming-path*))))
+
+(restas:define-route api/albums ("api/albums/"
+                                 :content-type "application/json")
+  (json (db-load-albums)))
+
+(restas:define-route api/album ("api/albums/:album-id/"
+                                :content-type "application/json")
+  (:sift-variables (album-id 'integer))
+  (json (db-load-album album-id)))
 
 (defvar *photo-storage-path* nil "Destination storage path")