mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 22:25:01 +00:00
wip: assets publish
This commit is contained in:
153
deps/publish/src/logseq/publish/worker.cljs
vendored
153
deps/publish/src/logseq/publish/worker.cljs
vendored
@@ -33,7 +33,7 @@
|
||||
[]
|
||||
#js {"access-control-allow-origin" "*"
|
||||
"access-control-allow-methods" "GET,POST,OPTIONS"
|
||||
"access-control-allow-headers" "content-type,authorization,x-publish-meta,if-none-match"
|
||||
"access-control-allow-headers" "content-type,authorization,x-publish-meta,x-asset-meta,if-none-match"
|
||||
"access-control-expose-headers" "etag"})
|
||||
|
||||
(defn merge-headers [base extra]
|
||||
@@ -61,6 +61,24 @@
|
||||
(defn not-found []
|
||||
(json-response {:error "not found"} 404))
|
||||
|
||||
(defn asset-content-type [ext]
|
||||
(case (string/lower-case (or ext ""))
|
||||
("png") "image/png"
|
||||
("jpg" "jpeg") "image/jpeg"
|
||||
("gif") "image/gif"
|
||||
("webp") "image/webp"
|
||||
("svg") "image/svg+xml"
|
||||
("bmp") "image/bmp"
|
||||
("avif") "image/avif"
|
||||
("mp4") "video/mp4"
|
||||
("webm") "video/webm"
|
||||
("mov") "video/quicktime"
|
||||
("mp3") "audio/mpeg"
|
||||
("wav") "audio/wav"
|
||||
("ogg") "audio/ogg"
|
||||
("pdf") "application/pdf"
|
||||
"application/octet-stream"))
|
||||
|
||||
(defn normalize-meta [meta]
|
||||
(when meta
|
||||
(if (map? meta)
|
||||
@@ -569,9 +587,42 @@
|
||||
(:block/name block)
|
||||
""))
|
||||
|
||||
(defn- asset-url [block ctx]
|
||||
(let [asset-type (:logseq.property.asset/type block)
|
||||
asset-uuid (:block/uuid block)
|
||||
external-url (:logseq.property.asset/external-url block)
|
||||
graph-uuid (:graph-uuid ctx)]
|
||||
(cond
|
||||
(string? external-url) external-url
|
||||
(and asset-uuid asset-type graph-uuid)
|
||||
(str "/asset/" graph-uuid "/" asset-uuid "." asset-type)
|
||||
:else nil)))
|
||||
|
||||
(defn- asset-node [block ctx]
|
||||
(let [asset-type (:logseq.property.asset/type block)
|
||||
asset-url (asset-url block ctx)
|
||||
title (or (:block/title block) (str asset-type))
|
||||
ext (string/lower-case (or asset-type ""))]
|
||||
(when asset-url
|
||||
(cond
|
||||
(contains? #{"png" "jpg" "jpeg" "gif" "webp" "svg" "bmp" "avif"} ext)
|
||||
[:img.asset-image {:src asset-url :alt title}]
|
||||
|
||||
(contains? #{"mp4" "webm" "mov"} ext)
|
||||
[:video.asset-video {:src asset-url :controls true}]
|
||||
|
||||
(contains? #{"mp3" "wav" "ogg"} ext)
|
||||
[:audio.asset-audio {:src asset-url :controls true}]
|
||||
|
||||
:else
|
||||
[:a.asset-link {:href asset-url :target "_blank"} title]))))
|
||||
|
||||
(defn block-display-node [block ctx]
|
||||
(let [display-type (:logseq.property.node/display-type block)]
|
||||
(let [display-type (:logseq.property.node/display-type block)
|
||||
asset-node (when (:logseq.property.asset/type block)
|
||||
(asset-node block ctx))]
|
||||
(case display-type
|
||||
:asset asset-node
|
||||
:code
|
||||
(let [lang (:logseq.property.code/lang block)
|
||||
attrs (cond-> {:class "code-block"}
|
||||
@@ -584,7 +635,8 @@
|
||||
:quote
|
||||
[:blockquote.quote-block (block-content-nodes block ctx)]
|
||||
|
||||
(block-content-nodes block ctx))))
|
||||
(or asset-node
|
||||
(block-content-nodes block ctx)))))
|
||||
|
||||
(defn block-content-from-ref [ref ctx]
|
||||
(let [raw (or (get ref "source_block_content") "")
|
||||
@@ -696,6 +748,9 @@
|
||||
entities)]
|
||||
(vec (distinct (concat page-entries block-entries)))))
|
||||
|
||||
(def ^:private void-tags
|
||||
#{"area" "base" "br" "col" "embed" "hr" "img" "input" "link" "meta" "param" "source" "track" "wbr"})
|
||||
|
||||
(defn render-hiccup [node]
|
||||
(cond
|
||||
(nil? node) ""
|
||||
@@ -720,11 +775,13 @@
|
||||
(map (fn [[k v]]
|
||||
(str " " (name k) "=\"" (escape-html (str v)) "\""))
|
||||
attrs)))]
|
||||
(str "<" tag (or attrs-str "") ">"
|
||||
(if (#{"style" "script"} tag)
|
||||
(apply str (map #(if (string? %) % (render-hiccup %)) children))
|
||||
(apply str (map render-hiccup children)))
|
||||
"</" tag ">"))
|
||||
(if (contains? void-tags tag)
|
||||
(str "<" tag (or attrs-str "") ">")
|
||||
(str "<" tag (or attrs-str "") ">"
|
||||
(if (#{"style" "script"} tag)
|
||||
(apply str (map #(if (string? %) % (render-hiccup %)) children))
|
||||
(apply str (map render-hiccup children)))
|
||||
"</" tag ">")))
|
||||
(seq? node) (apply str (map render-hiccup node))
|
||||
:else (escape-html (str node))))
|
||||
|
||||
@@ -933,6 +990,10 @@
|
||||
".code-block .cm-gutters{background:#1f2933;color:#8a7a63;border-right:1px solid rgba(199,179,143,0.25);}"
|
||||
".math-block{flex:1;padding:10px 12px;border-radius:10px;background:#f1e8d9;font-family:\"Times New Roman\",serif;}"
|
||||
".quote-block{flex:1;margin:0;padding:8px 12px;border-left:4px solid #c7b38f;background:#fff8ec;border-radius:8px;}"
|
||||
".asset-image{max-width:100%;border-radius:10px;border:1px solid #e6dccb;}"
|
||||
".asset-video,.asset-audio{width:100%;}"
|
||||
".asset-link{color:#1a5fb4;text-decoration:none;}"
|
||||
".asset-link:hover{text-decoration:underline;}"
|
||||
".page-properties{margin:0 0 24px;padding:12px 16px;border:1px solid #e6dccb;border-radius:12px;background:#fffdf8;}"
|
||||
".properties{margin:0;display:grid;grid-template-columns:140px 1fr;gap:6px 16px;}"
|
||||
".property{display:contents;}"
|
||||
@@ -1486,6 +1547,53 @@
|
||||
#js {"content-type" "text/html; charset=utf-8"}
|
||||
(cors-headers))}))))))
|
||||
|
||||
(defn parse-asset-meta-header [request]
|
||||
(let [meta-header (.get (.-headers request) "x-asset-meta")]
|
||||
(when meta-header
|
||||
(try
|
||||
(normalize-meta (js/JSON.parse meta-header))
|
||||
(catch :default _
|
||||
nil)))))
|
||||
|
||||
(defn handle-post-asset [request env]
|
||||
(js-await [auth-header (.get (.-headers request) "authorization")
|
||||
token (when (and auth-header (string/starts-with? auth-header "Bearer "))
|
||||
(subs auth-header 7))
|
||||
dev-skip? (= "true" (aget env "DEV_SKIP_AUTH"))
|
||||
claims (cond
|
||||
dev-skip? #js {:sub "dev"}
|
||||
(nil? token) nil
|
||||
:else (verify-jwt token env))]
|
||||
(let [claims (if dev-skip? #js {:sub "dev"} claims)]
|
||||
(if (and (not dev-skip?) (nil? claims))
|
||||
(unauthorized)
|
||||
(let [meta (parse-asset-meta-header request)
|
||||
graph-uuid (get meta :graph)
|
||||
asset-uuid (get meta :asset_uuid)
|
||||
asset-type (get meta :asset_type)
|
||||
checksum (get meta :checksum)]
|
||||
(if (or (nil? meta) (string/blank? graph-uuid) (string/blank? asset-uuid) (string/blank? asset-type))
|
||||
(bad-request "missing asset metadata")
|
||||
(js-await [body (.arrayBuffer request)
|
||||
r2 (aget env "PUBLISH_R2")
|
||||
r2-key (str "publish/assets/" graph-uuid "/" asset-uuid "." asset-type)
|
||||
^js existing (.head r2 r2-key)
|
||||
existing-checksum (when existing
|
||||
(when-let [meta (.-customMetadata existing)]
|
||||
(aget meta "checksum")))
|
||||
content-type (or (get meta :content_type)
|
||||
(asset-content-type asset-type))
|
||||
put? (not (and existing-checksum checksum (= existing-checksum checksum)))]
|
||||
(when put?
|
||||
(.put r2 r2-key body
|
||||
#js {:httpMetadata #js {:contentType content-type}
|
||||
:customMetadata #js {:checksum (or checksum "")
|
||||
:owner_sub (aget claims "sub")}}))
|
||||
(json-response {:asset_uuid asset-uuid
|
||||
:graph_uuid graph-uuid
|
||||
:asset_type asset-type
|
||||
:asset_url (str "/asset/" graph-uuid "/" asset-uuid "." asset-type)}))))))))
|
||||
|
||||
(defn handle-tag-page-html [graph-uuid tag-uuid env]
|
||||
(if (or (nil? graph-uuid) (nil? tag-uuid))
|
||||
(bad-request "missing graph uuid or tag uuid")
|
||||
@@ -1636,6 +1744,9 @@
|
||||
(and (string/starts-with? path "/page/") (= method "GET"))
|
||||
(handle-page-html request env)
|
||||
|
||||
(and (= path "/assets") (= method "POST"))
|
||||
(handle-post-asset request env)
|
||||
|
||||
(and (= path "/pages") (= method "POST"))
|
||||
(handle-post-pages request env)
|
||||
|
||||
@@ -1667,6 +1778,32 @@
|
||||
(handle-ref-name-json ref-name env)
|
||||
(handle-ref-name-html ref-name env)))
|
||||
|
||||
(and (string/starts-with? path "/asset/") (= method "GET"))
|
||||
(let [parts (string/split path #"/")
|
||||
graph-uuid (nth parts 2 nil)
|
||||
file-name (nth parts 3 nil)]
|
||||
(if (or (string/blank? graph-uuid) (string/blank? file-name))
|
||||
(bad-request "missing asset id")
|
||||
(let [ext-idx (string/last-index-of file-name ".")
|
||||
asset-uuid (when (and ext-idx (pos? ext-idx))
|
||||
(subs file-name 0 ext-idx))
|
||||
asset-type (when (and ext-idx (pos? ext-idx))
|
||||
(subs file-name (inc ext-idx)))]
|
||||
(if (or (string/blank? asset-uuid) (string/blank? asset-type))
|
||||
(bad-request "invalid asset id")
|
||||
(js-await [r2 (aget env "PUBLISH_R2")
|
||||
r2-key (str "publish/assets/" graph-uuid "/" asset-uuid "." asset-type)
|
||||
^js object (.get r2 r2-key)]
|
||||
(if-not object
|
||||
(not-found)
|
||||
(let [headers (merge-headers
|
||||
#js {"content-type" (or (some-> object .-httpMetadata .-contentType)
|
||||
(asset-content-type asset-type))
|
||||
"cache-control" "public, max-age=31536000, immutable"}
|
||||
(cors-headers))]
|
||||
(js/Response. (.-body object)
|
||||
#js {:headers headers}))))))))
|
||||
|
||||
(and (string/starts-with? path "/s/") (= method "GET"))
|
||||
(let [parts (string/split path #"/")
|
||||
short-id (nth parts 2 nil)]
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
(ns frontend.handler.publish
|
||||
"Prepare publish payloads for pages."
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[frontend.config :as config]
|
||||
[frontend.db :as db]
|
||||
[frontend.fs :as fs]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.common.path :as path]
|
||||
[logseq.db :as ldb]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -24,6 +27,97 @@
|
||||
[]
|
||||
(str config/PUBLISH-API-BASE "/pages"))
|
||||
|
||||
(defn- asset-upload-endpoint
|
||||
[]
|
||||
(str config/PUBLISH-API-BASE "/assets"))
|
||||
|
||||
(defn- asset-content-type
|
||||
[ext]
|
||||
(case (string/lower-case (or ext ""))
|
||||
("png") "image/png"
|
||||
("jpg" "jpeg") "image/jpeg"
|
||||
("gif") "image/gif"
|
||||
("webp") "image/webp"
|
||||
("svg") "image/svg+xml"
|
||||
("bmp") "image/bmp"
|
||||
("avif") "image/avif"
|
||||
("mp4") "video/mp4"
|
||||
("webm") "video/webm"
|
||||
("mov") "video/quicktime"
|
||||
("mp3") "audio/mpeg"
|
||||
("wav") "audio/wav"
|
||||
("ogg") "audio/ogg"
|
||||
("pdf") "application/pdf"
|
||||
"application/octet-stream"))
|
||||
|
||||
(defn- merge-attr
|
||||
[entity attr value]
|
||||
(let [existing (get entity attr ::none)]
|
||||
(cond
|
||||
(= existing ::none) (assoc entity attr value)
|
||||
(vector? existing) (assoc entity attr (conj existing value))
|
||||
(set? existing) (assoc entity attr (conj existing value))
|
||||
:else (assoc entity attr [existing value]))))
|
||||
|
||||
(defn- datoms->entities
|
||||
[datoms]
|
||||
(reduce
|
||||
(fn [acc datom]
|
||||
(let [[e a v _tx added?] datom]
|
||||
(if added?
|
||||
(update acc e (fn [entity]
|
||||
(merge-attr (or entity {:db/id e}) a v)))
|
||||
acc)))
|
||||
{}
|
||||
datoms))
|
||||
|
||||
(defn- asset-entities-from-payload
|
||||
[payload]
|
||||
(let [entities (datoms->entities (:datoms payload))]
|
||||
(->> entities
|
||||
vals
|
||||
(filter (fn [entity]
|
||||
(and (:logseq.property.asset/type entity)
|
||||
(:block/uuid entity)))))))
|
||||
|
||||
(defn- <upload-asset!
|
||||
[repo graph-uuid asset]
|
||||
(let [asset-type (:logseq.property.asset/type asset)
|
||||
asset-uuid (some-> (:block/uuid asset) str)
|
||||
external-url (:logseq.property.asset/external-url asset)
|
||||
token (state/get-auth-id-token)]
|
||||
(if (or (not (string? asset-type)) (string/blank? asset-type) external-url (nil? asset-uuid))
|
||||
(p/resolved nil)
|
||||
(p/let [repo-dir (config/get-repo-dir repo)
|
||||
asset-path (path/path-join "assets" (str asset-uuid "." asset-type))
|
||||
content (fs/read-file-raw repo-dir asset-path {})
|
||||
meta {:graph graph-uuid
|
||||
:asset_uuid asset-uuid
|
||||
:asset_type asset-type
|
||||
:checksum (:logseq.property.asset/checksum asset)
|
||||
:size (:logseq.property.asset/size asset)
|
||||
:title (:block/title asset)}
|
||||
headers (cond-> {"content-type" (asset-content-type asset-type)
|
||||
"x-asset-meta" (js/JSON.stringify (clj->js meta))}
|
||||
token (assoc "authorization" (str "Bearer " token)))
|
||||
resp (js/fetch (asset-upload-endpoint)
|
||||
(clj->js {:method "POST"
|
||||
:headers headers
|
||||
:body content}))]
|
||||
(when-not (.-ok resp)
|
||||
(js/console.warn "Asset publish failed" {:asset asset-uuid :status (.-status resp)}))
|
||||
resp))))
|
||||
|
||||
(defn- <upload-assets!
|
||||
[repo graph-uuid payload]
|
||||
(let [assets (asset-entities-from-payload payload)]
|
||||
(when (seq assets)
|
||||
(p/all (map (fn [asset]
|
||||
(p/catch (<upload-asset! repo graph-uuid asset)
|
||||
(fn [error]
|
||||
(js/console.warn "Asset publish error" error))))
|
||||
assets)))))
|
||||
|
||||
(defn- <post-publish!
|
||||
[payload]
|
||||
(let [token (state/get-auth-id-token)
|
||||
@@ -69,11 +163,11 @@
|
||||
(:db/id page)
|
||||
graph-uuid)]
|
||||
(if payload
|
||||
(-> (<post-publish! payload)
|
||||
(-> (p/let [_ (<upload-assets! repo graph-uuid payload)]
|
||||
(<post-publish! payload))
|
||||
(p/then (fn [resp]
|
||||
(p/let [json (.json resp)
|
||||
data (bean/->clj json)]
|
||||
(js/console.dir data)
|
||||
(let [short-url (:short_url data)
|
||||
graph-uuid (or (:graph-uuid payload)
|
||||
(some-> (ldb/get-graph-rtc-uuid db*) str))
|
||||
|
||||
Reference in New Issue
Block a user