mirror of
https://github.com/logseq/logseq.git
synced 2026-05-18 01:42:19 +00:00
fix: sqlite db backup on desktop
This commit is contained in:
40
deps/db/src/logseq/db/sqlite/backup.cljs
vendored
40
deps/db/src/logseq/db/sqlite/backup.cljs
vendored
@@ -1,6 +1,7 @@
|
||||
(ns logseq.db.sqlite.backup
|
||||
"Shared SQLite backup utilities for Node runtimes."
|
||||
(:require ["node:sqlite" :as node-sqlite]
|
||||
(:require ["node:fs" :as fs]
|
||||
["node:sqlite" :as node-sqlite]
|
||||
[clojure.string :as string]
|
||||
[goog.object :as gobj]
|
||||
[promesa.core :as p]))
|
||||
@@ -16,9 +17,6 @@
|
||||
(throw (ex-info "node:sqlite DatabaseSync constructor missing"
|
||||
{:module-keys (js->clj (js/Object.keys node-sqlite))}))))
|
||||
|
||||
(def ^:private DatabaseSync
|
||||
(resolve-database-sync-ctor))
|
||||
|
||||
(defn- resolve-sqlite-backup-fn
|
||||
[]
|
||||
(or (gobj/get node-sqlite "backup")
|
||||
@@ -30,17 +28,31 @@
|
||||
(def ^:private sqlite-backup-fn
|
||||
(resolve-sqlite-backup-fn))
|
||||
|
||||
(defn- remove-file-if-exists!
|
||||
[path]
|
||||
(fs/rmSync path #js {:force true}))
|
||||
|
||||
(defn backup-connection!
|
||||
[^js db path]
|
||||
(sqlite-backup-fn db path))
|
||||
(try
|
||||
(-> (sqlite-backup-fn db path)
|
||||
(p/catch (fn [error]
|
||||
(remove-file-if-exists! path)
|
||||
(throw error))))
|
||||
(catch :default error
|
||||
(remove-file-if-exists! path)
|
||||
(p/rejected error))))
|
||||
|
||||
(defn backup-db-file!
|
||||
[src-path dst-path]
|
||||
(let [db (new DatabaseSync src-path)]
|
||||
(-> (backup-connection! db dst-path)
|
||||
(p/finally (fn []
|
||||
(try
|
||||
(.close db)
|
||||
(catch :default error
|
||||
(when-not (string/includes? (str error) "database is not open")
|
||||
(throw error)))))))))
|
||||
([src-path dst-path]
|
||||
(let [DatabaseSync (resolve-database-sync-ctor)
|
||||
db (new DatabaseSync src-path)]
|
||||
(-> (backup-connection! db dst-path)
|
||||
(p/finally (fn []
|
||||
(try
|
||||
(.close db)
|
||||
(catch :default error
|
||||
(when-not (string/includes? (str error) "database is not open")
|
||||
(throw error)))))))))
|
||||
([db _src-path dst-path]
|
||||
(backup-connection! db dst-path)))
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
(:require ["fs-extra" :as fs]
|
||||
["path" :as node-path]
|
||||
[electron.backup-file :as backup-file]
|
||||
[electron.db-worker :as db-worker]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.cli.common.graph :as cli-common-graph]
|
||||
[logseq.cli.transport :as cli-transport]
|
||||
[logseq.common.graph-dir :as graph-dir]
|
||||
[logseq.db.common.sqlite :as common-sqlite]
|
||||
[logseq.db.sqlite.backup :as sqlite-backup]
|
||||
@@ -45,15 +47,15 @@
|
||||
(rand-int 1000000)
|
||||
".sqlite")))
|
||||
|
||||
(defn backup-db!
|
||||
[db-name {:keys [force-backup?]}]
|
||||
(defn backup-db-with-sqlite-backup!
|
||||
[db-name {:keys [force-backup? sqlite-backup!]}]
|
||||
(let [_ (ensure-graph-dir! db-name)
|
||||
[_db-name db-path] (common-sqlite/get-db-full-path (cli-common-graph/get-db-graphs-dir) db-name)
|
||||
backups-path (common-sqlite/get-db-backups-path (cli-common-graph/get-db-graphs-dir) db-name)]
|
||||
(when (fs/existsSync db-path)
|
||||
(let [tmp-path (temp-backup-path backups-path)]
|
||||
(-> (p/let [_ (fs/ensureDirSync backups-path)
|
||||
_ (sqlite-backup/backup-db-file! db-path tmp-path)
|
||||
_ (sqlite-backup! db-path tmp-path)
|
||||
payload (fs/readFileSync tmp-path)]
|
||||
(backup-file/backup-file db-name nil nil
|
||||
".sqlite"
|
||||
@@ -67,19 +69,42 @@
|
||||
(catch :default _
|
||||
nil)))))))))
|
||||
|
||||
(defn- active-repos
|
||||
(defn backup-db!
|
||||
[db-name opts]
|
||||
(backup-db-with-sqlite-backup!
|
||||
db-name
|
||||
(assoc opts :sqlite-backup! sqlite-backup/backup-db-file!)))
|
||||
|
||||
(defn backup-db-via-worker!
|
||||
[db-name window-id opts]
|
||||
(backup-db-with-sqlite-backup!
|
||||
db-name
|
||||
(assoc opts
|
||||
:sqlite-backup!
|
||||
(fn [_src-path dst-path]
|
||||
(p/let [runtime (db-worker/ensure-runtime! db-name window-id)]
|
||||
(cli-transport/invoke runtime
|
||||
:thread-api/backup-db-sqlite
|
||||
false
|
||||
[db-name dst-path]))))))
|
||||
|
||||
(defn- active-repo-window-ids
|
||||
[]
|
||||
(->> (:window->repo @*auto-backup)
|
||||
vals
|
||||
(remove nil?)
|
||||
set
|
||||
vec))
|
||||
(let [repo->window-ids (reduce-kv (fn [m window-id repo]
|
||||
(if (seq repo)
|
||||
(update m repo (fnil conj []) window-id)
|
||||
m))
|
||||
{}
|
||||
(:window->repo @*auto-backup))]
|
||||
(mapv (fn [[repo window-ids]]
|
||||
[repo (first window-ids)])
|
||||
repo->window-ids)))
|
||||
|
||||
(defn run-auto-backup!
|
||||
[]
|
||||
(p/all
|
||||
(for [repo (active-repos)]
|
||||
(-> (backup-db! repo {})
|
||||
(for [[repo window-id] (active-repo-window-ids)]
|
||||
(-> (backup-db-via-worker! repo window-id {})
|
||||
(p/catch (fn [error]
|
||||
(log/warn :electron/auto-db-backup-failed
|
||||
{:repo repo
|
||||
@@ -89,7 +114,7 @@
|
||||
(defn- reconcile-auto-backup-timer!
|
||||
[]
|
||||
(let [{:keys [interval-id]} @*auto-backup
|
||||
has-repos? (seq (active-repos))]
|
||||
has-repos? (seq (active-repo-window-ids))]
|
||||
(cond
|
||||
(and has-repos? (nil? interval-id))
|
||||
(let [id (js/setInterval (fn [] (run-auto-backup!))
|
||||
|
||||
@@ -234,10 +234,10 @@
|
||||
(p/rejected (ex-info "repo is required" {:code :missing-repo}))
|
||||
(db-worker/ensure-runtime! (canonical-repo repo) (.-id window))))
|
||||
|
||||
(defmethod handle :db-export [_window [_ repo force-backup?]]
|
||||
(defmethod handle :db-export [window [_ repo force-backup?]]
|
||||
(when-let [repo (canonical-repo repo)]
|
||||
(db/ensure-graph-dir! repo)
|
||||
(db/backup-db! repo {:force-backup? force-backup?})))
|
||||
(db/backup-db-via-worker! repo (.-id window) {:force-backup? force-backup?})))
|
||||
|
||||
(defmethod handle :db-get [_window [_ repo]]
|
||||
(when-let [repo (canonical-repo repo)]
|
||||
|
||||
@@ -114,6 +114,48 @@
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest backup-db-with-sqlite-backup-uses-provided-snapshot-fn
|
||||
(async done
|
||||
(let [graphs-dir (node-helper/create-tmp-dir "electron-db-backup-custom-snapshot")
|
||||
db-name "logseq_db_demo"
|
||||
[_ db-path] (common-sqlite/get-db-full-path graphs-dir db-name)
|
||||
backups-path (common-sqlite/get-db-backups-path graphs-dir db-name)
|
||||
custom-calls (atom [])
|
||||
fallback-calls (atom [])
|
||||
backup-calls (atom [])]
|
||||
(fs/ensureDirSync (node-path/dirname db-path))
|
||||
(fs/writeFileSync db-path "seed")
|
||||
(-> (p/with-redefs [cli-common-graph/get-db-graphs-dir (fn [] graphs-dir)
|
||||
sqlite-backup/backup-db-file! (fn [src dst]
|
||||
(swap! fallback-calls conj [src dst])
|
||||
(p/rejected (ex-info "fallback should not run" {})))
|
||||
backup-file/backup-file (fn [repo _dir _relative-path ext content & {:as opts}]
|
||||
(swap! backup-calls conj {:repo repo
|
||||
:ext ext
|
||||
:content (.toString content)
|
||||
:opts opts})
|
||||
nil)]
|
||||
(p/let [_ (electron-db/backup-db-with-sqlite-backup!
|
||||
db-name
|
||||
{:force-backup? true
|
||||
:sqlite-backup! (fn [src dst]
|
||||
(swap! custom-calls conj [src dst])
|
||||
(fs/writeFileSync dst "worker-copy")
|
||||
(p/resolved nil))})]
|
||||
(is (= [[db-path (second (first @custom-calls))]]
|
||||
@custom-calls))
|
||||
(is (empty? @fallback-calls))
|
||||
(is (= 1 (count @backup-calls)))
|
||||
(let [{:keys [repo ext content opts]} (first @backup-calls)]
|
||||
(is (= db-name repo))
|
||||
(is (= ".sqlite" ext))
|
||||
(is (= "worker-copy" content))
|
||||
(is (= backups-path (:backups-dir opts)))
|
||||
(is (= true (:force-backup? opts))))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest auto-backup-tracker-runs-hourly-for-active-repos
|
||||
(async done
|
||||
(let [set-interval-calls (atom [])
|
||||
@@ -128,9 +170,9 @@
|
||||
timer-id))
|
||||
(set! js/clearInterval (fn [id]
|
||||
(swap! clear-interval-calls conj id)))
|
||||
(-> (p/with-redefs [electron-db/backup-db! (fn [repo _]
|
||||
(swap! backup-calls conj repo)
|
||||
(p/resolved nil))]
|
||||
(-> (p/with-redefs [electron-db/backup-db-via-worker! (fn [repo window-id _]
|
||||
(swap! backup-calls conj [repo window-id])
|
||||
(p/resolved nil))]
|
||||
(p/let [_ (electron-db/sync-auto-backup-repo! 1 "logseq_db_demo")
|
||||
_ (electron-db/sync-auto-backup-repo! 2 "logseq_db_demo")
|
||||
_ ((ffirst @set-interval-calls))
|
||||
@@ -139,7 +181,7 @@
|
||||
(is (= 1 (count @set-interval-calls)))
|
||||
(is (= [timer-id] @clear-interval-calls))
|
||||
(is (= [3600000] (mapv second @set-interval-calls)))
|
||||
(is (= ["logseq_db_demo"] @backup-calls))))
|
||||
(is (= [["logseq_db_demo" 1]] @backup-calls))))
|
||||
(p/catch (fn [e]
|
||||
(is false (str "unexpected error: " e))))
|
||||
(p/finally (fn []
|
||||
|
||||
46
src/test/logseq/db/sqlite/backup_test.cljs
Normal file
46
src/test/logseq/db/sqlite/backup_test.cljs
Normal file
@@ -0,0 +1,46 @@
|
||||
(ns logseq.db.sqlite.backup-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[frontend.test.node-helper :as node-helper]
|
||||
[goog.object :as gobj]
|
||||
[logseq.db.sqlite.backup :as sqlite-backup]
|
||||
[promesa.core :as p]
|
||||
["fs-extra" :as fs]
|
||||
["path" :as node-path]))
|
||||
|
||||
(deftest backup-connection-removes-partial-destination-on-failure
|
||||
(async done
|
||||
(let [dir (node-helper/create-tmp-dir "sqlite-backup-failure")
|
||||
dst-path (node-path/join dir "backup.sqlite")
|
||||
expected-error (js/Error. "backup failed")]
|
||||
(-> (p/with-redefs [sqlite-backup/sqlite-backup-fn
|
||||
(fn [_db path]
|
||||
(fs/writeFileSync path "")
|
||||
(p/rejected expected-error))]
|
||||
(sqlite-backup/backup-connection! #js {} dst-path))
|
||||
(p/then (fn [_]
|
||||
(is false "backup should reject")))
|
||||
(p/catch (fn [error]
|
||||
(is (= expected-error error))
|
||||
(is (not (fs/existsSync dst-path)))))
|
||||
(p/finally done)))))
|
||||
|
||||
(deftest backup-db-file-with-existing-connection-propagates-backup-failure
|
||||
(async done
|
||||
(let [dir (node-helper/create-tmp-dir "sqlite-backup-file-failure")
|
||||
dst-path (node-path/join dir "backup.sqlite")
|
||||
expected-error (js/Error. "backup failed")
|
||||
closed? (atom false)
|
||||
db #js {}]
|
||||
(gobj/set db "close" (fn [] (reset! closed? true)))
|
||||
(-> (p/with-redefs [sqlite-backup/sqlite-backup-fn
|
||||
(fn [_db path]
|
||||
(fs/writeFileSync path "")
|
||||
(p/rejected expected-error))]
|
||||
(sqlite-backup/backup-db-file! db "source.sqlite" dst-path))
|
||||
(p/then (fn [_]
|
||||
(is false "backup should reject")))
|
||||
(p/catch (fn [error]
|
||||
(is (= expected-error error))
|
||||
(is (not @closed?))
|
||||
(is (not (fs/existsSync dst-path)))))
|
||||
(p/finally done)))))
|
||||
Reference in New Issue
Block a user