mirror of
https://github.com/penpot/penpot.git
synced 2025-06-04 12:01:38 +02:00
♻️ Refactor thumbnail generation.
This commit is contained in:
parent
60ec32f7cc
commit
3f554df687
5 changed files with 194 additions and 142 deletions
|
@ -34,6 +34,8 @@
|
||||||
:media-uri "http://localhost:3449/media/"
|
:media-uri "http://localhost:3449/media/"
|
||||||
:assets-uri "http://localhost:3449/static/"
|
:assets-uri "http://localhost:3449/static/"
|
||||||
|
|
||||||
|
:image-process-max-threads 2
|
||||||
|
|
||||||
:sendmail-backend "console"
|
:sendmail-backend "console"
|
||||||
:sendmail-reply-to "no-reply@example.com"
|
:sendmail-reply-to "no-reply@example.com"
|
||||||
:sendmail-from "no-reply@example.com"
|
:sendmail-from "no-reply@example.com"
|
||||||
|
@ -71,6 +73,7 @@
|
||||||
(s/def ::debug-humanize-transit ::us/boolean)
|
(s/def ::debug-humanize-transit ::us/boolean)
|
||||||
(s/def ::public-uri ::us/string)
|
(s/def ::public-uri ::us/string)
|
||||||
(s/def ::backend-uri ::us/string)
|
(s/def ::backend-uri ::us/string)
|
||||||
|
(s/def ::image-process-max-threads ::us/integer)
|
||||||
|
|
||||||
(s/def ::google-client-id ::us/string)
|
(s/def ::google-client-id ::us/string)
|
||||||
(s/def ::google-client-secret ::us/string)
|
(s/def ::google-client-secret ::us/string)
|
||||||
|
@ -101,7 +104,8 @@
|
||||||
::smtp-ssl
|
::smtp-ssl
|
||||||
::debug-humanize-transit
|
::debug-humanize-transit
|
||||||
::allow-demo-users
|
::allow-demo-users
|
||||||
::registration-enabled]))
|
::registration-enabled
|
||||||
|
::image-process-max-threads]))
|
||||||
|
|
||||||
(defn env->config
|
(defn env->config
|
||||||
[env]
|
[env]
|
||||||
|
|
|
@ -10,108 +10,155 @@
|
||||||
(ns uxbox.images
|
(ns uxbox.images
|
||||||
"Image postprocessing."
|
"Image postprocessing."
|
||||||
(:require
|
(:require
|
||||||
|
[clojure.core.async :as a]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.core :as fs]
|
[datoteka.core :as fs]
|
||||||
[uxbox.common.exceptions :as ex]
|
[mount.core :refer [defstate]]
|
||||||
|
[uxbox.config :as cfg]
|
||||||
[uxbox.common.data :as d]
|
[uxbox.common.data :as d]
|
||||||
|
[uxbox.common.exceptions :as ex]
|
||||||
[uxbox.common.spec :as us]
|
[uxbox.common.spec :as us]
|
||||||
[uxbox.util.storage :as ust]
|
[uxbox.media :as media]
|
||||||
[uxbox.media :as media])
|
[uxbox.util.storage :as ust])
|
||||||
(:import
|
(:import
|
||||||
java.io.ByteArrayInputStream
|
java.io.ByteArrayInputStream
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
|
java.util.concurrent.Semaphore
|
||||||
org.im4java.core.ConvertCmd
|
org.im4java.core.ConvertCmd
|
||||||
org.im4java.core.Info
|
org.im4java.core.Info
|
||||||
org.im4java.core.IMOperation))
|
org.im4java.core.IMOperation))
|
||||||
|
|
||||||
;; --- Helpers
|
(defstate semaphore
|
||||||
|
:start (Semaphore. (:image-process-max-threads cfg/config 1)))
|
||||||
(defn format->extension
|
|
||||||
[format]
|
|
||||||
(case format
|
|
||||||
"jpeg" ".jpg"
|
|
||||||
"webp" ".webp"))
|
|
||||||
|
|
||||||
(defn format->mtype
|
|
||||||
[format]
|
|
||||||
(case format
|
|
||||||
"jpeg" "image/jpeg"
|
|
||||||
"webp" "image/webp"))
|
|
||||||
|
|
||||||
;; --- Thumbnails Generation
|
;; --- Thumbnails Generation
|
||||||
|
|
||||||
|
(s/def ::cmd keyword?)
|
||||||
|
|
||||||
|
(s/def ::path (s/or :path fs/path?
|
||||||
|
:string string?
|
||||||
|
:file fs/file?))
|
||||||
|
(s/def ::mtype string?)
|
||||||
|
|
||||||
|
(s/def ::input
|
||||||
|
(s/keys :req-un [::path]
|
||||||
|
:opt-un [::mtype]))
|
||||||
|
|
||||||
(s/def ::width integer?)
|
(s/def ::width integer?)
|
||||||
(s/def ::height integer?)
|
(s/def ::height integer?)
|
||||||
|
(s/def ::format #{:jpeg :webp :png})
|
||||||
(s/def ::quality #(< 0 % 101))
|
(s/def ::quality #(< 0 % 101))
|
||||||
(s/def ::format #{"jpeg" "webp"})
|
|
||||||
(s/def ::thumbnail-opts
|
(s/def ::thumbnail-params
|
||||||
(s/keys :opt-un [::format ::quality ::width ::height]))
|
(s/keys :req-un [::cmd ::input ::format ::width ::height]))
|
||||||
|
|
||||||
;; Related info on how thumbnails generation
|
;; Related info on how thumbnails generation
|
||||||
;; http://www.imagemagick.org/Usage/thumbnails/
|
;; http://www.imagemagick.org/Usage/thumbnails/
|
||||||
|
|
||||||
(defn generate-thumbnail
|
(defn format->extension
|
||||||
([input] (generate-thumbnail input nil))
|
[format]
|
||||||
([input {:keys [quality format width height]
|
(case format
|
||||||
:or {format "jpeg"
|
:png ".png"
|
||||||
quality 92
|
:jpeg ".jpg"
|
||||||
width 200
|
:webp ".webp"))
|
||||||
height 200}
|
|
||||||
:as opts}]
|
|
||||||
(us/assert ::thumbnail-opts opts)
|
|
||||||
(us/assert fs/path? input)
|
|
||||||
(let [ext (format->extension format)
|
|
||||||
tmp (fs/create-tempfile :suffix ext)
|
|
||||||
opr (doto (IMOperation.)
|
|
||||||
(.addImage)
|
|
||||||
(.autoOrient)
|
|
||||||
(.strip)
|
|
||||||
(.thumbnail (int width) (int height) ">")
|
|
||||||
(.quality (double quality))
|
|
||||||
(.addImage))]
|
|
||||||
(doto (ConvertCmd.)
|
|
||||||
(.run opr (into-array (map str [input tmp]))))
|
|
||||||
(let [thumbnail-data (fs/slurp-bytes tmp)]
|
|
||||||
(fs/delete tmp)
|
|
||||||
(ByteArrayInputStream. thumbnail-data)))))
|
|
||||||
|
|
||||||
(defn generate-profile-thumbnail
|
(defn format->mtype
|
||||||
([input] (generate-thumbnail input nil))
|
[format]
|
||||||
([input {:keys [quality format width height]
|
(case format
|
||||||
:or {format "jpeg"
|
:png "image/png"
|
||||||
quality 92
|
:jpeg "image/jpeg"
|
||||||
width 200
|
:webp "image/webp"))
|
||||||
height 200}
|
|
||||||
:as opts}]
|
|
||||||
(us/assert ::thumbnail-opts opts)
|
|
||||||
(us/assert fs/path? input)
|
|
||||||
(let [ext (format->extension format)
|
|
||||||
tmp (fs/create-tempfile :suffix ext)
|
|
||||||
opr (doto (IMOperation.)
|
|
||||||
(.addImage)
|
|
||||||
(.autoOrient)
|
|
||||||
(.strip)
|
|
||||||
(.thumbnail (int width) (int height) "^")
|
|
||||||
(.gravity "center")
|
|
||||||
(.extent (int width) (int height))
|
|
||||||
(.quality (double quality))
|
|
||||||
(.addImage))]
|
|
||||||
(doto (ConvertCmd.)
|
|
||||||
(.run opr (into-array (map str [input tmp]))))
|
|
||||||
(let [thumbnail-data (fs/slurp-bytes tmp)]
|
|
||||||
(fs/delete tmp)
|
|
||||||
(ByteArrayInputStream. thumbnail-data)))))
|
|
||||||
|
|
||||||
(defn info
|
(defn mtype->format
|
||||||
[content-type path]
|
[mtype]
|
||||||
(let [instance (Info. (str path))]
|
(case mtype
|
||||||
(when-not (= content-type (.getProperty instance "Mime type"))
|
"image/jpeg" :jpeg
|
||||||
|
"image/webp" :webp
|
||||||
|
"image/png" :png
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn- generic-process
|
||||||
|
[{:keys [input format quality operation] :as params}]
|
||||||
|
(let [{:keys [path mtype]} input
|
||||||
|
format (or (mtype->format mtype) format)
|
||||||
|
ext (format->extension format)
|
||||||
|
tmp (fs/create-tempfile :suffix ext)]
|
||||||
|
|
||||||
|
(doto (ConvertCmd.)
|
||||||
|
(.run operation (into-array (map str [path tmp]))))
|
||||||
|
|
||||||
|
(let [thumbnail-data (fs/slurp-bytes tmp)]
|
||||||
|
(fs/delete tmp)
|
||||||
|
(assoc params
|
||||||
|
:format format
|
||||||
|
:mtype (format->mtype format)
|
||||||
|
:data (ByteArrayInputStream. thumbnail-data)))))
|
||||||
|
|
||||||
|
(defmulti process :cmd)
|
||||||
|
|
||||||
|
(defmethod process :generic-thumbnail
|
||||||
|
[{:keys [quality width height] :as params}]
|
||||||
|
(us/assert ::thumbnail-params params)
|
||||||
|
(let [op (doto (IMOperation.)
|
||||||
|
(.addImage)
|
||||||
|
(.autoOrient)
|
||||||
|
(.strip)
|
||||||
|
(.thumbnail (int width) (int height) ">")
|
||||||
|
(.quality (double quality))
|
||||||
|
(.addImage))]
|
||||||
|
(generic-process (assoc params :operation op))))
|
||||||
|
|
||||||
|
(defmethod process :profile-thumbnail
|
||||||
|
[{:keys [quality width height] :as params}]
|
||||||
|
(us/assert ::thumbnail-params params)
|
||||||
|
(let [op (doto (IMOperation.)
|
||||||
|
(.addImage)
|
||||||
|
(.autoOrient)
|
||||||
|
(.strip)
|
||||||
|
(.thumbnail (int width) (int height) "^")
|
||||||
|
(.gravity "center")
|
||||||
|
(.extent (int width) (int height))
|
||||||
|
(.quality (double quality))
|
||||||
|
(.addImage))]
|
||||||
|
(generic-process (assoc params :operation op))))
|
||||||
|
|
||||||
|
(defmethod process :info
|
||||||
|
[{:keys [input] :as params}]
|
||||||
|
(us/assert ::input input)
|
||||||
|
(let [{:keys [path mtype]} input
|
||||||
|
instance (Info. (str path))
|
||||||
|
mtype' (.getProperty instance "Mime type")]
|
||||||
|
|
||||||
|
(when (and (string? mtype)
|
||||||
|
(not= mtype mtype'))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :image-type-mismatch
|
:code :image-type-mismatch
|
||||||
:hint "Seems like you are uploading a file whose content does not match the extension."))
|
:hint "Seems like you are uploading a file whose content does not match the extension."))
|
||||||
{:width (.getImageWidth instance)
|
{:width (.getImageWidth instance)
|
||||||
:height (.getImageHeight instance)}))
|
:height (.getImageHeight instance)
|
||||||
|
:mtype mtype'}))
|
||||||
|
|
||||||
|
(defmethod process :default
|
||||||
|
[{:keys [cmd] :as params}]
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :not-implemented
|
||||||
|
:hint (str "No impl found for process cmd:" cmd)))
|
||||||
|
|
||||||
|
(defn run
|
||||||
|
[params]
|
||||||
|
(try
|
||||||
|
(.acquire semaphore)
|
||||||
|
(let [res (a/<!! (a/thread
|
||||||
|
(try
|
||||||
|
(process params)
|
||||||
|
(catch Throwable e
|
||||||
|
e))))]
|
||||||
|
(if (instance? Throwable res)
|
||||||
|
(throw res)
|
||||||
|
res))
|
||||||
|
(finally
|
||||||
|
(.release semaphore))))
|
||||||
|
|
||||||
(defn resolve-urls
|
(defn resolve-urls
|
||||||
[row src dst]
|
[row src dst]
|
||||||
|
|
|
@ -119,12 +119,6 @@
|
||||||
|
|
||||||
(mark-file-deleted conn params)))
|
(mark-file-deleted conn params)))
|
||||||
|
|
||||||
(def ^:private sql:mark-file-deleted
|
|
||||||
"update file
|
|
||||||
set deleted_at = clock_timestamp()
|
|
||||||
where id = ?
|
|
||||||
and deleted_at is null")
|
|
||||||
|
|
||||||
(defn mark-file-deleted
|
(defn mark-file-deleted
|
||||||
[conn {:keys [id] :as params}]
|
[conn {:keys [id] :as params}]
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
|
@ -150,14 +144,6 @@
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(create-file-image conn params)))
|
(create-file-image conn params)))
|
||||||
|
|
||||||
(def ^:private sql:insert-file-image
|
|
||||||
"insert into file_image
|
|
||||||
(file_id, name, path, width, height, mtype,
|
|
||||||
thumb_path, thumb_width, thumb_height,
|
|
||||||
thumb_quality, thumb_mtype)
|
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
||||||
returning *")
|
|
||||||
|
|
||||||
(defn- create-file-image
|
(defn- create-file-image
|
||||||
[conn {:keys [content file-id name] :as params}]
|
[conn {:keys [content file-id name] :as params}]
|
||||||
(when-not (imgs/valid-image-types? (:content-type content))
|
(when-not (imgs/valid-image-types? (:content-type content))
|
||||||
|
@ -165,22 +151,26 @@
|
||||||
:code :image-type-not-allowed
|
:code :image-type-not-allowed
|
||||||
:hint "Seems like you are uploading an invalid image."))
|
:hint "Seems like you are uploading an invalid image."))
|
||||||
|
|
||||||
(let [image-opts (images/info (:content-type content) (:tempfile content))
|
(let [info (images/run {:cmd :info :input {:path (:tempfile content)
|
||||||
image-path (imgs/persist-image-on-fs content)
|
:mtype (:content-type content)}})
|
||||||
thumb-opts imgs/thumbnail-options
|
path (imgs/persist-image-on-fs content)
|
||||||
thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path)]
|
opts (assoc imgs/thumbnail-options
|
||||||
|
:input {:mtype (:mtype info)
|
||||||
|
:path path})
|
||||||
|
thumb (imgs/persist-image-thumbnail-on-fs opts)]
|
||||||
|
|
||||||
(-> (db/insert! conn :file-image
|
(-> (db/insert! conn :file-image
|
||||||
{:file-id file-id
|
{:file-id file-id
|
||||||
:name name
|
:name name
|
||||||
:path (str image-path)
|
:path (str path)
|
||||||
:width (:width image-opts)
|
:width (:width info)
|
||||||
:height (:height image-opts)
|
:height (:height info)
|
||||||
:mtype (:content-type content)
|
:mtype (:mtype info)
|
||||||
:thumb-path (str thumb-path)
|
:thumb-path (str (:path thumb))
|
||||||
:thumb-width (:width thumb-opts)
|
:thumb-width (:width thumb)
|
||||||
:thumb-height (:height thumb-opts)
|
:thumb-height (:height thumb)
|
||||||
:thumb-quality (:quality thumb-opts)
|
:thumb-quality (:quality thumb)
|
||||||
:thumb-mtype (images/format->mtype (:format thumb-opts))})
|
:thumb-mtype (:mtype thumb)})
|
||||||
(images/resolve-urls :path :uri)
|
(images/resolve-urls :path :uri)
|
||||||
(images/resolve-urls :thumb-path :thumb-uri))))
|
(images/resolve-urls :thumb-path :thumb-uri))))
|
||||||
|
|
||||||
|
@ -193,9 +183,6 @@
|
||||||
(s/def ::import-image-to-file
|
(s/def ::import-image-to-file
|
||||||
(s/keys :req-un [::image-id ::file-id ::profile-id]))
|
(s/keys :req-un [::image-id ::file-id ::profile-id]))
|
||||||
|
|
||||||
(def ^:private sql:select-image-by-id
|
|
||||||
"select img.* from image as img where id=$1")
|
|
||||||
|
|
||||||
(sm/defmutation ::import-image-to-file
|
(sm/defmutation ::import-image-to-file
|
||||||
[{:keys [image-id file-id profile-id] :as params}]
|
[{:keys [image-id file-id profile-id] :as params}]
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
{:width 800
|
{:width 800
|
||||||
:height 800
|
:height 800
|
||||||
:quality 85
|
:quality 85
|
||||||
:format "jpeg"})
|
:format :jpeg})
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::id ::us/uuid)
|
||||||
(s/def ::name ::us/string)
|
(s/def ::name ::us/string)
|
||||||
|
@ -146,23 +146,27 @@
|
||||||
:code :image-type-not-allowed
|
:code :image-type-not-allowed
|
||||||
:hint "Seems like you are uploading an invalid image."))
|
:hint "Seems like you are uploading an invalid image."))
|
||||||
|
|
||||||
(let [image-opts (images/info (:content-type content) (:tempfile content))
|
(let [info (images/run {:cmd :info :input {:path (:tempfile content)
|
||||||
image-path (persist-image-on-fs content)
|
:mtype (:content-type content)}})
|
||||||
thumb-opts thumbnail-options
|
path (persist-image-on-fs content)
|
||||||
thumb-path (persist-image-thumbnail-on-fs thumb-opts image-path)]
|
opts (assoc thumbnail-options
|
||||||
|
:input {:mtype (:mtype info)
|
||||||
|
:path path})
|
||||||
|
thumb (persist-image-thumbnail-on-fs opts)]
|
||||||
|
|
||||||
(-> (db/insert! conn :image
|
(-> (db/insert! conn :image
|
||||||
{:id (or id (uuid/next))
|
{:id (or id (uuid/next))
|
||||||
:library-id library-id
|
:library-id library-id
|
||||||
:name name
|
:name name
|
||||||
:path (str image-path)
|
:path (str path)
|
||||||
:width (:width image-opts)
|
:width (:width info)
|
||||||
:height (:height image-opts)
|
:height (:height info)
|
||||||
:mtype (:content-type content)
|
:mtype (:mtype info)
|
||||||
:thumb-path (str thumb-path)
|
:thumb-path (str (:path thumb))
|
||||||
:thumb-width (:width thumb-opts)
|
:thumb-width (:width thumb)
|
||||||
:thumb-height (:height thumb-opts)
|
:thumb-height (:height thumb)
|
||||||
:thumb-quality (:quality thumb-opts)
|
:thumb-quality (:quality thumb)
|
||||||
:thumb-mtype (images/format->mtype (:format thumb-opts))})
|
:thumb-mtype (:mtype thumb)})
|
||||||
(images/resolve-urls :path :uri)
|
(images/resolve-urls :path :uri)
|
||||||
(images/resolve-urls :thumb-path :thumb-uri))))
|
(images/resolve-urls :thumb-path :thumb-uri))))
|
||||||
|
|
||||||
|
@ -172,14 +176,21 @@
|
||||||
(ust/save! media/media-storage filename tempfile)))
|
(ust/save! media/media-storage filename tempfile)))
|
||||||
|
|
||||||
(defn persist-image-thumbnail-on-fs
|
(defn persist-image-thumbnail-on-fs
|
||||||
[thumb-opts input-path]
|
[{:keys [input] :as params}]
|
||||||
(let [input-path (ust/lookup media/media-storage input-path)
|
(let [path (ust/lookup media/media-storage (:path input))
|
||||||
thumb-data (images/generate-thumbnail input-path thumb-opts)
|
thumb (images/run
|
||||||
[filename _] (fs/split-ext (fs/name input-path))
|
(-> params
|
||||||
thumb-name (->> (images/format->extension (:format thumb-opts))
|
(assoc :cmd :generic-thumbnail)
|
||||||
(str "thumbnail-" filename))]
|
(update :input assoc :path path)))
|
||||||
(ust/save! media/media-storage thumb-name thumb-data)))
|
|
||||||
|
|
||||||
|
name (str "thumbnail-"
|
||||||
|
(first (fs/split-ext (fs/name (:path input))))
|
||||||
|
(images/format->extension (:format thumb)))
|
||||||
|
path (ust/save! media/media-storage name (:data thumb))]
|
||||||
|
|
||||||
|
(-> thumb
|
||||||
|
(dissoc :data :input)
|
||||||
|
(assoc :path path))))
|
||||||
|
|
||||||
;; --- Mutation: Rename Image
|
;; --- Mutation: Rename Image
|
||||||
|
|
||||||
|
|
|
@ -272,8 +272,15 @@
|
||||||
|
|
||||||
(sm/defmutation ::update-profile-photo
|
(sm/defmutation ::update-profile-photo
|
||||||
[{:keys [profile-id file] :as params}]
|
[{:keys [profile-id file] :as params}]
|
||||||
|
(when-not (imgs/valid-image-types? (:content-type file))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :image-type-not-allowed
|
||||||
|
:hint "Seems like you are uploading an invalid image."))
|
||||||
|
|
||||||
(db/with-atomic [conn db/pool]
|
(db/with-atomic [conn db/pool]
|
||||||
(let [profile (profile/retrieve-profile conn profile-id)
|
(let [profile (profile/retrieve-profile conn profile-id)
|
||||||
|
_ (images/run {:cmd :info :input {:path (:tempfile file)
|
||||||
|
:mtype (:content-type file)}})
|
||||||
photo (upload-photo conn params)]
|
photo (upload-photo conn params)]
|
||||||
|
|
||||||
;; Schedule deletion of old photo
|
;; Schedule deletion of old photo
|
||||||
|
@ -286,21 +293,18 @@
|
||||||
|
|
||||||
(defn- upload-photo
|
(defn- upload-photo
|
||||||
[conn {:keys [file profile-id]}]
|
[conn {:keys [file profile-id]}]
|
||||||
(when-not (imgs/valid-image-types? (:content-type file))
|
(let [prefix (-> (sodi.prng/random-bytes 8)
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :image-type-not-allowed
|
|
||||||
:hint "Seems like you are uploading an invalid image."))
|
|
||||||
(let [image-opts (images/info (:content-type file) (:tempfile file))
|
|
||||||
thumb-opts {:width 256
|
|
||||||
:height 256
|
|
||||||
:quality 75
|
|
||||||
:format "jpeg"}
|
|
||||||
prefix (-> (sodi.prng/random-bytes 8)
|
|
||||||
(sodi.util/bytes->b64s))
|
(sodi.util/bytes->b64s))
|
||||||
name (str prefix ".jpg")
|
thumb (images/run
|
||||||
path (fs/path (:tempfile file))
|
{:cmd :profile-thumbnail
|
||||||
photo (images/generate-profile-thumbnail path thumb-opts)]
|
:format :jpeg
|
||||||
(ust/save! media/media-storage name photo)))
|
:quality 85
|
||||||
|
:width 256
|
||||||
|
:height 256
|
||||||
|
:input {:path (fs/path (:tempfile file))
|
||||||
|
:mtype (:content-type file)}})
|
||||||
|
name (str prefix (images/format->extension (:format thumb)))]
|
||||||
|
(ust/save! media/media-storage name (:data thumb))))
|
||||||
|
|
||||||
(defn- update-profile-photo
|
(defn- update-profile-photo
|
||||||
[conn profile-id path]
|
[conn profile-id path]
|
||||||
|
@ -309,7 +313,6 @@
|
||||||
{:id profile-id})
|
{:id profile-id})
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Request Email Change
|
;; --- Mutation: Request Email Change
|
||||||
|
|
||||||
(declare select-profile-for-update)
|
(declare select-profile-for-update)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue