about summary refs log tree commit diff
diff options
context:
space:
mode:
authorArun Isaac2020-07-31 03:32:44 +0530
committerArun Isaac2020-07-31 03:32:44 +0530
commitcb070712c9326a97c750c9ccb5958fa0fa53e41b (patch)
tree3306e80a9d9371c3fde987796e0818f9fb4482c8
parent44f86d8131d21097311cf29b5127bb33a897f3c4 (diff)
downloadennum-cb070712c9326a97c750c9ccb5958fa0fa53e41b.tar.gz
ennum-cb070712c9326a97c750c9ccb5958fa0fa53e41b.tar.lz
ennum-cb070712c9326a97c750c9ccb5958fa0fa53e41b.zip
Switch from ennum-operation to ennum-exp.
ennum-exp is a simple embedded domain specific language to express
blog publishing in a more expression oriented form.

* ennum.el (ennum-operation): Delete type.
(ennum--do-operation): Delete function.
(ennum-intern, ennum--hash, ennum-file-hash, ennum--file-hash,
ennum--set-file-modes-recursively, ennum--rewrite-inputs, ennum-eval,
ennum-store-item-union): New functions.
(ennum-exp): New macro.
(ennum-setting): Introduce :store setting.
(ennum-publish-post, ennum-publish-generic, ennum-publish-index,
ennum-publish-feed, ennum--feed-entry, ennum-publish-image,
ennum-publish-copy, ennum-publish-link, ennum-publish): Switch from
ennum-operation to ennum-exp.
-rw-r--r--ennum.el542
1 files changed, 312 insertions, 230 deletions
diff --git a/ennum.el b/ennum.el
index a1fac83..adba985 100644
--- a/ennum.el
+++ b/ennum.el
@@ -15,15 +15,126 @@
 (defvar ennum-blog nil
   "Property list specifying ennum publish settings")
 
