mirror of
https://github.com/penpot/penpot.git
synced 2025-01-10 17:00:36 -05:00
🎉 Add new storage implementation (builtin in backend).
This commit is contained in:
parent
a87d83e10e
commit
22e558478a
1 changed files with 181 additions and 0 deletions
181
backend/src/uxbox/util/storage.clj
Normal file
181
backend/src/uxbox/util/storage.clj
Normal file
|
@ -0,0 +1,181 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.util.storage
|
||||
"A local filesystem storage implementation."
|
||||
(:require
|
||||
[clojure.java.io :as io]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs]
|
||||
[datoteka.proto :as fp]
|
||||
[sodi.prng :as sodi.prng]
|
||||
[sodi.util :as sodi.util]
|
||||
[uxbox.common.exceptions :as ex])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.net.URI
|
||||
java.nio.file.Files
|
||||
java.nio.file.NoSuchFileException
|
||||
java.nio.file.Path
|
||||
java.security.MessageDigest))
|
||||
|
||||
(defn uri
|
||||
[v]
|
||||
(cond
|
||||
(instance? URI v) v
|
||||
(string? v) (URI. v)
|
||||
:else (throw (IllegalArgumentException. "unexpected input"))))
|
||||
|
||||
(defn- normalize-path
|
||||
[^Path base ^Path path]
|
||||
(when (fs/absolute? path)
|
||||
(ex/raise :type :filesystem-error
|
||||
:code :suspicious-operation
|
||||
:hint "Suspicios operation: absolute path not allowed."
|
||||
:contex {:path path :base base}))
|
||||
(let [^Path fullpath (.resolve base path)
|
||||
^Path fullpath (.normalize fullpath)]
|
||||
(when-not (.startsWith fullpath base)
|
||||
(ex/raise :type :filesystem-error
|
||||
:code :suspicious-operation
|
||||
:hint "Suspicios operation: go to parent dir is not allowed."
|
||||
:contex {:path path :base base}))
|
||||
fullpath))
|
||||
|
||||
(defn- transform-path
|
||||
[storage ^Path path]
|
||||
(if-let [xf (::xf storage)]
|
||||
((xf (fn [a b] b)) nil path)
|
||||
path))
|
||||
|
||||
(defn blob
|
||||
[v]
|
||||
(let [data (.getBytes v "UTF-8")]
|
||||
(ByteArrayInputStream. ^bytes data)))
|
||||
|
||||
(defn save!
|
||||
[storage path content]
|
||||
(s/assert ::storage storage)
|
||||
(let [^Path base (::base-path storage)
|
||||
^Path path (->> (fs/path path)
|
||||
(transform-path storage))
|
||||
^Path fullpath (normalize-path base path)]
|
||||
(when-not (fs/exists? (.getParent fullpath))
|
||||
(fs/create-dir (.getParent fullpath)))
|
||||
(loop [iteration nil]
|
||||
(let [[basepath ext] (fs/split-ext fullpath)
|
||||
candidate (fs/path (str basepath iteration ext))]
|
||||
(if (fs/exists? candidate)
|
||||
(recur (if (nil? iteration) 1 (inc iteration)))
|
||||
(with-open [^InputStream src (io/input-stream content)
|
||||
^OutputStream dst (io/output-stream candidate)]
|
||||
(io/copy src dst)
|
||||
(fs/relativize candidate base)))))))
|
||||
|
||||
(defn delete!
|
||||
[storage path]
|
||||
(s/assert ::storage storage)
|
||||
(try
|
||||
(->> (fs/path path)
|
||||
(normalize-path (::base-path storage))
|
||||
(fs/delete))
|
||||
true
|
||||
(catch java.nio.file.NoSuchFileException e
|
||||
false)))
|
||||
|
||||
(defn clear!
|
||||
[storage]
|
||||
(s/assert ::storage storage)
|
||||
(fs/delete (::base-path storage))
|
||||
(fs/create-dir (::base-path storage))
|
||||
nil)
|
||||
|
||||
(defn exists?
|
||||
[storage path]
|
||||
(s/assert ::storage storage)
|
||||
(->> (fs/path path)
|
||||
(normalize-path (::base-path storage))
|
||||
(fs/exists?)))
|
||||
|
||||
(defn lookup
|
||||
[storage path]
|
||||
(s/assert ::storage storage)
|
||||
(->> (fs/path path)
|
||||
(normalize-path (::base-path storage))))
|
||||
|
||||
(defn public-uri
|
||||
[storage path]
|
||||
(s/assert ::storage storage)
|
||||
(let [^URI base (::base-uri storage)
|
||||
^String path (str path)]
|
||||
(.resolve base path)))
|
||||
|
||||
(s/def ::base-path (s/or :path fs/path? :str string?))
|
||||
(s/def ::base-uri (s/or :uri #(instance? URI %) :str string?))
|
||||
(s/def ::xf fn?)
|
||||
|
||||
(s/def ::storage
|
||||
(s/keys :req [::base-path] :opt [::xf ::base-uri]))
|
||||
|
||||
(s/def ::create-options
|
||||
(s/keys :req-un [::base-path] :opt-un [::xf ::base-uri]))
|
||||
|
||||
(defn create
|
||||
"Create an instance of local FileSystem storage providing an
|
||||
absolute base path.
|
||||
|
||||
If that path does not exists it will be automatically created,
|
||||
if it exists but is not a directory, an exception will be
|
||||
raised.
|
||||
|
||||
This function expects a map with the following options:
|
||||
- `:base-path`: a fisical directory on your local machine
|
||||
- `:base-uri`: a base uri used for resolve the files
|
||||
"
|
||||
[{:keys [base-path base-uri xf] :as options}]
|
||||
(s/assert ::create-options options)
|
||||
(let [^Path base-path (fs/path base-path)]
|
||||
(when (and (fs/exists? base-path)
|
||||
(not (fs/directory? base-path)))
|
||||
(ex/raise :type :filesystem-error
|
||||
:code :file-already-exists
|
||||
:hint "File already exists, expects directory."))
|
||||
(when-not (fs/exists? base-path)
|
||||
(fs/create-dir base-path))
|
||||
(cond-> {::base-path base-path}
|
||||
base-uri (assoc ::base-uri (uri base-uri))
|
||||
xf (assoc ::xf xf))))
|
||||
|
||||
(defn- bytes->sha256
|
||||
[^bytes data]
|
||||
(let [^MessageDigest md (MessageDigest/getInstance "SHA-256")]
|
||||
(.update md data)
|
||||
(.digest md)))
|
||||
|
||||
(defn hash-path
|
||||
[^Path path]
|
||||
(let [name (str (.getFileName path))
|
||||
hash (-> (sodi.prng/random-nonce 64)
|
||||
(bytes->sha256)
|
||||
(sodi.util/bytes->b64s))
|
||||
tokens (re-seq #"[\w\d\-\_]{3}" hash)
|
||||
path-tokens (take 6 tokens)
|
||||
rest-tokens (drop 6 tokens)
|
||||
path (fs/path path-tokens)
|
||||
frest (apply str rest-tokens)]
|
||||
(fs/path (list path frest name))))
|
||||
|
||||
(defn slugify-filename
|
||||
[path]
|
||||
(let [parent (or (fs/parent path) "")
|
||||
[name ext] (fs/split-ext (fs/name path))]
|
||||
(fs/path parent (str (str/uslug name) ext))))
|
Loading…
Reference in a new issue