diff --git a/src/main/frontend/components/imports.cljs b/src/main/frontend/components/imports.cljs index cc73cec822..f4329b9188 100644 --- a/src/main/frontend/components/imports.cljs +++ b/src/main/frontend/components/imports.cljs @@ -75,7 +75,7 @@ (js/setTimeout ui-handler/re-render-root! 500))) (defn- lsq-import-handler - [e & {:keys [sqlite? debug-transit? graph-name db-edn?]}] + [e & {:keys [sqlite? sqlite-zip? debug-transit? graph-name db-edn?]}] (let [file (first (array-seq (.-files (.-target e))))] (cond sqlite? @@ -100,6 +100,18 @@ (js/console.error e))) (.readAsArrayBuffer reader file)))) + sqlite-zip? + (let [graph-name (string/trim graph-name)] + (cond + (string/blank? graph-name) + (notification/show! "Empty graph name." :error) + + (repo-handler/graph-already-exists? graph-name) + (notification/show! "Please specify another name as another graph with this name already exists!" :error) + + :else + (db-import-handler/import-from-sqlite-zip! file graph-name finished-cb))) + (or debug-transit? db-edn?) (let [graph-name (string/trim graph-name)] (cond @@ -473,6 +485,19 @@ (shui/dialog-open! #(set-graph-name-dialog e {:sqlite? true})))}]] + [:label.action-input.flex.items-center.mx-2.my-2 + [:span.as-flex-center [:i (svg/logo 28)]] + [:span.flex.flex-col + [[:strong "SQLite + assets (.zip)"] + [:small "Import a zip containing db.sqlite and an assets folder"]]] + [:input.absolute.hidden + {:id "import-sqlite-zip" + :type "file" + :accept ".zip" + :on-change (fn [e] + (shui/dialog-open! + #(set-graph-name-dialog e {:sqlite-zip? true})))}]] + (when-not (util/mobile?) [:label.action-input.flex.items-center.mx-2.my-2 [:span.as-flex-center [:i (svg/logo 28)]] diff --git a/src/main/frontend/handler/db_based/import.cljs b/src/main/frontend/handler/db_based/import.cljs index 72b244ad6e..941c36d03a 100644 --- a/src/main/frontend/handler/db_based/import.cljs +++ b/src/main/frontend/handler/db_based/import.cljs @@ -1,9 +1,12 @@ (ns frontend.handler.db-based.import "Handles DB graph imports" - (:require [clojure.edn :as edn] + (:require ["jszip" :as JSZip] + [clojure.edn :as edn] + [clojure.string :as string] [datascript.core :as d] [frontend.config :as config] [frontend.db :as db] + [frontend.fs :as fs] [frontend.handler.notification :as notification] [frontend.handler.repo :as repo-handler] [frontend.handler.ui :as ui-handler] @@ -12,12 +15,88 @@ [frontend.persist-db :as persist-db] [frontend.state :as state] [frontend.util :as util] + [logseq.common.config :as common-config] + [logseq.common.path :as path] [logseq.db :as ldb] [logseq.db.sqlite.export :as sqlite-export] [logseq.db.sqlite.util :as sqlite-util] [logseq.shui.ui :as shui] [promesa.core :as p])) +(defn- zip-entries + [^js zip] + (->> (js/Object.keys (.-files zip)) + (array-seq) + (map (fn [name] + (let [entry (aget (.-files zip) name)] + {:name name + :entry entry + :dir? (true? (.-dir entry))}))) + vec)) + +(defn- sqlite-entry + [entries] + (let [candidates (filter (fn [{:keys [name dir?]}] + (let [name (-> name + (string/replace #"\\+" "/") + string/lower-case)] + (and (not dir?) + (string/ends-with? name "db.sqlite")))) + entries)] + (first (sort-by (comp count :name) candidates)))) + +(defn- asset-entry? + [name] + (or (string/starts-with? name "assets/") + (string/includes? name "/assets/"))) + +(defn- asset-file-name + [name] + (let [name (if-let [idx (string/last-index-of name "/assets/")] + (subs name (+ idx (count "/assets/"))) + (string/replace-first name #"^assets/" ""))] + (last (string/split name #"/")))) + +(defn- > entries + (filter (fn [{:keys [name dir?]}] + (and (not dir?) + (asset-entry? name)))) + vec)] + (if (empty? assets) + (p/resolved {:copied 0 :failed []}) + (p/let [repo-dir (config/get-repo-dir repo) + assets-dir (path/path-join repo-dir common-config/local-assets-dir) + _ (fs/mkdir-if-not-exists assets-dir)] + (p/loop [remaining assets + count 0 + failed []] + (if (empty? remaining) + {:copied count :failed failed} + (let [{:keys [name entry]} (first remaining) + file-name (asset-file-name name)] + (if (string/blank? file-name) + (p/recur (subvec remaining 1) count failed) + (p/let [data (.async entry "uint8array") + write-result (p/catch ( (p/let [zip-buffer (.arrayBuffer file) + ^js zip (.loadAsync JSZip zip-buffer) + entries (zip-entries zip) + entry (sqlite-entry entries) + asset-total (count (filter (fn [{:keys [name dir?]}] + (and (not dir?) + (asset-entry? name))) + entries)) + _ (when (pos? asset-total) + (state/set-state! :graph/importing :sqlite-zip) + (state/set-state! :graph/importing-state {:total asset-total + :current-idx 0 + :current-page "Assets"}))] + (if-not entry + (notification/show! "Zip missing db.sqlite. Please check the archive structure." :error) + (p/let [sqlite-buffer (.async ^js (:entry entry) "arraybuffer")] + (import-from-sqlite-db! + sqlite-buffer + bare-graph-name + (fn [] + (p/let [repo (state/get-current-repo) + {:keys [copied failed]} + (js failed))) + (state/set-state! :graph/importing nil) + (state/set-state! :graph/importing-state nil) + (finished-ok-handler))))))) + (p/catch + (fn [e] + (js/console.error e) + (state/set-state! :graph/importing nil) + (state/set-state! :graph/importing-state nil) + (notification/show! (str "Zip import failed: " (.-message e)) :error))))) + (defn import-from-debug-transit! [bare-graph-name raw finished-ok-handler] (let [graph (str config/db-version-prefix bare-graph-name)