-(cl-defstruct (ennum-post (:constructor ennum-make-post)
+;; TODO: Should the store have an absolute path to deal with directory
+;; changes? Yes, since we ask for an absolute working directory, we
+;; should.
+(defun ennum-intern (filename)
+  (let* ((store-item
+          (expand-file-name (ennum-file-hash filename)
+                            (ennum-setting :store)))
+         (interned-path (expand-file-name (file-name-nondirectory filename)
+                                          store-item)))
+    (unless (file-exists-p store-item)
+      (ennum-copy filename interned-path)
+      (ennum--set-file-modes-recursively store-item #o555 #o444 #o555))
+    interned-path))
+
+(defun ennum--hash ()
+  "Return SHA256 hash of buffer contents encoded using
+filename-safe base64 encoding. See RFC 4648ยง5. Briefly, this is a
+variant of base64 encoding where characters + and / are replaced
+respectively by - and _, and the pad character = is optional."
+  (replace-regexp-in-string
+   (rx (any ?+ ?/ ?=))
+   (lambda (str)
+     (pcase str
+       ("+" "-")
+       ("/" "_")
+       ("=" "")))
+   (base64-encode-string
+    (secure-hash 'sha256 (current-buffer) nil nil t))))
+
+(defun ennum-file-hash (file)
+  (ennum--file-hash file (file-attribute-modification-time
+                          (file-attributes file))))
+
+(defmemoize ennum--file-hash (file last-modified)
+  (with-temp-buffer
+    ;; TODO: Use ennum-with-file-contents
+    (set-buffer-multibyte nil)
+    (insert-file-contents-literally file)
+    (ennum--hash)))
+
+(defun ennum--set-file-modes-recursively (directory directory-mode file-mode executable-file-mode)
+  (chmod directory directory-mode)
+  (seq-do (lambda (file)
+            (cond
+             ((file-directory-p file) (chmod file directory-mode))
+             ((file-executable-p file) (chmod file executable-file-mode))
+             (t (chmod file file-mode))))
+          (ennum-directory-files directory t t)))
+
+(cl-defstruct (ennum-exp (:constructor ennum-make-exp)
                          (:copier nil))
+  inputs proc)
+
+(defun ennum--rewrite-inputs (exp)
+  (pcase exp
+    (`(ennum-input ,arg)
+     (let ((input-identifier (gensym "input")))
+       (vector input-identifier (list arg) (list input-identifier))))
+    (`(,parent . ,childern)
+     (seq-reduce (pcase-lambda (`[,result ,inputs ,input-identifiers] node)
+                   (pcase (ennum--rewrite-inputs node)
+                     (`[,modified-tree ,new-inputs ,new-input-identifiers]
+                      (vector (append result (list modified-tree))
+                              (append inputs new-inputs)
+                              (append input-identifiers new-input-identifiers)))))
+                 exp
+                 (vector nil nil nil)))
+    (leaf (vector exp nil nil))))
+
+(defmacro ennum-exp (&rest body)
+  (let ((raw-expression `(progn ,@body)))
+    (pcase (ennum--rewrite-inputs raw-expression)
+      (`[,rewritten-body ,inputs ,input-identifiers]
+       `(ennum-eval
+         (ennum-make-exp
+          :inputs (list ,@inputs)
+          :proc (lambda ,input-identifiers ,rewritten-body)))))))
+
+(defun ennum-eval (exp)
+  (let ((output
+         (expand-file-name
+          (with-temp-buffer
+            (let ((print-length nil)
+                  (print-level nil))
+              (print exp (current-buffer)))
+            (ennum--hash))
+          (ennum-setting :store))))
+    ;; Create store if it doesn't exist
+    (ennum-mkdir-p (ennum-setting :store))
+    ;; Create store item if it doesn't already exist
+    (if (file-exists-p output)
+        (message "Skipping build of %s" output)
+      (message "Building %s" output)
+      (ennum-with-temporary-directory temporary-directory
+        (ennum-with-current-directory temporary-directory
+          (apply (ennum-exp-proc exp)
+                 (ennum-exp-inputs exp)))
+        (rename-file temporary-directory output))
+      (ennum--set-file-modes-recursively output #o555 #o444 #o555))
+    output))
+
+(defun ennum-store-item-union (items)
+  "Return a store item that is the union of ITEMS."
+  (ennum-exp
+   (seq-do (lambda (item)
+             (seq-do (lambda (destination)
+                       ;; TODO: Print warning about overwriting files?
+                       (let ((source (expand-file-name destination item)))
+                         (unless (file-exists-p destination)
+                           (if (file-directory-p source)
+                               (make-directory destination)
+                             (copy-file source destination t)))))
+                     (reverse (ennum-directory-files item nil t))))
+           (ennum-input items))))
+
+(cl-defstruct (ennum-post (:constructor ennum-make-post)
+                          (:copier nil))
   filename slug author date language links tangle
   summary tags thumbnail title translation-group video-posters)
 
-(cl-defstruct (ennum-operation (:constructor ennum-make-operation)
-                              (:copier nil))
-  inputs outputs publish)
-
 (defun ennum-posts (posts-directory)
   (sort (ennum--filter-map (lambda (file)
                              (when (string= (file-name-extension file) "org")
@@ -126,63 +237,56 @@ non-nil, include directories in the output."
   (concat (file-name-sans-extension filename) ".html"))
 
 (defun ennum-publish-post (posts)
-  (let ((link-publish-operations
-         (seq-mapcat 'ennum-publish-link (seq-mapcat 'ennum-post-links posts)))
-        (input-post-files (seq-map 'ennum-post-filename posts)))
-    (append
-     (list
-      (ennum-make-operation
-       :inputs (append input-post-files
-                       (seq-mapcat 'ennum-operation-inputs link-publish-operations))
-       :outputs (seq-map 'ennum--org-output-filename input-post-files)
-       :publish
-       (lambda (&rest output-files)
-         (seq-mapn
-          (lambda (post output-file)
-            (let ((system-time-locale (map-elt (ennum-setting :locale-alist)
-                                               (ennum-post-language post) nil 'string=)))
-              (ennum-with-file-contents (ennum-post-filename post)
-                (org-export-to-file
-                    'ennum-html output-file nil nil nil nil
-                    (list :ennum-translations (seq-remove (apply-partially 'equal post) posts)
-                          :ennum-video-posters (ennum-post-video-posters post))))))
-          posts
-          output-files))))
-     (ennum--filter-map
-      (lambda (post)
-        (when (ennum-post-tangle post)
-          (ennum-make-operation
-           :inputs (list (ennum-post-filename post))
-           :outputs (seq-map (lambda (tangle-output)
-                               (ennum--expand-relative tangle-output
-                                                      (ennum-setting :static-directory)))
-                             (ennum-post-tangle post))
-           :publish (lambda (&rest output-files)
-                      ;; TODO: Handle tangle outputs that are nested
-                      ;; into directories, and when each tangle output
-                      ;; is nested into a different directory.
-                      (let ((post-file-copy (concat
-                                             (file-name-directory (first output-files))
-                                             (file-name-nondirectory (ennum-post-filename post)))))
-                        (copy-file (ennum-post-filename post) post-file-copy)
-                        (org-babel-tangle-file post-file-copy)
-                        (delete-file post-file-copy))))))
-      posts)
-     link-publish-operations)))
+  (seq-mapcat (lambda (post)
+                (append
+                 (list
+                  (ennum-exp
+                   (ennum-input (seq-map (lambda (post)
+                                           (ennum-intern (ennum-post-filename post)))
+                                         posts))
+                   (let ((input-org-file (ennum-input (ennum-intern (ennum-post-filename post))))
+                         (output-file
+                          (ennum--org-output-filename (ennum-post-filename post))))
+                     (ennum-mkdir-p (file-name-directory output-file))
+                     (let ((system-time-locale (map-elt (ennum-setting :locale-alist)
+                                                        (ennum-post-language post) nil 'string=)))
+                       (ennum-with-file-contents input-org-file
+                         ;; TODO: Centralize these with feed generation
+                         (org-export-to-file
+                             'ennum-html output-file nil nil nil nil
+                             (list :ennum-translations (seq-remove (apply-partially 'equal post) posts)
+                                   :ennum-video-posters (ennum-post-video-posters post))))))))
+                 (when (ennum-post-tangle post)
+                   (list
+                    (ennum-exp
+                     ;; TODO: Handle tangle outputs that are nested
+                     ;; into directories, and when each tangle output
+                     ;; is nested into a different directory.
+                     (let* ((input-org-file (ennum-input (ennum-intern (ennum-post-filename post))))
+                            (post-file-copy
+                             (expand-file-name
+                              (file-name-nondirectory input-org-file)
+                              (ennum-setting :static-directory))))
+                       (ennum-copy input-org-file post-file-copy)
+                       (org-babel-tangle-file post-file-copy)
+                       (delete-file post-file-copy)))))
+                 (seq-mapcat 'ennum-publish-link (ennum-post-links post))))
+              posts))
 
 (defun ennum-publish-generic (other-files-directory file)
-  (ennum-make-operation
-   :inputs (list (ennum--expand-relative file other-files-directory))
-   :outputs
-   (list (pcase (file-name-extension file)
-           ("org" (ennum--org-output-filename file))
-           (_ file)))
-   :publish (lambda (output-file)
-              (pcase (file-name-extension file)
-                ("org" (ennum-with-file-contents file
-                         (org-export-to-file 'html output-file)))
-                (_ (ennum-copy (ennum--expand-relative file other-files-directory)
-                               output-file))))))
+  (ennum-exp
+   (let ((interned-file (ennum-input (ennum-intern (ennum--expand-relative file other-files-directory))))
+         (output-file
+          (pcase (file-name-extension file)
+            ("org" (ennum--org-output-filename file))
+            (_ file))))
+     (pcase (file-name-extension interned-file)
+       ("org"
+        (when (file-name-directory output-file)
+          (ennum-mkdir-p (file-name-directory output-file)))
+        (ennum-with-file-contents interned-file
+          (org-export-to-file 'html output-file)))
+       (_ (ennum-copy interned-file output-file))))))
 
 (defun ennum-video-poster (video)
   (or (seq-find (lambda (file)
@@ -211,51 +315,55 @@ non-nil, include directories in the output."
   (let* ((tongue (ennum-post-language (first posts)))
          (number-of-pages (ceiling (length posts) posts-per-page))
          (page-numbers (number-sequence 1 number-of-pages)))
-    (ennum-make-operation
-     :inputs (seq-map 'ennum-post-filename posts)
-     :outputs (cons (ennum-add-tongue-suffix (format "%s.html" filename-prefix) tongue)
-                    (seq-map (apply-partially 'ennum-index-filename filename-prefix tongue "html")
-                             page-numbers))
-     :publish
-     (lambda (home-page &rest output-files)
-       (let ((system-time-locale (map-elt (ennum-setting :locale-alist) tongue nil 'string=)))
-         (seq-mapn
-          (lambda (posts page-number output-file)
-            (with-temp-buffer
-              (insert (format "#+TITLE: %s\n" title))
-              (insert (format "#+LANGUAGE: %s\n" tongue))
-              (insert "#+OPTIONS: num:nil toc:nil\n\n")
-              (seq-do (lambda (post)
-                        (insert (format "* [[post:%s][%s]]\n" (ennum-post-slug post)))
-                        (insert (format-time-string "/%b %e, %Y/\n\n" (ennum-post-date post)))
-                        (when-let ((thumbnail (ennum-post-thumbnail post)))
-                          (insert (format "[[thumbnail:%s]]\n\n" thumbnail)))
-                        (when-let ((summary (ennum-post-summary post)))
-                          (insert summary)
-                          (insert "\n\n"))
-                        (when-let ((tags (ennum-post-tags post)))
-                          (insert "Tags: ")
-                          (insert
-                           (string-join
-                            (seq-map (lambda (tag)
-                                       (format "[[tag:%s][%s]]" (ennum-add-tongue-suffix tag tongue) tag))
-                                     tags)
-                            ", "))
-                          (insert "\n\n")))
-                      posts)
-              (unless (= page-number 1)
-                (insert (format "[[./%s][Newer posts]]\n\n"
-                                (ennum-index-filename (file-name-nondirectory filename-prefix)
-                                                     tongue nil (1- page-number)))))
-              (unless (= page-number number-of-pages)
-                (insert (format "[[./%s][Older posts]]\n"
-                                (ennum-index-filename (file-name-nondirectory filename-prefix)
-                                                     tongue nil (1+ page-number)))))
-              (org-export-to-file 'html output-file)))
-          (seq-partition posts posts-per-page)
-          page-numbers
-          output-files))
-       (copy-file (first output-files) home-page)))))
+    (ennum-exp
+     (ennum-input (seq-map (lambda (post)
+                             (ennum-intern (ennum-post-filename post)))
+                           posts))
+     (let ((output-files (seq-map (apply-partially 'ennum-index-filename filename-prefix tongue "html")
+                                  page-numbers))
+           (system-time-locale (map-elt (ennum-setting :locale-alist) tongue nil 'string=)))
+       (seq-mapn
+        (lambda (posts page-number output-file)
+          (when (file-name-directory output-file)
+            (ennum-mkdir-p (file-name-directory output-file)))
+          (with-temp-buffer
+            (insert (format "#+TITLE: %s\n" title))
+            (insert (format "#+LANGUAGE: %s\n" tongue))
+            (insert "#+OPTIONS: num:nil toc:nil\n\n")
+            (seq-do (lambda (post)
+                      (insert (format "* [[post:%s][%s]]\n"
+                                      (ennum-post-slug post)
+                                      (ennum-post-title post)))
+                      (insert (format-time-string "/%b %e, %Y/\n\n" (ennum-post-date post)))
+                      (when-let ((thumbnail (ennum-post-thumbnail post)))
+                        (insert (format "[[thumbnail:%s]]\n\n" thumbnail)))
+                      (when-let ((summary (ennum-post-summary post)))
+                        (insert summary)
+                        (insert "\n\n"))
+                      (when-let ((tags (ennum-post-tags post)))
+                        (insert "Tags: ")
+                        (insert
+                         (string-join
+                          (seq-map (lambda (tag)
+                                     (format "[[tag:%s][%s]]" (ennum-add-tongue-suffix tag tongue) tag))
+                                   tags)
+                          ", "))
+                        (insert "\n\n")))
+                    posts)
+            (unless (= page-number 1)
+              (insert (format "[[./%s][Newer posts]]\n\n"
+                              (ennum-index-filename (file-name-nondirectory filename-prefix)
+                                                    tongue nil (1- page-number)))))
+            (unless (= page-number number-of-pages)
+              (insert (format "[[./%s][Older posts]]\n"
+                              (ennum-index-filename (file-name-nondirectory filename-prefix)
+                                                    tongue nil (1+ page-number)))))
+            (org-export-to-file 'html output-file)))
+        (seq-partition posts posts-per-page)
+        page-numbers
+        output-files)
+       (copy-file (first output-files)
+                  (ennum-add-tongue-suffix (format "%s.html" filename-prefix) tongue))))))
 
 (defun ennum--absolute-uri (path)
   (format "%s://%s/%s"
@@ -267,26 +375,30 @@ non-nil, include directories in the output."
   (format-time-string "%Y-%m-%dT%H:%M:%SZ" date))
 
 (defun ennum-publish-feed (feed-file title rights posts)
-  (ennum-make-operation
-   :inputs (seq-map 'ennum-post-filename posts)
-   :outputs (list feed-file)
-   :publish
-   (lambda (output-file)
-     (with-temp-file output-file
-       (insert
-        (xmlgen
-         `(feed :xmlns "http://www.w3.org/2005/Atom"
-                (id ,(ennum--absolute-uri ""))
-                (title ,title)
-                (updated ,(ennum--atom-date (ennum-post-date (first posts))))
-                (link :rel "self" :href ,(ennum--absolute-uri feed-file))
-                (generator
-                 ,(format "Emacs %d.%d Org-mode %s ennum %s"
-                          emacs-major-version emacs-minor-version (org-version) ennum-version))
-                (rights ,rights)
-                ,@(seq-map 'ennum--feed-entry posts))))))))
-
-(defun ennum--feed-entry (post)
+  (ennum-exp
+   ;; TODO: Create ennu-mkdir-p-for-file
+   (when (file-name-directory feed-file)
+     (ennum-mkdir-p (file-name-directory feed-file)))
+   (with-temp-file feed-file
+     (insert
+      (xmlgen
+       `(feed :xmlns "http://www.w3.org/2005/Atom"
+              (id ,(ennum--absolute-uri ""))
+              (title ,title)
+              (updated ,(ennum--atom-date (ennum-post-date (first posts))))
+              (link :rel "self" :href ,(ennum--absolute-uri feed-file))
+              (generator
+               ,(format "Emacs %d.%d Org-mode %s ennum %s"
+                        emacs-major-version emacs-minor-version (org-version) ennum-version))
+              (rights ,rights)
+              ,@(seq-mapn (lambda (post interned-post-file)
+                            (ennum--feed-entry post interned-post-file))
+                          posts
+                          (ennum-input (seq-map (lambda (post)
+                                                  (ennum-intern (ennum-post-filename post)))
+                                                posts)))))))))
+
+(defun ennum--feed-entry (post interned-post-file)
   (let ((link (ennum--absolute-uri (ennum--org-output-filename
                                    (ennum-post-filename post)))))
     `(entry (id ,link)
@@ -316,7 +428,7 @@ non-nil, include directories in the output."
          :blog-scheme :default-image-width
          :image-link-width :index-posts-per-page
          :locale-alist :other-files-directory
-         :tag-directory :thumbnail-image-width)
+         :store :tag-directory :thumbnail-image-width)
      (plist-get (org-combine-plists
                  (list :atom-feed-number-of-posts 12
                        :atom-feed-file "blog.atom"
@@ -325,6 +437,7 @@ non-nil, include directories in the output."
                        :image-link-width 1024
                        :index-posts-per-page 12
                        :locale-alist '(("en" . "C"))
+                       :store ".ennum"
                        :tag-directory "tag"
                        :thumbnail-image-width 320)
                  ennum-blog)
@@ -339,23 +452,22 @@ non-nil, include directories in the output."
 (defun ennum--expand-relative (name directory)
   (concat (file-name-as-directory directory) name))
 
-(defun ennum-publish-image (widths image)
-  (ennum-make-operation
-   :inputs (list image)
-   :outputs (seq-map (apply-partially 'ennum-image-output-filename image)
-                     widths)
-   :publish
-   (lambda (&rest output-files)
-     (seq-mapn (lambda (output-file width)
-                 (ennum-image-optimize-image
-                  (ennum-image-resize-image image output-file width)))
-               output-files widths))))
+(defun ennum-publish-image (image width)
+  (ennum-exp
+   (let ((input-image (ennum-input (ennum-intern image))))
+     (ennum-mkdir-p (ennum-setting :images-directory))
+     (ennum-image-optimize-image
+      (ennum-image-resize-image
+       input-image
+       (ennum--expand-relative
+        (ennum-image-output-filename
+         (file-name-nondirectory input-image) width)
+        (ennum-setting :images-directory))
+       width)))))
 
 (defun ennum-publish-copy (file)
-  (ennum-make-operation
-   :inputs (list file)
-   :outputs (list file)
-   :publish (apply-partially 'ennum-copy file)))
+  (ennum-exp
+   (ennum-copy (ennum-input (ennum-intern file)) file)))
 
 (defun newest-file (files)
   (pcase files
@@ -380,55 +492,25 @@ if DESTINATION already exists."
 (defun ennum--filter-map (function sequence)
   (seq-filter 'identity (seq-map function sequence)))
 
-;; TODO: What if a file was removed from the inputs? Detect that
-;; change as well.
-
-;; Two separate problems
-;; - tracking of list of inputs
-;; - depending on a function of inputs, or equivalently intermediate files
-
-;; Solve both problems with an "ennum store"
-(defun ennum--do-operation (temporary-directory operation)
-  ;; TODO: Check all outputs were created correctly.
-  (let* ((expand (lambda (directory file)
-                   (expand-file-name file directory)))
-         (inputs (ennum-operation-inputs operation))
-         (outputs (ennum-operation-outputs operation))
-         (absolute-outputs
-          (seq-map (apply-partially expand temporary-directory)
-                   outputs))
-         (previous-outputs
-          (seq-map (apply-partially expand (ennum-setting :output-directory))
-                   outputs)))
-    (cond
-     ((and (seq-every-p 'file-exists-p previous-outputs)
-           (file-newer-than-file-p (newest-file previous-outputs)
-                                   (newest-file inputs)))
-      (message "Skipping publishing %s to %s" inputs outputs)
-      (seq-mapn 'ennum-copy previous-outputs absolute-outputs))
-     (t (message "Publishing %s to %s" inputs outputs)
-        (seq-do 'ennum-mkdir-p
-                (seq-uniq
-                 (seq-map 'file-name-directory absolute-outputs)))
-        (apply (ennum-operation-publish operation) absolute-outputs)))))
-
-
 (defun ennum-publish-link (link)
   (pcase link
     (`("image" . ,path)
-     (list
-      (ennum-publish-image
-       (list (ennum-setting :default-image-width)
-             (ennum-setting :image-link-width))
-       (ennum--expand-relative path (ennum-setting :images-directory)))))
+     (seq-map (lambda (width)
+                (ennum-publish-image
+                 (ennum--expand-relative path (ennum-setting :images-directory))
+                 width))
+              (list (ennum-setting :default-image-width)
+                    (ennum-setting :image-link-width))))
     (`("static" . ,path)
      (list
-      (ennum-publish-copy (ennum--expand-relative path (ennum-setting :static-directory)))))
+      (ennum-publish-copy
+       (ennum--expand-relative path (ennum-setting :static-directory)))))
     (`("video" . ,path)
-     (list
-      (ennum-publish-copy (ennum--expand-relative path (ennum-setting :video-directory)))
-      (ennum-publish-copy (ennum--expand-relative (ennum-video-poster path)
-                                                (ennum-setting :images-directory)))))))
+     (seq-map 'ennum-publish-copy
+              (list
+               (ennum--expand-relative path (ennum-setting :video-directory))
+               (ennum--expand-relative (ennum-video-poster path)
+                                       (ennum-setting :images-directory)))))))
 
 (defmacro ennum-with-current-directory (directory &rest body)
   "Change to DIRECTORY, evaluate BODY and restore the current
@@ -469,53 +551,53 @@ as keys. Keys are compared using `equal'."
 
 (defun ennum-publish ()
   (interactive)
-  (let ((make-backup-files nil)
-        (blog-title (ennum-setting :blog-title))
-        (posts-per-page (ennum-setting :index-posts-per-page)))
-    (ennum-with-current-directory (ennum-setting :working-directory)
-      (ennum-with-temporary-directory temporary-directory
-        (seq-do
-         (apply-partially 'ennum--do-operation temporary-directory)
-         (append
-          (let ((posts (ennum-posts (ennum-setting :posts-directory))))
-            (append
-             ;; Publish posts
-             (seq-mapcat (pcase-lambda (`(,translation-group . ,posts))
-                           (ennum-publish-post posts))
-                         (seq-group-by 'ennum-post-translation-group posts))
-             ;; Publish feed
-             (list (ennum-publish-feed (ennum-setting :atom-feed-file)
-                                      blog-title
-                                      (ennum-setting :blog-license)
-                                      (seq-take posts (ennum-setting :atom-feed-number-of-posts))))
-             ;; Publish indices
-             (seq-map
-              (pcase-lambda (`(,tongue . ,posts))
-                (ennum-publish-index "index" blog-title posts-per-page posts))
-              (seq-group-by 'ennum-post-language posts))
-             (seq-mapcat
-              (pcase-lambda (`(,tag . ,posts))
-                (seq-map
-                 (pcase-lambda (`(,tongue . ,posts))
-                   (ennum-publish-index
-                    (ennum--expand-relative tag (ennum-setting :tag-directory))
-                    tag posts-per-page posts))
-                 (seq-group-by 'ennum-post-language posts)))
-              (ennum-many-to-many-group-by 'ennum-post-tags posts))
-             ;; Publish thumbnails
-             (seq-map
-              (apply-partially 'ennum-publish-image (list (ennum-setting :thumbnail-image-width)))
+  (ennum-with-current-directory (ennum-setting :working-directory)
+    (let* ((blog-title (ennum-setting :blog-title))
+           (posts (ennum-posts (ennum-setting :posts-directory)))
+           (posts-per-page (ennum-setting :index-posts-per-page))
+           (other-files-directory (ennum-setting :other-files-directory))
+           (result
+            (ennum-store-item-union
+             (append
+              ;; Publish posts
+              (seq-mapcat (pcase-lambda (`(,translation-group . ,posts))
+                            (ennum-publish-post posts))
+                          (seq-group-by 'ennum-post-translation-group posts))
+              ;; Publish feed
+              (list (ennum-publish-feed (ennum-setting :atom-feed-file)
+                                        blog-title
+                                        (ennum-setting :blog-license)
+                                        (seq-take posts (ennum-setting :atom-feed-number-of-posts))))
+              ;; Publish indices
+              (seq-map
+               (pcase-lambda (`(,tongue . ,posts))
+                 (ennum-publish-index "index" blog-title posts-per-page posts))
+               (seq-group-by 'ennum-post-language posts))
+              ;; Publish tag indices
+              (seq-mapcat
+               (pcase-lambda (`(,tag . ,posts))
+                 (seq-map
+                  (pcase-lambda (`(,tongue . ,posts))
+                    (ennum-publish-index
+                     (ennum--expand-relative tag (ennum-setting :tag-directory))
+                     tag posts-per-page posts))
+                  (seq-group-by 'ennum-post-language posts)))
+               (ennum-many-to-many-group-by 'ennum-post-tags posts))
+              ;; Publish thumbnails
               (seq-map (lambda (image)
-                         (ennum--expand-relative image (ennum-setting :images-directory)))
-                       (seq-uniq (ennum--filter-map 'ennum-post-thumbnail posts))))))
-          ;; Publish other files
-          (when-let ((other-files-directory (ennum-setting :other-files-directory)))
-            (seq-map (apply-partially 'ennum-publish-generic other-files-directory)
-                     (ennum-directory-files other-files-directory)))))
-        ;; Replace old output directory
-        (let ((output (ennum-setting :output-directory)))
-          (delete-directory output t)
-          (rename-file temporary-directory output t))))))
+                         (ennum-publish-image
+                          (ennum--expand-relative image (ennum-setting :images-directory))
+                          (ennum-setting :thumbnail-image-width)))
+                       (ennum--filter-map 'ennum-post-thumbnail posts))
+              ;; Publish other files
+              (seq-map (apply-partially 'ennum-publish-generic other-files-directory)
+                       (ennum-directory-files other-files-directory))))))
+      ;; Replace old output directory
+      (when (file-exists-p (ennum-setting :output-directory))
+        (ennum--set-file-modes-recursively (ennum-setting :output-directory) #o755 #o644 #o755)
+        (delete-directory (ennum-setting :output-directory) t))
+      (copy-directory result (ennum-setting :output-directory))
+      (message "Ennum published to %s" (expand-file-name (ennum-setting :output-directory))))))
 
 ;;; Server
 ;;;