app.cljs (10438B)
1 (ns rmjxyz.app 2 (:require [cljs.nodejs :as nodejs] 3 ["express" :as express] 4 ["express-handlebars" :refer [engine]] 5 [goog.string :as gstring] 6 [goog.string.format] 7 [cljs.core.async :refer-macros [go]] 8 [cljs.pprint :refer [pprint]] 9 [cljs.core.async.interop :refer-macros [<p!]])) 10 11 (println "Initializing server...") 12 (defonce app (atom nil)) 13 (defonce index-windows (atom nil)) 14 (defonce fs (js/require "fs")) 15 (defonce path (js/require "path")) 16 (defonce process (js/require "process")) 17 (defonce permStrings ["---" "--x" "-w-" "-wx" "r--" "r-x" "rw-" "rwx"]) 18 (defonce mons [ "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" 19 "Aug" "Sep" "Oct" "Nov" "Dec" ]) 20 (defonce post-items (atom nil)) 21 (defonce post-windows (atom nil)) 22 (defonce index-windows (atom nil)) 23 (defonce index-items (atom nil)) 24 (defonce all-bkg-scripts (atom nil)) 25 (defonce item-update-time (* 1000 60 5)) 26 (defonce comments-list '("industrial_society" "sneed" "france" 27 "anime" "choppa" "gigachad" "peter" 28 "shrek" "tanku" "toem" "troll" "virusexe" 29 "windows")) 30 31 (defn mon-by-index 32 "Get a month (abbreviation) by its index. Returns nil if i is out of range." 33 [i] 34 (nth mons i nil)) 35 36 (defn permissions-to-string 37 "Convert Unix file permission mode to string. Return nil on invalid mode." 38 [mode] 39 (nth permStrings mode nil)) 40 41 (defn get-file-name 42 "Get just the name of a file (no directory, no extension)." 43 [file-path] 44 (.-name (. path parse file-path))) 45 46 (defn ls-time 47 "Convert time stamp in milliseconds to LS time format." 48 [timeMS] 49 (let [file-date (js/Date. timeMS)] 50 (str (mon-by-index (.getMonth file-date)) " " (gstring/format "%02d" (.getDate file-date)) " " 51 ;; If the file was updated this year then set the last column to the 52 ;; hour and minute. Else, the last column should be the year. 53 (if (= (.getFullYear file-date) (.getFullYear (js/Date.))) 54 (str (gstring/format "%02d" (.getHours file-date)) ":" (gstring/format "%02d" (.getMinutes file-date))) 55 (str (.getFullYear file-date)))))) 56 57 (defn get-basename 58 "Get the basename of a file." 59 [file-path] 60 (.-name (.parse path (.basename path file-path)))) 61 62 (defn create-lstat 63 "Create an LSStat object for use in rendering." 64 [file-path] 65 (let [stats (.statSync fs file-path) 66 unixFilePerms (when stats (.toString (bit-and (.-mode stats) (js/parseInt "777" 8)) 8))] 67 (if stats 68 {:perms (str (if (.isDirectory stats) "d" "-") 69 (permissions-to-string (js/parseInt (first unixFilePerms))) 70 (permissions-to-string (js/parseInt (second unixFilePerms))) 71 (permissions-to-string (js/parseInt (nth unixFilePerms 2)))) 72 :numLinks (.-nlink stats) 73 :fileSize (gstring/format "%4d" (.-size stats)) 74 :mtime (ls-time (.-mtimeMs stats)) 75 :basename (get-file-name (.basename path file-path)) 76 :realpath file-path 77 } 78 ;; TODO actually deal with error. 79 (js/console.error "Could not stat" file-path)))) 80 81 (defn ls-list 82 "Create a list of ls-stats from a list of file paths. Looks into public." 83 ([paths] 84 (for [file paths] 85 (create-lstat file))) 86 ([basedir ext paths] 87 (for [file paths] 88 (create-lstat (.join path basedir (str file ext)))))) 89 90 (defn ls-dir 91 "Create a list of ls-stats from a directory." 92 [dir-path ext] 93 (when (and (.existsSync fs dir-path) (.isDirectory (.lstatSync fs dir-path))) 94 (vec (for [file (.readdirSync fs dir-path) 95 :when (= (.extname path file) ext)] 96 (create-lstat (.join path dir-path file)))))) 97 98 (defn create-command 99 "Create a command object for rendering in the website." 100 ;; LS list. 101 ([dir ext paths display-path] {:args (if display-path display-path dir) 102 :lsList (ls-list dir ext paths)}) 103 ;; Cat. 104 ([the-path trim-path] {:args (if trim-path (get-file-name the-path) the-path) 105 :markup the-path})) 106 107 (defn create-ls 108 "Create a ls-listing from a pre-existing set of files." 109 [dir ls-list site-path] 110 {:args dir :lsList 111 (for [new-stat ls-list] 112 (assoc new-stat :basename (.join path site-path (get new-stat :basename))))}) 113 114 (defn create-windows 115 "Create the window data for the site." 116 [commands-list] 117 {:windows (for [cmds commands-list] 118 {:commands cmds})}) 119 120 (defn serve-404 121 "Serve the 404 page from path to res." 122 [file ^js res] (.. res (status 404) (render "404"))) 123 124 (defn serve-200 125 "Serve a page with result 200." 126 ([template ^js res] (.. res (status 200) (render template))) 127 ([template ^js res obj] 128 (.. res (status 200) (render template obj)))) 129 130 131 132 (defn index-information 133 "Make a JS object for use in index.handlebars." 134 [window-list] 135 (clj->js (merge window-list 136 {:bkgScript (.join path "/site-bkgs/bin/" (rand-nth @all-bkg-scripts)) 137 :comment (.join path "comments/" (rand-nth comments-list))}))) 138 139 (defn json-create-items 140 "Create an items list from a JSON file." 141 [json-path] 142 (let [^js obj (.parse js/JSON (.readFileSync fs json-path "utf8"))] 143 (vec 144 (flatten 145 (for [^js win (.-wins obj)] 146 (for [^js cmd (.-cmds win) 147 :when (.-what cmd)] 148 (for [^js path (.-what cmd)] 149 path))))))) 150 151 (defn json-create-windows 152 "Create a windows vector from JSON file." 153 [json-path] 154 (let [^js obj (.parse js/JSON (.readFileSync fs json-path "utf8"))] 155 (create-windows 156 (vec 157 (for [^js win (.-wins obj)] 158 (vec 159 (for [^js cmd (.-cmds win)] 160 (cond 161 (= (.-type cmd) "cat") (create-command (.-where cmd) 162 (when (.-trim cmd) true)) 163 (= (.-type cmd) "ls") (create-command (.dirname path json-path) 164 (if (.-ext cmd) (.-ext cmd) "") 165 (.-what cmd) 166 (when (.-where cmd) (.-where cmd))))))))))) 167 (defn get-mtime 168 "Get time (ms) when the file at file-path was updated." 169 [file-path] 170 (.-mtimeMs (.statSync fs file-path))) 171 172 (defn update-files! 173 "If a list of files has changed on disk and it's been five minutes then re-read them." 174 [file-list collection-path update-func window-list window-update-func] 175 (let [time-ms (.getTime (get @file-list :when))] 176 (when (and (>= (- time-ms (.getTime (js/Date.))) item-update-time) 177 (> (get-mtime collection-path) time-ms)) 178 (reset! file-list 179 {:content 180 ;; Update the entire list, new post added, post removed, name change, etc.. 181 (update-func collection-path) 182 :when (js/Date.)}) 183 (reset! window-list (window-update-func))))) 184 185 (defn update-post-items [dir-path] {:when (js/Date.) :content (ls-dir dir-path ".handlebars")}) 186 (defn update-index-items [json-path] {:when (js/Date.) :content (json-create-items json-path)}) 187 188 (defn init-server 189 "Set the server's routes." 190 [] 191 (println "Starting server...") 192 (let [server (express)] 193 ;; Server settings. 194 (.use server (.static express (.join path (.cwd process) "public"))) 195 (.use server (.static express (.join path (.cwd process) "external"))) 196 (.use server (.json express)) 197 (.engine server "handlebars" (engine (clj->js {:defaultLayout "main"}))) 198 (.set server "view engine" "handlebars") 199 (.set server "views" (.join path (.cwd process) "views")) 200 201 202 ;; Server paths. 203 (.get server "/posts/:post" (fn [^js req res next] 204 (let [post (.toLowerCase (.-post (.-params req)))] 205 (if (some #(= post (get % :basename)) (get @post-items :content)) 206 (serve-200 "index" res (index-information 207 (create-windows [[(create-command (.join path "content/posts" post) true)]]))) 208 (serve-404 post res))))) 209 210 (.get server "/posts" (fn [^js req res next] 211 (update-files! post-items "./views/partials/content/posts" update-post-items post-windows 212 (fn [] (create-windows [[(create-ls "posts" (get @post-items :content) "posts")]]))) 213 (serve-200 "index" res (index-information @post-windows)))) 214 (.get server "/:item" (fn [^js req res next] 215 (let [item (.toLowerCase (.-item (.-params req)))] 216 (if (some #(= item %) (for [file-path (get @index-items :content)] 217 (get-basename file-path))) 218 (serve-200 "index" res (index-information (create-windows [[(create-command (.join path "content/" item) true)]]))) 219 (serve-404 item res))))) 220 (.get server "/" (fn [^js req res next] 221 (update-files! index-items "./views/partials/content/index.json" update-index-items 222 index-windows (fn [] (json-create-windows "./views/partials/content/index.json"))) 223 (serve-200 "index" res (index-information @index-windows)))) 224 (.get server "*" (fn [^js req res next] (serve-404 "Sneed" res))) 225 (.listen server 3000 (fn [] (println "Starting server on port 3000"))))) 226 227 (defn start! 228 "Start the server." 229 [] 230 (reset! app (init-server)) 231 (reset! post-items (update-post-items "./views/partials/content/posts")) 232 (reset! post-windows (create-windows [[(create-ls "posts" (get @post-items :content) "posts")]])) 233 ;; TODO put these in a json object. 234 (reset! index-items (update-index-items "./views/partials/content/index.json")) 235 (reset! index-windows (json-create-windows "./views/partials/content/index.json")) 236 237 (reset! all-bkg-scripts (let [files (.readdirSync fs "./external/site-bkgs/bin/")] 238 (for [file files 239 :when (and (= (.extname path file) ".js") (not= file "backs.js"))] 240 file)))) 241 242 (defn main! 243 "Main function" 244 [] 245 (start!)) 246 247 (defn reload! 248 "Stop the server." 249 [] 250 (.close @app) 251 (reset! app nil) 252 (start!)) 253