diff --git a/src/electron/electron/core.cljs b/src/electron/electron/core.cljs index 043d1b7eef..75070ec981 100644 --- a/src/electron/electron/core.cljs +++ b/src/electron/electron/core.cljs @@ -216,7 +216,7 @@ :accelerator false}]) [{:label "Always on Top" :type "checkbox" - :click (fn [menuItem browserWindow] + :click (fn [^js menuItem ^js browserWindow] ;; switch alwaysOnTop state (.setAlwaysOnTop browserWindow (.-checked menuItem)))}])}) ;; Windows has no about role diff --git a/src/main/frontend/handler/assets.cljs b/src/main/frontend/handler/assets.cljs index a7cb3fdd5d..3e59450d53 100644 --- a/src/main/frontend/handler/assets.cljs +++ b/src/main/frontend/handler/assets.cljs @@ -157,9 +157,37 @@ (-> (if (string? file) file (.arrayBuffer file)) (p/then db-asset/array + [payload] + (let [keys (->> (js/Object.keys payload) + (js->clj) + (filter #(re-matches #"\d+" %)) + (sort-by #(js/parseInt % 10)))] + (when (seq keys) + (clj->js (map #(aget payload %) keys))))) + +(defn- indexed-map->array + [payload] + (let [keys (->> (keys payload) + (filter #(re-matches #"\d+" (str %))) + (sort-by #(js/parseInt (str %) 10)))] + (when (seq keys) + (clj->js (map #(get payload %) keys))))) + (defn ->uint8 [payload] (cond + (and (exists? js/Blob) + (instance? js/Blob payload)) + payload + (instance? js/Uint8Array payload) payload @@ -176,10 +204,32 @@ (sequential? payload) (js/Uint8Array. (clj->js payload)) + (and (= "Buffer" (field-value payload "type")) + (some? (field-value payload "data"))) + (->uint8 (field-value payload "data")) + + (map? payload) + (if-let [data (indexed-map->array payload)] + (js/Uint8Array. data) + (throw (ex-info "unsupported binary payload" + {:payload-type (str (type payload)) + :keys (mapv str (keys payload))}))) + (and (object? payload) - (= "Buffer" (aget payload "type")) - (array? (aget payload "data"))) - (js/Uint8Array. (aget payload "data")) + (number? (aget payload "length"))) + (js/Uint8Array. payload) + + (object? payload) + (if-let [data (indexed-object->array payload)] + (js/Uint8Array. data) + (throw (ex-info "unsupported binary payload" + {:payload-type (str (type payload)) + :object-tag (try + (.call (.-toString (.-prototype js/Object)) payload) + (catch :default _ nil)) + :keys (try + (js->clj (js/Object.keys payload)) + (catch :default _ nil))}))) :else (throw (ex-info "unsupported binary payload" @@ -187,13 +237,17 @@ (defn repo + (string/replace #"^/+" "") + (str "_" (quot (util/time-ms) 1000)) + (str "." (string/lower-case (name extension))))) + +(defn- normalize-zip-entry + [[filename data]] + (try + [filename (assets-handler/->uint8 data)] + (catch :default e + (throw (ex-info "unsupported zip entry payload" + (assoc (or (ex-data e) {}) + :filename filename) + e))))) + +(defn- (p/let [db-data (persist-db/ (p/let [db-data ( repo - (string/replace #"^/+" "") - (str "_" (quot (util/time-ms) 1000)) - (str "." (string/lower-case (name extension))))) - (defn export-repo-as-debug-transit! [repo] (p/let [result (export-common-handler/ + (p/let [{:keys [status body]} + (fetch-fn {:method "POST" + :url (import-db-binary-url base-url repo) + :headers (binary-headers auth-token) + :body data}) + parsed (parse-response-body body)] + (if (<= 200 status 299) + (let [result (ldb/read-transit-str (:resultTransit parsed))] + (when on-invoke-success + (on-invoke-success method args result)) + result) + (let [error (:error parsed)] + (throw (ex-info (or (:message error) "db-worker invoke failed") + (cond-> {:status status + :code (normalize-code (:code error))} + error (assoc :error error))))))) + (p/catch (fn [error] + (when on-invoke-failure + (on-invoke-failure method args error)) + (throw error)))))) + (defn connect-events! [{:keys [base-url auth-token event-handler open-sse-fn schedule-fn reconnect-delay-ms on-event-error]} wrapped-worker] (let [connected? (atom true) @@ -191,7 +227,7 @@ ( - (invoke! client "thread-api/import-db-binary" [repo data]) + (import-db-binary! client repo data) (p/catch (fn [error] (log/error :import-db-error repo error "SQLiteDB import error") (notification/show! (t :storage/sqlitedb-import-error error) :error) {}))))) diff --git a/src/main/frontend/worker/db_worker_node.cljs b/src/main/frontend/worker/db_worker_node.cljs index f91d29312a..81478e962f 100644 --- a/src/main/frontend/worker/db_worker_node.cljs +++ b/src/main/frontend/worker/db_worker_node.cljs @@ -37,17 +37,22 @@ (.writeHead res status #js {"Content-Type" "text/plain"}) (.end res text)) -(defn- (p/do! (.remoteInvokeBinary proxy method-str repo payload)) + (p/finally (fn [] + (js/clearTimeout timeout-id)))))) + (defn- (p/let [binary ( (p/let [body (vec [^js payload] @@ -26,3 +29,56 @@ (is (instance? js/Uint8Array output)) (is (= [10 11 12] (uint8->vec output))))) +(deftest coerce-buffer-like-map-to-uint8-test + (let [buffer-like {"type" "Buffer" + "data" [13 14 15]} + output (#'assets/->uint8 buffer-like)] + (is (instance? js/Uint8Array output)) + (is (= [13 14 15] (uint8->vec output))))) + +(deftest coerce-buffer-like-object-with-seq-data-to-uint8-test + (let [buffer-like #js {:type "Buffer" + :data [16 17 18]} + output (#'assets/->uint8 buffer-like)] + (is (instance? js/Uint8Array output)) + (is (= [16 17 18] (uint8->vec output))))) + +(deftest coerce-indexed-byte-object-to-uint8-test + (let [buffer-like #js {"0" 19 + "1" 20 + "2" 21} + output (#'assets/->uint8 buffer-like)] + (is (instance? js/Uint8Array output)) + (is (= [19 20 21] (uint8->vec output))))) + +(deftest coerce-indexed-byte-map-to-uint8-test + (let [buffer-like {"0" 22 + "1" 23 + "2" 24} + output (#'assets/->uint8 buffer-like)] + (is (instance? js/Uint8Array output)) + (is (= [22 23 24] (uint8->vec output))))) + +(deftest get-all-assets-does-not-readdir-missing-assets-dir + (async done + (let [readdir-calls (atom 0) + original-assets-root config/get-current-repo-assets-root + original-stat fs/stat + original-readdir fs/readdir] + (set! config/get-current-repo-assets-root (constantly "/tmp/graph/assets")) + (set! fs/stat (fn [path] + (is (= "/tmp/graph/assets" path)) + (p/rejected (js/Error. "ENOENT")))) + (set! fs/readdir (fn [& _args] + (swap! readdir-calls inc) + (p/rejected (js/Error. "readdir should not be called")))) + (-> (p/let [result (assets/ (export/db-based-export-repo-as-zip! "logseq_db_big_graph") + (p/then (fn [_] + (let [expected-path "/tmp/logseq/graphs/logseq_db_big_graph/export/big_graph_123.zip"] + (is (= ["/tmp/logseq/graphs/logseq_db_big_graph/export"] + @mkdir-calls)) + (is (= expected-path (ffirst @writes))) + (is (instance? js/ArrayBuffer (second (first @writes)))) + (is (= [[(str "ZIP exported to " expected-path ".") :success false]] + @notification-calls))))) + (p/catch (fn [e] + (is false (str "unexpected error: " e)))) + (p/finally + (fn [] + (set! util/electron? original-electron?) + (set! util/time-ms original-time-ms) + (set! config/get-repo-dir original-get-repo-dir) + (set! fs/mkdir-if-not-exists original-mkdir-if-not-exists) + (set! (.-apis js/window) original-apis) + (set! persist-db/InRemote client nil nil) - payload (.from js/Buffer "sqlite-bytes")] - (-> (p/with-redefs [remote/invoke! (fn [client' method args] - (swap! calls conj [client' method args]) - (p/resolved nil))] - (p/let [_ (protocol/ (p/let [_ (protocol/InRemote client nil nil) + payload (js/ArrayBuffer. 3) + view (js/Uint8Array. payload)] + (aset view 0 1) + (aset view 1 2) + (aset view 2 3) + (-> (p/let [_ (protocol/ (p/let [{:keys [host port stop!]} + (start-daemon! {:root-dir data-dir + :repo repo-a}) + _ (reset! daemon-a {:stop! stop!}) + _ (invoke host port "thread-api/create-or-open-db" [repo-a {}]) + _ (invoke host port "thread-api/transact" + [repo-a + [{:block/uuid page-uuid + :block/title "Raw SQLite Import Page" + :block/name "raw-sqlite-import-page" + :block/tags #{:logseq.class/Page} + :block/created-at now + :block/updated-at now}] + {} + nil]) + export-binary (invoke host port "thread-api/export-db-binary" [repo-a])] + (is (instance? js/Uint8Array export-binary)) + (is (pos? (.-byteLength export-binary))) + (p/let [_ ((:stop! @daemon-a)) + {:keys [host port stop!]} + (start-daemon! {:root-dir data-dir + :repo repo-b}) + _ (reset! daemon-b {:stop! stop!}) + {:keys [status body]} (invoke-import-db-binary-raw host port repo-b export-binary) + parsed (js->clj (js/JSON.parse body) :keywordize-keys true) + _ (invoke host port "thread-api/create-or-open-db" [repo-b {}]) + result (invoke host port "thread-api/q" + [repo-b + ['[:find ?e + :in $ ?title + :where [?e :block/title ?title]] + "Raw SQLite Import Page"]])] + (is (= 200 status)) + (is (:ok parsed)) + (is (seq result)))) + (p/catch (fn [e] + (println "[db-worker-node-test] import-sqlite-raw error:" e) + (is false (str e)))) + (p/finally (fn [] + (let [stop-a (:stop! @daemon-a) + stop-b (:stop! @daemon-b)] + (cond + (and stop-a stop-b) + (-> (stop-a) + (p/finally (fn [] (-> (stop-b) (p/finally (fn [] (done))))))) + + stop-a + (-> (stop-a) (p/finally (fn [] (done)))) + + stop-b + (-> (stop-b) (p/finally (fn [] (done)))) + + :else + (done))))))))) + (deftest db-worker-node-export-client-ops-db-binary (async done (let [daemon (atom nil)