1
0
mirror of https://github.com/vrtmrz/obsidian-livesync.git synced 2024-12-12 09:04:06 +02:00

- Actions for maintaining databases moved to the 🎛️Maintain databases.

- Clean-up of unreferenced chunks has been implemented on an **experimental**.
This commit is contained in:
vorotamoroz 2023-04-13 17:33:17 +09:00
parent 5e75917b8d
commit 9a27c9bfe5

View File

@ -1,5 +1,5 @@
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, RequestUrlParam, requestUrl, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings } from "./lib/src/types";
import { DEFAULT_SETTINGS, LOG_LEVEL, ObsidianLiveSyncSettings, ConfigPassphraseStore, RemoteDBSettings, NewEntry } from "./lib/src/types";
import { delay } from "./lib/src/utils";
import { Semaphore } from "./lib/src/semaphore";
import { versionNumberString2Number } from "./lib/src/strbin";
@ -8,22 +8,25 @@ import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
import { testCrypt } from "./lib/src/e2ee_v2";
import ObsidianLiveSyncPlugin from "./main";
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string) => {
const _requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, path?: string, body?: any, method?: string) => {
const utf8str = String.fromCharCode.apply(null, new TextEncoder().encode(`${username}:${password}`));
const encoded = window.btoa(utf8str);
const authHeader = "Basic " + encoded;
// const origin = "capacitor://localhost";
const transformedHeaders: Record<string, string> = { authorization: authHeader, origin: origin };
const uri = `${baseUri}/_node/_local/_config${key ? "/" + key : ""}`;
const uri = `${baseUri}/${path}`;
const requestParam: RequestUrlParam = {
url: uri,
method: body ? "PUT" : "GET",
method: method || (body ? "PUT" : "GET"),
headers: transformedHeaders,
contentType: "application/json",
body: body ? JSON.stringify(body) : undefined,
};
return await requestUrl(requestParam);
}
const requestToCouchDB = async (baseUri: string, username: string, password: string, origin: string, key?: string, body?: string, method?: string) => {
const uri = `_node/_local/_config${key ? "/" + key : ""}`;
return await _requestToCouchDB(baseUri, username, password, origin, uri, body, method);
};
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
plugin: ObsidianLiveSyncPlugin;
@ -74,7 +77,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
<label class='sls-setting-label c-40'><input type='radio' name='disp' value='40' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔧</div></label>
<label class='sls-setting-label c-50 wizardHidden'><input type='radio' name='disp' value='50' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🧰</div></label>
<label class='sls-setting-label c-60 wizardHidden'><input type='radio' name='disp' value='60' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🔌</div></label>
<!-- <label class='sls-setting-label c-70 wizardHidden'><input type='radio' name='disp' value='70' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🚑</div></label>-->
<label class='sls-setting-label c-70 wizardHidden'><input type='radio' name='disp' value='70' class='sls-setting-tab' ><div class='sls-setting-menu-btn'>🎛</div></label>
`;
const menuTabs = w.querySelectorAll(".sls-setting-label");
const changeDisplay = (screen: string) => {
@ -486,7 +489,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
this.plugin.settings.passphrase = passphrase;
this.plugin.settings.useDynamicIterationCount = useDynamicIterationCount;
this.plugin.settings.usePathObfuscation = usePathObfuscation;
Logger("All synchronizations have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL.NOTICE)
Logger("All synchronization have been temporarily disabled. Please enable them after the fetching, if you need them.", LOG_LEVEL.NOTICE)
await this.plugin.saveSettings();
markDirtyControl();
applyDisplayEnabled();
@ -504,34 +507,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
}
new Setting(containerRemoteDatabaseEl)
.setName("Overwrite remote database")
.setDesc("Overwrite remote database with local DB and passphrase.")
.setClass("wizardHidden")
.addButton((button) =>
button
.setButtonText("Send")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("remoteOnly");
})
)
new Setting(containerRemoteDatabaseEl)
.setName("Rebuild everything")
.setDesc("Rebuild local and remote database with local files.")
.setClass("wizardHidden")
.addButton((button) =>
button
.setButtonText("Rebuild")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("rebuildBothByThisDevice");
})
)
new Setting(containerRemoteDatabaseEl)
.setName("Test Database Connection")
.setDesc("Open database connection. If the remote database is not found and you have the privilege to create a database, the database will be created.")
@ -715,19 +690,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
text: "",
});
new Setting(containerRemoteDatabaseEl)
.setName("Lock remote database")
.setDesc("Lock remote database to prevent synchronization with other devices.")
.setClass("wizardHidden")
.addButton((button) =>
button
.setButtonText("Lock")
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.markRemoteLocked();
})
);
let rebuildRemote = false;
new Setting(containerRemoteDatabaseEl)
@ -792,21 +754,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
})
);
new Setting(containerLocalDatabaseEl)
.setName("Fetch rebuilt DB")
.setDesc("Restore or reconstruct local database from remote database.")
.setClass("wizardHidden")
.addButton((button) =>
button
.setButtonText("Fetch")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("localOnly");
})
)
let newDatabaseName = this.plugin.settings.additionalSuffixOfDatabaseName + "";
new Setting(containerLocalDatabaseEl)
.setName("Database suffix")
@ -1695,18 +1642,59 @@ ${stringifyYaml(pluginConfig)}`;
})
);
new Setting(containerHatchEl)
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
.addButton((button) =>
button
.setButtonText("Discard")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin.resetLocalDatabase();
await this.plugin.initializeDatabase();
})
);
const localDatabaseCleanUp = async (force: boolean) => {
const usedMap = new Map();
const existMap = new Map();
const db = this.plugin.localDatabase.localDatabase;
if ((db as any)?.adapter != "indexeddb") {
if (force) {
Logger("Fetch from the remote database", LOG_LEVEL.NOTICE, "clean-up-db");
await rebuildDB("localOnly");
return;
} else {
Logger("This feature requires enabling `Use new adapter`. Please enable it", LOG_LEVEL.NOTICE, "clean-up-db");
return;
}
}
Logger(`The remote database locked for garbage collection`, LOG_LEVEL.NOTICE, "clean-up-db");
Logger(`Gathering chunk usage information`, LOG_LEVEL.NOTICE, "clean-up-db");
const xx = await db.allDocs({ startkey: "h:", endkey: `h:\u{10ffff}` });
for (const xxd of xx.rows) {
const chunk = xxd.id
existMap.set(chunk, xxd.value.rev);
}
const x = await db.find({ limit: 999999999, selector: { children: { $exists: true, $type: "array" } }, fields: ["_id", "mtime", "children"] });
for (const temp of x.docs) {
for (const chunk of (temp as NewEntry).children) {
usedMap.set(chunk, (usedMap.has(chunk) ? usedMap.get(chunk) : 0) + 1);
existMap.delete(chunk);
}
}
const payload = {} as Record<string, string[]>;
for (const [id, rev] of existMap) {
payload[id] = [rev];
}
const removeItems = Object.keys(payload).length;
if (removeItems == 0) {
Logger(`No unreferenced chunks found`, LOG_LEVEL.NOTICE, "clean-up-db");
return;
}
Logger(`Deleting unreferenced chunks: ${removeItems}`, LOG_LEVEL.NOTICE, "clean-up-db");
for (const [id, rev] of existMap) {
//@ts-ignore
const ret = await db.purge(id, rev);
Logger(ret, LOG_LEVEL.VERBOSE);
}
this.plugin.localDatabase.refreshSettings();
Logger(`Compacting local database...`, LOG_LEVEL.NOTICE, "clean-up-db");
await db.compact();
Logger("Done!", LOG_LEVEL.NOTICE, "clean-up-db");
}
addScreenElement("50", containerHatchEl);
// With great respect, thank you TfTHacker!
@ -1784,73 +1772,171 @@ ${stringifyYaml(pluginConfig)}`;
addScreenElement("60", containerPluginSettings);
// const containerCorruptedDataEl = containerEl.createDiv();
const containerMaintenanceEl = containerEl.createDiv();
// containerCorruptedDataEl.createEl("h3", { text: "Corrupted or missing data" });
// containerCorruptedDataEl.createEl("h4", { text: "Corrupted" });
// if (Object.keys(this.plugin.localDatabase.corruptedEntries).length > 0) {
// const cx = containerCorruptedDataEl.createEl("div", { text: "If you have a copy of these files on any device, simply edit them once and sync. If not, there's nothing we can do except deleting them. sorry.." });
// for (const k in this.plugin.localDatabase.corruptedEntries) {
// const xx = cx.createEl("div", { text: `${k}` });
containerMaintenanceEl.createEl("h3", { text: "Maintain databases" });
// const ba = xx.createEl("button", { text: `Delete this` }, (e) => {
// e.addEventListener("click", async () => {
// await this.plugin.localDatabase.deleteDBEntry(k as string as FilePathWithPrefix /* should be explained */);
// xx.remove();
// });
// });
// ba.addClass("mod-warning");
// //TODO: FIX LATER
// // xx.createEl("button", { text: `Restore from file` }, (e) => {
// // e.addEventListener("click", async () => {
// // const f = await this.app.vault.getFiles().filter((e) => this.plugin.path2id(e.path) == k);
// // if (f.length == 0) {
// // Logger("Not found in vault", LOG_LEVEL.NOTICE);
// // return;
// // }
// // await this.plugin.updateIntoDB(f[0]);
// // xx.remove();
// // });
// // });
// // xx.addClass("mod-warning");
// }
// } else {
// containerCorruptedDataEl.createEl("div", { text: "There is no corrupted data." });
// }
// containerCorruptedDataEl.createEl("h4", { text: "Missing or waiting" });
// if (Object.keys(this.plugin.queuedFiles).length > 0) {
// const cx = containerCorruptedDataEl.createEl("div", {
// text: "These files have missing or waiting chunks. Perhaps these chunks will arrive in a while after replication. But if they don't, you have to restore it's database entry from a existing local file by hitting the button below.",
// });
// const files = [...new Set([...this.plugin.queuedFiles.map((e) => e.entry._id)])];
// for (const k of files) {
// const xx = cx.createEl("div", { text: `${this.plugin.id2path(k)}` });
containerMaintenanceEl.createEl("h4", { text: "The remote database" });
// const ba = xx.createEl("button", { text: `Delete this` }, (e) => {
// e.addEventListener("click", async () => {
// await this.plugin.localDatabase.deleteDBEntry(k);
// xx.remove();
// });
// });
// ba.addClass("mod-warning");
// xx.createEl("button", { text: `Restore from file` }, (e) => {
// e.addEventListener("click", async () => {
// const f = await this.app.vault.getFiles().filter((e) => this.plugin.path2id(e.path) == k);
// if (f.length == 0) {
// Logger("Not found in vault", LOG_LEVEL.NOTICE);
// return;
// }
// await this.plugin.updateIntoDB(f[0]);
// xx.remove();
// });
// });
// xx.addClass("mod-warning");
// }
// } else {
// containerCorruptedDataEl.createEl("div", { text: "There is no missing or waiting chunk." });
// }
// applyDisplayEnabled();
// addScreenElement("70", containerCorruptedDataEl);
new Setting(containerMaintenanceEl)
.setName("Lock remote database")
.setDesc("Lock remote database to prevent synchronization with other devices.")
.addButton((button) =>
button
.setButtonText("Lock")
.setDisabled(false)
.setWarning()
.onClick(async () => {
await this.plugin.markRemoteLocked();
})
);
new Setting(containerMaintenanceEl)
.setName("Overwrite remote database")
.setDesc("Overwrite remote database with local DB and passphrase.")
.addButton((button) =>
button
.setButtonText("Send")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("remoteOnly");
})
)
new Setting(containerMaintenanceEl)
.setName("(Experimental) Clean the remote database")
.setDesc("")
.addButton((button) =>
button.setButtonText("Perform cleaning")
.setDisabled(false)
.setWarning()
.onClick(async () => {
// @ts-ignore
this.plugin.app.setting.close()
try {
const usedMap = new Map();
const existMap = new Map();
const ret = await this.plugin.replicator.connectRemoteCouchDBWithSetting(this.plugin.settings, this.plugin.isMobile);
if (typeof ret === "string") {
Logger(`Connect error: ${ret}`, LOG_LEVEL.NOTICE, "clean-up-db");
return;
}
const info = ret.info;
Logger(JSON.stringify(info), LOG_LEVEL.VERBOSE, "clean-up-db");
Logger(`Database data-size:${(info as any)?.data_size ?? "-"}, disk-size: ${(info as any)?.disk_size ?? "-"}`);
Logger(`The remote database locked for garbage collection`, LOG_LEVEL.NOTICE, "clean-up-db");
await this.plugin.markRemoteLocked();
Logger(`Gathering chunk usage information`, LOG_LEVEL.NOTICE, "clean-up-db");
const db = ret.db;
const xx = await db.allDocs({ startkey: "h:", endkey: `h:\u{10ffff}` });
for (const xxd of xx.rows) {
const chunk = xxd.id
existMap.set(chunk, xxd.value.rev);
}
const x = await db.find({ limit: 999999999, selector: { children: { $exists: true, $type: "array" } }, fields: ["_id", "mtime", "children"] });
for (const temp of x.docs) {
for (const chunk of (temp as NewEntry).children) {
usedMap.set(chunk, (usedMap.has(chunk) ? usedMap.get(chunk) : 0) + 1);
existMap.delete(chunk);
}
}
const payload = {} as Record<string, string[]>;
for (const [id, rev] of existMap) {
payload[id] = [rev];
}
const removeItems = Object.keys(payload).length;
if (removeItems == 0) {
Logger(`No unreferenced chunk found`, LOG_LEVEL.NOTICE, "clean-up-db");
return;
}
Logger(`Deleting unreferenced chunks: ${removeItems}`, LOG_LEVEL.NOTICE, "clean-up-db");
const rets = await _requestToCouchDB(
`${this.plugin.settings.couchDB_URI}/${this.plugin.settings.couchDB_DBNAME}`,
this.plugin.settings.couchDB_USER,
this.plugin.settings.couchDB_PASSWORD,
undefined,
"_purge",
payload, "POST");
// const result = await rets();
Logger(JSON.stringify(rets.text), LOG_LEVEL.VERBOSE);
Logger(`Compacting database...`, LOG_LEVEL.NOTICE, "clean-up-db");
await db.compact();
const endInfo = await db.info();
Logger(`Result database data-size:${(endInfo as any)?.data_size ?? "-"}, disk-size: ${(endInfo as any)?.disk_size ?? "-"}`);
Logger(JSON.stringify(endInfo), LOG_LEVEL.VERBOSE, "clean-up-db");
Logger(`Local database cleaning up...`);
await localDatabaseCleanUp(true);
} catch (ex) {
Logger("Failed to clean up db.")
Logger(ex, LOG_LEVEL.VERBOSE);
}
})
);
containerMaintenanceEl.createEl("h4", { text: "The local database" });
new Setting(containerMaintenanceEl)
.setName("Fetch rebuilt DB")
.setDesc("Restore or reconstruct local database from remote database.")
.addButton((button) =>
button
.setButtonText("Fetch")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("localOnly");
})
)
new Setting(containerMaintenanceEl)
.setName("(Experimental) Clean the local database")
.setDesc("")
.addButton((button) =>
button.setButtonText("Perform cleaning")
.setDisabled(false)
.setWarning()
.onClick(async () => {
// @ts-ignore
this.plugin.app.setting.close()
await localDatabaseCleanUp(false);
await this.plugin.markRemoteResolved();
})
);
new Setting(containerMaintenanceEl)
.setName("Discard local database to reset or uninstall Self-hosted LiveSync")
.addButton((button) =>
button
.setButtonText("Discard")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await this.plugin.resetLocalDatabase();
await this.plugin.initializeDatabase();
})
);
containerMaintenanceEl.createEl("h4", { text: "Both databases" });
new Setting(containerMaintenanceEl)
.setName("Rebuild everything")
.setDesc("Rebuild local and remote database with local files.")
.addButton((button) =>
button
.setButtonText("Rebuild")
.setWarning()
.setDisabled(false)
.onClick(async () => {
await rebuildDB("rebuildBothByThisDevice");
})
)
applyDisplayEnabled();
addScreenElement("70", containerMaintenanceEl);
applyDisplayEnabled();
if (this.selectedScreen == "") {