mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-05 01:18:47 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0b202bd91 | ||
|
|
d54b7e2d93 | ||
|
|
6952ef37f5 | ||
|
|
9630bcbae8 | ||
|
|
c3f925ab9a | ||
|
|
034dc0538f | ||
|
|
b6136df836 | ||
|
|
24aacdc2a1 | ||
|
|
f91109b1ad |
@@ -16,7 +16,8 @@ There are three methods to set up Self-hosted LiveSync.
|
||||
|
||||
### 1. Using setup URIs
|
||||
|
||||
> [!TIP] What is the setup URI? Why is it required?
|
||||
> [!TIP]
|
||||
> What is the setup URI? Why is it required?
|
||||
> The setup URI is the encrypted representation of Self-hosted LiveSync configuration as a URI. This starts `obsidian://setuplivesync?settings=`. This is encrypted with a passphrase, so that it can be shared relatively securely between devices. It is a bit long, but it is one line. This allows a series of settings to be set at once without any inconsistencies.
|
||||
>
|
||||
> If you have configured the remote database by [Automated setup on Fly.io](./setup_flyio.md#a-very-automated-setup) or [set up your server with the tool](./setup_own_server.md#1-generate-the-setup-uri-on-a-desktop-device-or-server), **you should have one of them**
|
||||
@@ -54,6 +55,7 @@ If you do not have any setup URI, Press the `start` button. The setting dialogue
|
||||
#### Test database connection and Check database configuration
|
||||
|
||||
We can check the connectivity to the database, and the database settings.
|
||||
|
||||

|
||||
|
||||
#### Check and Fix database configuration
|
||||
@@ -104,4 +106,4 @@ And, please copy the setup URI by `Copy current settings as a new setup URI` and
|
||||
## At the subsequent device
|
||||
After installing Self-hosted LiveSync on the first device, we should have a setup URI. **The first choice is to use it**. Please share it with the device you want to setup.
|
||||
|
||||
It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it.
|
||||
It is completely same as [Using setup URIs on the first device](#1-using-setup-uris). Please refer it.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.22.14",
|
||||
"version": "0.22.17",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.22.14",
|
||||
"version": "0.22.17",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.22.14",
|
||||
"version": "0.22.17",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.22.14",
|
||||
"version": "0.22.17",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Notice, type PluginManifest, parseYaml, normalizePath, type ListedFiles
|
||||
import type { EntryDoc, LoadedEntry, InternalFileEntry, FilePathWithPrefix, FilePath, DocumentID, AnyEntry, SavingEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE } from "./lib/src/types";
|
||||
import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
||||
import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, isDocContentSame, sendSignal, waitForSignal } from "./lib/src/utils";
|
||||
import { createSavingEntryFromLoadedEntry, createTextBlob, delay, fireAndForget, getDocData, isDocContentSame, throttle } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { readString, decodeBinary, arrayBufferToBase64, digestHash } from "./lib/src/strbin";
|
||||
import { serialized } from "./lib/src/lock";
|
||||
@@ -305,7 +305,8 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
createMissingConfigurationEntry() {
|
||||
createMissingConfigurationEntry = throttle(() => this._createMissingConfigurationEntry(), 1000);
|
||||
_createMissingConfigurationEntry() {
|
||||
let saveRequired = false;
|
||||
for (const v of this.pluginList) {
|
||||
const key = `${v.category}/${v.name}`;
|
||||
@@ -335,7 +336,11 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
try {
|
||||
const pluginData = await this.loadPluginData(path);
|
||||
if (pluginData) {
|
||||
return [pluginData];
|
||||
let newList = [...this.pluginList];
|
||||
newList = newList.filter(x => x.documentPath != pluginData.documentPath);
|
||||
newList.push(pluginData);
|
||||
this.pluginList = newList;
|
||||
pluginList.set(newList);
|
||||
}
|
||||
// Failed to load
|
||||
return [];
|
||||
@@ -345,28 +350,9 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return [];
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 5, delay: 100, yieldThreshold: 10, maintainDelay: false }).pipeTo(
|
||||
new QueueProcessor(
|
||||
async (pluginDataList) => {
|
||||
// Concurrency is two, therefore, we can unlock the previous awaiting.
|
||||
sendSignal("plugin-next-load");
|
||||
let newList = [...this.pluginList];
|
||||
for (const item of pluginDataList) {
|
||||
newList = newList.filter(x => x.documentPath != item.documentPath);
|
||||
newList.push(item)
|
||||
}
|
||||
this.pluginList = newList;
|
||||
pluginList.set(newList);
|
||||
if (pluginDataList.length != 10) {
|
||||
// If the queue is going to be empty, await subsequent for a while.
|
||||
await waitForSignal("plugin-next-load", 1000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
, { suspended: false, batchSize: 10, concurrentLimit: 2, delay: 100, yieldThreshold: 25, totalRemainingReactiveSource: pluginScanningCount, maintainDelay: false })).startPipeline().root.onIdle(() => {
|
||||
// Logger(`All files enumerated`, LOG_LEVEL_INFO, "get-plugins");
|
||||
this.createMissingConfigurationEntry();
|
||||
});
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 10, delay: 100, yieldThreshold: 10, maintainDelay: false, totalRemainingReactiveSource: pluginScanningCount }).startPipeline().root.onUpdateProgress(() => {
|
||||
this.createMissingConfigurationEntry();
|
||||
});
|
||||
|
||||
|
||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||
@@ -412,7 +398,7 @@ export class ConfigSync extends LiveSyncCommands {
|
||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry, pluginDataA: PluginDataEx, pluginDataB: PluginDataEx): Promise<boolean> {
|
||||
const fileA = { ...pluginDataA.files[0], ctime: pluginDataA.files[0].mtime, _id: `${pluginDataA.documentPath}` as DocumentID };
|
||||
const fileB = pluginDataB.files[0];
|
||||
const docAx = { ...docA, ...fileA } as LoadedEntry, docBx = { ...docB, ...fileB } as LoadedEntry
|
||||
const docAx = { ...docA, ...fileA, datatype: "newnote" } as LoadedEntry, docBx = { ...docB, ...fileB, datatype: "newnote" } as LoadedEntry
|
||||
return serialized("config:merge-data", () => new Promise((res) => {
|
||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||
// const docs = [docA, docB];
|
||||
|
||||
@@ -9,7 +9,7 @@ import { serialized } from "./lib/src/lock";
|
||||
import { JsonResolveModal } from "./JsonResolveModal";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
||||
import { KeyedQueueProcessor, QueueProcessor } from "./lib/src/processor";
|
||||
import { QueueProcessor } from "./lib/src/processor";
|
||||
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "./lib/src/stores";
|
||||
|
||||
export class HiddenFileSync extends LiveSyncCommands {
|
||||
@@ -73,15 +73,15 @@ export class HiddenFileSync extends LiveSyncCommands {
|
||||
}
|
||||
|
||||
procInternalFile(filename: string) {
|
||||
this.internalFileProcessor.enqueueWithKey(filename, filename);
|
||||
this.internalFileProcessor.enqueue(filename);
|
||||
}
|
||||
internalFileProcessor = new KeyedQueueProcessor<string, any>(
|
||||
internalFileProcessor = new QueueProcessor<string, any>(
|
||||
async (filenames) => {
|
||||
Logger(`START :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
|
||||
await this.syncInternalFilesAndDatabase("pull", false, false, filenames);
|
||||
Logger(`DONE :Applying hidden ${filenames.length} files change`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 10, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount }
|
||||
}, { batchSize: 100, concurrentLimit: 1, delay: 10, yieldThreshold: 100, suspended: false, totalRemainingReactiveSource: hiddenFilesEventCount }
|
||||
);
|
||||
|
||||
recentProcessedInternalFiles = [] as string[];
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
import { normalizePath, type PluginManifest } from "./deps";
|
||||
import type { DocumentID, EntryDoc, FilePathWithPrefix, LoadedEntry, SavingEntry } from "./lib/src/types";
|
||||
import { LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE } from "./lib/src/types";
|
||||
import { type PluginDataEntry, PERIODIC_PLUGIN_SWEEP, type PluginList, type DevicePluginList, PSCHeader, PSCHeaderEnd } from "./types";
|
||||
import { createTextBlob, getDocData, isDocContentSame } from "./lib/src/utils";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import { isPluginMetadata, PeriodicProcessor } from "./utils";
|
||||
import { PluginDialogModal } from "./dialogs";
|
||||
import { NewNotice } from "./lib/src/wrapper";
|
||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||
import { serialized, skipIfDuplicated } from "./lib/src/lock";
|
||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||
|
||||
export class PluginAndTheirSettings extends LiveSyncCommands {
|
||||
|
||||
get deviceAndVaultName() {
|
||||
return this.plugin.deviceAndVaultName;
|
||||
}
|
||||
pluginDialog: PluginDialogModal = null;
|
||||
periodicPluginSweepProcessor = new PeriodicProcessor(this.plugin, async () => await this.sweepPlugin(false));
|
||||
|
||||
showPluginSyncModal() {
|
||||
if (this.pluginDialog != null) {
|
||||
this.pluginDialog.open();
|
||||
} else {
|
||||
this.pluginDialog = new PluginDialogModal(this.app, this.plugin);
|
||||
this.pluginDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
hidePluginSyncModal() {
|
||||
if (this.pluginDialog != null) {
|
||||
this.pluginDialog.close();
|
||||
this.pluginDialog = null;
|
||||
}
|
||||
}
|
||||
onload(): void | Promise<void> {
|
||||
this.plugin.addCommand({
|
||||
id: "livesync-plugin-dialog",
|
||||
name: "Show Plugins and their settings",
|
||||
callback: () => {
|
||||
this.showPluginSyncModal();
|
||||
},
|
||||
});
|
||||
this.showPluginSyncModal();
|
||||
}
|
||||
onunload() {
|
||||
this.hidePluginSyncModal();
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
}
|
||||
parseReplicationResultItem(doc: PouchDB.Core.ExistingDocument<EntryDoc>) {
|
||||
if (isPluginMetadata(doc._id)) {
|
||||
if (this.settings.notifyPluginOrSettingUpdated) {
|
||||
this.triggerCheckPluginUpdate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async beforeReplicate(showMessage: boolean) {
|
||||
if (this.settings.autoSweepPlugins) {
|
||||
await this.sweepPlugin(showMessage);
|
||||
}
|
||||
}
|
||||
async onResume() {
|
||||
if (this.plugin.suspended)
|
||||
return;
|
||||
if (this.settings.autoSweepPlugins) {
|
||||
await this.sweepPlugin(false);
|
||||
}
|
||||
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||
}
|
||||
async onInitializeDatabase(showNotice: boolean) {
|
||||
if (this.settings.usePluginSync) {
|
||||
try {
|
||||
Logger("Scanning plugins...");
|
||||
await this.sweepPlugin(showNotice);
|
||||
Logger("Scanning plugins done");
|
||||
} catch (ex) {
|
||||
Logger("Scanning plugins failed");
|
||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async realizeSettingSyncMode() {
|
||||
this.periodicPluginSweepProcessor?.disable();
|
||||
if (this.plugin.suspended)
|
||||
return;
|
||||
if (this.settings.autoSweepPlugins) {
|
||||
await this.sweepPlugin(false);
|
||||
}
|
||||
this.periodicPluginSweepProcessor.enable(this.settings.autoSweepPluginsPeriodic && !this.settings.watchInternalFileChanges ? (PERIODIC_PLUGIN_SWEEP * 1000) : 0);
|
||||
}
|
||||
|
||||
triggerCheckPluginUpdate() {
|
||||
(async () => await this.checkPluginUpdate())();
|
||||
}
|
||||
|
||||
|
||||
async getPluginList(): Promise<{ plugins: PluginList; allPlugins: DevicePluginList; thisDevicePlugins: DevicePluginList; }> {
|
||||
const docList = await this.localDatabase.allDocsRaw<PluginDataEntry>({ startkey: PSCHeader, endkey: PSCHeaderEnd, include_docs: false });
|
||||
const oldDocs: PluginDataEntry[] = ((await Promise.all(docList.rows.map(async (e) => await this.localDatabase.getDBEntry(e.id as FilePathWithPrefix /* WARN!! THIS SHOULD BE WRAPPED */)))).filter((e) => e !== false) as LoadedEntry[]).map((e) => JSON.parse(getDocData(e.data)));
|
||||
const plugins: { [key: string]: PluginDataEntry[]; } = {};
|
||||
const allPlugins: { [key: string]: PluginDataEntry; } = {};
|
||||
const thisDevicePlugins: { [key: string]: PluginDataEntry; } = {};
|
||||
for (const v of oldDocs) {
|
||||
if (typeof plugins[v.deviceVaultName] === "undefined") {
|
||||
plugins[v.deviceVaultName] = [];
|
||||
}
|
||||
plugins[v.deviceVaultName].push(v);
|
||||
allPlugins[v._id] = v;
|
||||
if (v.deviceVaultName == this.deviceAndVaultName) {
|
||||
thisDevicePlugins[v.manifest.id] = v;
|
||||
}
|
||||
}
|
||||
return { plugins, allPlugins, thisDevicePlugins };
|
||||
}
|
||||
|
||||
async checkPluginUpdate() {
|
||||
if (!this.plugin.settings.usePluginSync)
|
||||
return;
|
||||
await this.sweepPlugin(false);
|
||||
const { allPlugins, thisDevicePlugins } = await this.getPluginList();
|
||||
const arrPlugins = Object.values(allPlugins);
|
||||
let updateFound = false;
|
||||
for (const plugin of arrPlugins) {
|
||||
const ownPlugin = thisDevicePlugins[plugin.manifest.id];
|
||||
if (ownPlugin) {
|
||||
const remoteVersion = versionNumberString2Number(plugin.manifest.version);
|
||||
const ownVersion = versionNumberString2Number(ownPlugin.manifest.version);
|
||||
if (remoteVersion > ownVersion) {
|
||||
updateFound = true;
|
||||
}
|
||||
if (((plugin.mtime / 1000) | 0) > ((ownPlugin.mtime / 1000) | 0) && (plugin.dataJson ?? "") != (ownPlugin.dataJson ?? "")) {
|
||||
updateFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updateFound) {
|
||||
const fragment = createFragment((doc) => {
|
||||
doc.createEl("a", null, (a) => {
|
||||
a.text = "There're some new plugins or their settings";
|
||||
a.addEventListener("click", () => this.showPluginSyncModal());
|
||||
});
|
||||
});
|
||||
NewNotice(fragment, 10000);
|
||||
} else {
|
||||
Logger("Everything is up to date.", LOG_LEVEL_NOTICE);
|
||||
}
|
||||
}
|
||||
|
||||
async sweepPlugin(showMessage = false, specificPluginPath = "") {
|
||||
if (!this.settings.usePluginSync)
|
||||
return;
|
||||
if (!this.localDatabase.isReady)
|
||||
return;
|
||||
// @ts-ignore
|
||||
const pl = this.app.plugins;
|
||||
const manifests: PluginManifest[] = Object.values(pl.manifests);
|
||||
let specificPlugin = "";
|
||||
if (specificPluginPath != "") {
|
||||
specificPlugin = manifests.find(e => e.dir.endsWith("/" + specificPluginPath))?.id ?? "";
|
||||
}
|
||||
await skipIfDuplicated("sweepplugin", async () => {
|
||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
if (!this.deviceAndVaultName) {
|
||||
Logger("You have to set your device name.", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
Logger("Scanning plugins", logLevel);
|
||||
const oldDocs = await this.localDatabase.allDocsRaw<EntryDoc>({
|
||||
startkey: `ps:${this.deviceAndVaultName}-${specificPlugin}`,
|
||||
endkey: `ps:${this.deviceAndVaultName}-${specificPlugin}\u{10ffff}`,
|
||||
include_docs: true,
|
||||
});
|
||||
// Logger("OLD DOCS.", LOG_LEVEL_VERBOSE);
|
||||
// sweep current plugin.
|
||||
const procs = manifests.map(async (m) => {
|
||||
const pluginDataEntryID = `ps:${this.deviceAndVaultName}-${m.id}` as DocumentID;
|
||||
try {
|
||||
if (specificPlugin && m.id != specificPlugin) {
|
||||
return;
|
||||
}
|
||||
Logger(`Reading plugin:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||
const path = normalizePath(m.dir) + "/";
|
||||
const files = ["manifest.json", "main.js", "styles.css", "data.json"];
|
||||
const pluginData: { [key: string]: string; } = {};
|
||||
for (const file of files) {
|
||||
const thePath = path + file;
|
||||
if (await this.plugin.vaultAccess.adapterExists(thePath)) {
|
||||
pluginData[file] = await this.plugin.vaultAccess.adapterRead(thePath);
|
||||
}
|
||||
}
|
||||
let mtime = 0;
|
||||
if (await this.plugin.vaultAccess.adapterExists(path + "/data.json")) {
|
||||
mtime = (await this.plugin.vaultAccess.adapterStat(path + "/data.json")).mtime;
|
||||
}
|
||||
|
||||
const p: PluginDataEntry = {
|
||||
_id: pluginDataEntryID,
|
||||
dataJson: pluginData["data.json"],
|
||||
deviceVaultName: this.deviceAndVaultName,
|
||||
mainJs: pluginData["main.js"],
|
||||
styleCss: pluginData["styles.css"],
|
||||
manifest: m,
|
||||
manifestJson: pluginData["manifest.json"],
|
||||
mtime: mtime,
|
||||
type: "plugin",
|
||||
};
|
||||
const blob = createTextBlob(JSON.stringify(p));
|
||||
const d: SavingEntry = {
|
||||
_id: p._id,
|
||||
path: p._id as string as FilePathWithPrefix,
|
||||
data: blob,
|
||||
ctime: mtime,
|
||||
mtime: mtime,
|
||||
size: blob.size,
|
||||
children: [],
|
||||
datatype: "plain",
|
||||
type: "plain"
|
||||
};
|
||||
Logger(`check diff:${m.name}(${m.id})`, LOG_LEVEL_VERBOSE);
|
||||
await serialized("plugin-" + m.id, async () => {
|
||||
const old = await this.localDatabase.getDBEntry(p._id as string as FilePathWithPrefix /* This also should be explained */, null, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted };
|
||||
const newData = { data: d.data, deleted: d._deleted };
|
||||
if (await isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) {
|
||||
Logger(`Nothing changed:${m.name}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await this.localDatabase.putDBEntry(d);
|
||||
Logger(`Plugin saved:${m.name}`, logLevel);
|
||||
});
|
||||
} catch (ex) {
|
||||
Logger(`Plugin save failed:${m.name}`, LOG_LEVEL_NOTICE);
|
||||
} finally {
|
||||
oldDocs.rows = oldDocs.rows.filter((e) => e.id != pluginDataEntryID);
|
||||
}
|
||||
//remove saved plugin data.
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(procs);
|
||||
|
||||
const delDocs = oldDocs.rows.map((e) => {
|
||||
// e.doc._deleted = true;
|
||||
if (e.doc.type == "newnote" || e.doc.type == "plain") {
|
||||
e.doc.deleted = true;
|
||||
if (this.settings.deleteMetadataOfDeletedFiles) {
|
||||
e.doc._deleted = true;
|
||||
}
|
||||
} else {
|
||||
e.doc._deleted = true;
|
||||
}
|
||||
return e.doc;
|
||||
});
|
||||
Logger(`Deleting old plugin:(${delDocs.length})`, LOG_LEVEL_VERBOSE);
|
||||
await this.localDatabase.bulkDocsRaw(delDocs);
|
||||
Logger(`Scan plugin done.`, logLevel);
|
||||
});
|
||||
}
|
||||
|
||||
async applyPluginData(plugin: PluginDataEntry) {
|
||||
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
// @ts-ignore
|
||||
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
if (plugin.dataJson)
|
||||
await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "data.json", plugin.dataJson);
|
||||
Logger("wrote:" + pluginTargetFolderPath + "data.json", LOG_LEVEL_NOTICE);
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async applyPlugin(plugin: PluginDataEntry) {
|
||||
await serialized("plugin-" + plugin.manifest.id, async () => {
|
||||
// @ts-ignore
|
||||
const stat = this.app.plugins.enabledPlugins.has(plugin.manifest.id) == true;
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.unloadPlugin(plugin.manifest.id);
|
||||
Logger(`Unload plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
|
||||
const pluginTargetFolderPath = normalizePath(plugin.manifest.dir) + "/";
|
||||
if ((await this.plugin.vaultAccess.adapterExists(pluginTargetFolderPath)) === false) {
|
||||
await this.app.vault.adapter.mkdir(pluginTargetFolderPath);
|
||||
}
|
||||
await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "main.js", plugin.mainJs);
|
||||
await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "manifest.json", plugin.manifestJson);
|
||||
if (plugin.styleCss)
|
||||
await this.plugin.vaultAccess.adapterWrite(pluginTargetFolderPath + "styles.css", plugin.styleCss);
|
||||
if (stat) {
|
||||
// @ts-ignore
|
||||
await this.app.plugins.loadPlugin(plugin.manifest.id);
|
||||
Logger(`Load plugin:${plugin.manifest.id}`, LOG_LEVEL_NOTICE);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export class SetupLiveSync extends LiveSyncCommands {
|
||||
const encryptingPassphrase = await askString(this.app, "Encrypt your settings", "The passphrase to encrypt the setup URI", "", true);
|
||||
if (encryptingPassphrase === false)
|
||||
return;
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" };
|
||||
const setting = { ...this.settings, configPassphraseStore: "", encryptedCouchDBConnection: "", encryptedPassphrase: "" } as Partial<ObsidianLiveSyncSettings>;
|
||||
if (stripExtra) {
|
||||
delete setting.pluginSyncExtendedSetting;
|
||||
}
|
||||
@@ -377,9 +377,6 @@ Of course, we are able to disable these features.`
|
||||
await this.plugin.replicateAllFromServer(true);
|
||||
await delay(1000);
|
||||
await this.plugin.replicateAllFromServer(true);
|
||||
// if (!tryLessFetching) {
|
||||
// await this.fetchRemoteChunks();
|
||||
// }
|
||||
await this.resumeReflectingDatabase();
|
||||
await this.askHiddenFileConfiguration({ enableFetch: true });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ function readDocument(w: LoadedEntry) {
|
||||
if (isImage(w.path)) {
|
||||
return new Uint8Array(decodeBinary(w.data));
|
||||
}
|
||||
if (w.data == "plain") return getDocData(w.data);
|
||||
if (w.type == "plain" || w.datatype == "plain") return getDocData(w.data);
|
||||
if (isComparableTextDecode(w.path)) return readString(new Uint8Array(decodeBinary(w.data)));
|
||||
if (isComparableText(w.path)) return getDocData(w.data);
|
||||
try {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ObsidianLiveSyncPlugin from "./main";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { AnyEntry, FilePathWithPrefix } from "./lib/src/types";
|
||||
import { getDocData, isDocContentSame, readAsBlob } from "./lib/src/utils";
|
||||
import { getDocData, isAnyNote, isDocContentSame, readAsBlob } from "./lib/src/utils";
|
||||
import { diff_match_patch } from "./deps";
|
||||
import { DocumentHistoryModal } from "./DocumentHistoryModal";
|
||||
import { isPlainText, stripAllPrefixes } from "./lib/src/path";
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
type HistoryData = {
|
||||
id: string;
|
||||
rev: string;
|
||||
rev?: string;
|
||||
path: string;
|
||||
dirname: string;
|
||||
filename: string;
|
||||
@@ -53,12 +53,12 @@
|
||||
if (docA.mtime < range_from_epoch) {
|
||||
continue;
|
||||
}
|
||||
if (docA.type != "newnote" && docA.type != "plain") continue;
|
||||
if (!isAnyNote(docA)) continue;
|
||||
const path = plugin.getPath(docA as AnyEntry);
|
||||
const isPlain = isPlainText(docA.path);
|
||||
const revs = await db.getRaw(docA._id, { revs_info: true });
|
||||
let p: string = undefined;
|
||||
const reversedRevs = revs._revs_info.reverse();
|
||||
let p: string | undefined = undefined;
|
||||
const reversedRevs = (revs._revs_info ?? []).reverse();
|
||||
const DIFF_DELETE = -1;
|
||||
|
||||
const DIFF_EQUAL = 0;
|
||||
@@ -177,7 +177,7 @@
|
||||
onDestroy(() => {});
|
||||
|
||||
function showHistory(file: string, rev: string) {
|
||||
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, null, rev).open();
|
||||
new DocumentHistoryModal(plugin.app, plugin, file as unknown as FilePathWithPrefix, undefined, rev).open();
|
||||
}
|
||||
function openFile(file: string) {
|
||||
plugin.app.workspace.openLinkText(file, file);
|
||||
@@ -232,7 +232,7 @@
|
||||
<td>
|
||||
<span class="rev">
|
||||
{#if entry.isPlain}
|
||||
<a on:click={() => showHistory(entry.path, entry.rev)}>{entry.rev}</a>
|
||||
<a on:click={() => showHistory(entry.path, entry?.rev || "")}>{entry.rev}</a>
|
||||
{:else}
|
||||
{entry.rev}
|
||||
{/if}
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
import { mergeObject } from "./utils";
|
||||
|
||||
export let docs: LoadedEntry[] = [];
|
||||
export let callback: (keepRev: string, mergedStr?: string) => Promise<void> = async (_, __) => {
|
||||
export let callback: (keepRev?: string, mergedStr?: string) => Promise<void> = async (_, __) => {
|
||||
Promise.resolve();
|
||||
};
|
||||
export let filename: FilePath = "" as FilePath;
|
||||
export let nameA: string = "A";
|
||||
export let nameB: string = "B";
|
||||
export let defaultSelect: string = "";
|
||||
let docA: LoadedEntry = undefined;
|
||||
let docB: LoadedEntry = undefined;
|
||||
let docA: LoadedEntry;
|
||||
let docB: LoadedEntry;
|
||||
let docAContent = "";
|
||||
let docBContent = "";
|
||||
let objA: any = {};
|
||||
@@ -28,7 +28,8 @@
|
||||
function docToString(doc: LoadedEntry) {
|
||||
return doc.datatype == "plain" ? getDocData(doc.data) : readString(new Uint8Array(decodeBinary(doc.data)));
|
||||
}
|
||||
function revStringToRevNumber(rev: string) {
|
||||
function revStringToRevNumber(rev?: string) {
|
||||
if (!rev) return "";
|
||||
return rev.split("-")[0];
|
||||
}
|
||||
|
||||
@@ -44,15 +45,15 @@
|
||||
}
|
||||
function apply() {
|
||||
if (docA._id == docB._id) {
|
||||
if (mode == "A") return callback(docA._rev, null);
|
||||
if (mode == "B") return callback(docB._rev, null);
|
||||
if (mode == "A") return callback(docA._rev!, undefined);
|
||||
if (mode == "B") return callback(docB._rev!, undefined);
|
||||
} else {
|
||||
if (mode == "A") return callback(null, docToString(docA));
|
||||
if (mode == "B") return callback(null, docToString(docB));
|
||||
if (mode == "A") return callback(undefined, docToString(docA));
|
||||
if (mode == "B") return callback(undefined, docToString(docB));
|
||||
}
|
||||
if (mode == "BA") return callback(null, JSON.stringify(objBA, null, 2));
|
||||
if (mode == "AB") return callback(null, JSON.stringify(objAB, null, 2));
|
||||
callback(null, null);
|
||||
if (mode == "BA") return callback(undefined, JSON.stringify(objBA, null, 2));
|
||||
if (mode == "AB") return callback(undefined, JSON.stringify(objAB, null, 2));
|
||||
callback(undefined, undefined);
|
||||
}
|
||||
$: {
|
||||
if (docs && docs.length >= 1) {
|
||||
@@ -133,13 +134,17 @@
|
||||
{/if}
|
||||
<div>
|
||||
{nameA}
|
||||
{#if docA._id == docB._id} Rev:{revStringToRevNumber(docA._rev)} {/if} ,{new Date(docA.mtime).toLocaleString()}
|
||||
{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docA._rev)}
|
||||
{/if} ,{new Date(docA.mtime).toLocaleString()}
|
||||
{docAContent.length} letters
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{nameB}
|
||||
{#if docA._id == docB._id} Rev:{revStringToRevNumber(docB._rev)} {/if} ,{new Date(docB.mtime).toLocaleString()}
|
||||
{#if docA._id == docB._id}
|
||||
Rev:{revStringToRevNumber(docB._rev)}
|
||||
{/if} ,{new Date(docB.mtime).toLocaleString()}
|
||||
{docBContent.length} letters
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface KeyValueDatabase {
|
||||
clear(): Promise<void>;
|
||||
keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise<IDBValidKey[]>;
|
||||
close(): void;
|
||||
destroy(): void;
|
||||
destroy(): Promise<void>;
|
||||
}
|
||||
const databaseCache: { [key: string]: IDBPDatabase<any> } = {};
|
||||
export const OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatabase> => {
|
||||
@@ -20,8 +20,7 @@ export const OpenKeyValueDatabase = async (dbKey: string): Promise<KeyValueDatab
|
||||
db.createObjectStore(storeKey);
|
||||
},
|
||||
});
|
||||
let db: IDBPDatabase<any> = null;
|
||||
db = await dbPromise;
|
||||
const db = await dbPromise;
|
||||
databaseCache[dbKey] = db;
|
||||
return {
|
||||
get<T>(key: string): Promise<T> {
|
||||
|
||||
83
src/MultipleRegExpControl.svelte
Normal file
83
src/MultipleRegExpControl.svelte
Normal file
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
export let patterns = [] as string[];
|
||||
export let originals = [] as string[];
|
||||
|
||||
export let apply: (args: string[]) => Promise<void> = (_: string[]) => Promise.resolve();
|
||||
function revert() {
|
||||
patterns = [...originals];
|
||||
}
|
||||
const CHECK_OK = "✔";
|
||||
const CHECK_NG = "⚠";
|
||||
const MARK_MODIFIED = "✏ ";
|
||||
function checkRegExp(pattern: string) {
|
||||
if (pattern.trim() == "") return "";
|
||||
try {
|
||||
const _ = new RegExp(pattern);
|
||||
return CHECK_OK;
|
||||
} catch (ex) {
|
||||
return CHECK_NG;
|
||||
}
|
||||
}
|
||||
$: status = patterns.map((e) => checkRegExp(e));
|
||||
$: modified = patterns.map((e, i) => (e != originals?.[i] ?? "" ? MARK_MODIFIED : ""));
|
||||
|
||||
function remove(idx: number) {
|
||||
patterns[idx] = "";
|
||||
}
|
||||
function add() {
|
||||
patterns = [...patterns, ""];
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each patterns as pattern, idx}
|
||||
<li><label>{modified[idx]}{status[idx]}</label><input type="text" bind:value={pattern} class={modified[idx]} /><button class="iconbutton" on:click={() => remove(idx)}>🗑</button></li>
|
||||
{/each}
|
||||
<li>
|
||||
<label><button on:click={() => add()}>Add</button></label>
|
||||
</li>
|
||||
<li class="buttons">
|
||||
<button on:click={() => apply(patterns)} disabled={status.some((e) => e == CHECK_NG) || modified.every((e) => e == "")}>Apply</button>
|
||||
<button on:click={() => revert()} disabled={status.some((e) => e == CHECK_NG) || modified.every((e) => e == "")}>Revert</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
label {
|
||||
min-width: 4em;
|
||||
width: 4em;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ul {
|
||||
flex-grow: 1;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
li {
|
||||
padding: var(--size-2-1) var(--size-4-1);
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--size-4-2);
|
||||
}
|
||||
li input {
|
||||
min-width: 10em;
|
||||
}
|
||||
li.buttons {
|
||||
}
|
||||
button.iconbutton {
|
||||
max-width: 4em;
|
||||
}
|
||||
span.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, TextAreaComponent, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO, type LoadedEntry, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED } from "./lib/src/types";
|
||||
import { App, PluginSettingTab, Setting, sanitizeHTMLToDom, MarkdownRenderer, stringifyYaml } from "./deps";
|
||||
import { DEFAULT_SETTINGS, type ObsidianLiveSyncSettings, type ConfigPassphraseStore, type RemoteDBSettings, type FilePathWithPrefix, type HashAlgorithm, type DocumentID, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, LOG_LEVEL_INFO, type LoadedEntry, PREFERRED_SETTING_CLOUDANT, PREFERRED_SETTING_SELF_HOSTED, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR } from "./lib/src/types";
|
||||
import { createBlob, delay, isDocContentSame, readAsBlob } from "./lib/src/utils";
|
||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
@@ -9,6 +9,7 @@ import ObsidianLiveSyncPlugin from "./main";
|
||||
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
||||
import { request, type ButtonComponent, TFile } from "obsidian";
|
||||
import { shouldBeIgnored } from "./lib/src/path";
|
||||
import MultipleRegExpControl from './MultipleRegExpControl.svelte';
|
||||
|
||||
|
||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
@@ -46,11 +47,6 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
let useDynamicIterationCount = this.plugin.settings.useDynamicIterationCount;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
// const preferred_setting = isCloudantURI(this.plugin.settings.couchDB_URI) ? PREFERRED_SETTING_CLOUDANT : PREFERRED_SETTING_SELF_HOSTED;
|
||||
// const default_setting = { ...DEFAULT_SETTINGS };
|
||||
|
||||
|
||||
containerEl.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
|
||||
containerEl.addClass("sls-setting");
|
||||
containerEl.removeClass("isWizard");
|
||||
@@ -1342,43 +1338,48 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
});
|
||||
text.inputEl.setAttribute("type", "number");
|
||||
});
|
||||
let skipPatternTextArea: TextAreaComponent;
|
||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, ^\\.git\\/, \\/obsidian-livesync\\/";
|
||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Folders and files to ignore")
|
||||
.setDesc(
|
||||
"Regular expression, If you use hidden file sync between desktop and mobile, adding `workspace$` is recommended."
|
||||
)
|
||||
.setClass("wizardHidden")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.syncInternalFilesIgnorePatterns)
|
||||
.setPlaceholder("\\/node_modules\\/, \\/\\.git\\/")
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncInternalFilesIgnorePatterns = value;
|
||||
|
||||
const pat = this.plugin.settings.syncInternalFilesIgnorePatterns.split(",").map(x => x.trim()).filter(x => x != "");
|
||||
const patSetting = new Setting(containerSyncSettingEl)
|
||||
.setName("Hidden files ignore patterns")
|
||||
.setDesc("");
|
||||
|
||||
new MultipleRegExpControl(
|
||||
{
|
||||
target: patSetting.controlEl,
|
||||
props: {
|
||||
patterns: pat, originals: [...pat], apply: async (newPatterns) => {
|
||||
this.plugin.settings.syncInternalFilesIgnorePatterns = newPatterns.map(e => e.trim()).filter(e => e != "").join(", ");
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
skipPatternTextArea = text;
|
||||
return text;
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
const addDefaultPatterns = async (patterns: string) => {
|
||||
const oldList = this.plugin.settings.syncInternalFilesIgnorePatterns.split(",").map(x => x.trim()).filter(x => x != "");
|
||||
const newList = patterns.split(",").map(x => x.trim()).filter(x => x != "");
|
||||
const allSet = new Set([...oldList, ...newList]);
|
||||
this.plugin.settings.syncInternalFilesIgnorePatterns = [...allSet].join(", ");
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}
|
||||
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Restore the skip pattern to default")
|
||||
.setName("Add default patterns")
|
||||
.setClass("wizardHidden")
|
||||
.addButton((button) => {
|
||||
button.setButtonText("Default")
|
||||
.onClick(async () => {
|
||||
skipPatternTextArea.setValue(defaultSkipPattern);
|
||||
this.plugin.settings.syncInternalFilesIgnorePatterns = defaultSkipPattern;
|
||||
await this.plugin.saveSettings();
|
||||
await addDefaultPatterns(defaultSkipPattern);
|
||||
})
|
||||
}).addButton((button) => {
|
||||
button.setButtonText("Cross-platform")
|
||||
.onClick(async () => {
|
||||
skipPatternTextArea.setValue(defaultSkipPatternXPlat);
|
||||
this.plugin.settings.syncInternalFilesIgnorePatterns = defaultSkipPatternXPlat;
|
||||
await this.plugin.saveSettings();
|
||||
await addDefaultPatterns(defaultSkipPatternXPlat);
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1430,54 +1431,41 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||
containerSyncSettingEl.createEl("h4", {
|
||||
text: sanitizeHTMLToDom(`Targets`),
|
||||
}).addClass("wizardHidden");
|
||||
new Setting(containerSyncSettingEl)
|
||||
|
||||
const syncFilesSetting = new Setting(containerSyncSettingEl)
|
||||
.setName("Synchronising files")
|
||||
.setDesc("(RegExp) Empty to sync all files. set filter as a regular expression to limit synchronising files.")
|
||||
.setClass("wizardHidden")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.syncOnlyRegEx)
|
||||
.setPlaceholder("\\.md$|\\.txt")
|
||||
.onChange(async (value) => {
|
||||
let isValidRegExp = false;
|
||||
try {
|
||||
new RegExp(value);
|
||||
isValidRegExp = true;
|
||||
} catch (_) {
|
||||
// NO OP.
|
||||
}
|
||||
if (isValidRegExp || value.trim() == "") {
|
||||
this.plugin.settings.syncOnlyRegEx = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
return text;
|
||||
new MultipleRegExpControl(
|
||||
{
|
||||
target: syncFilesSetting.controlEl,
|
||||
props: {
|
||||
patterns: this.plugin.settings.syncOnlyRegEx.split("|[]|"), originals: [...this.plugin.settings.syncOnlyRegEx.split("|[]|")], apply: async (newPatterns) => {
|
||||
this.plugin.settings.syncOnlyRegEx = newPatterns.map(e => e.trim()).filter(e => e != "").join("|[]|");
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
new Setting(containerSyncSettingEl)
|
||||
)
|
||||
|
||||
const nonSyncFilesSetting = new Setting(containerSyncSettingEl)
|
||||
.setName("Non-Synchronising files")
|
||||
.setDesc("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
|
||||
.setClass("wizardHidden")
|
||||
.addTextArea((text) => {
|
||||
text
|
||||
.setValue(this.plugin.settings.syncIgnoreRegEx)
|
||||
.setPlaceholder("\\.pdf$")
|
||||
.onChange(async (value) => {
|
||||
let isValidRegExp = false;
|
||||
try {
|
||||
new RegExp(value);
|
||||
isValidRegExp = true;
|
||||
} catch (_) {
|
||||
// NO OP.
|
||||
}
|
||||
if (isValidRegExp || value.trim() == "") {
|
||||
this.plugin.settings.syncIgnoreRegEx = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
})
|
||||
return text;
|
||||
.setClass("wizardHidden");
|
||||
|
||||
new MultipleRegExpControl(
|
||||
{
|
||||
target: nonSyncFilesSetting.controlEl,
|
||||
props: {
|
||||
patterns: this.plugin.settings.syncIgnoreRegEx.split("|[]|"), originals: [...this.plugin.settings.syncIgnoreRegEx.split("|[]|")], apply: async (newPatterns) => {
|
||||
this.plugin.settings.syncIgnoreRegEx = newPatterns.map(e => e.trim()).filter(e => e != "").join("|[]|");
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
new Setting(containerSyncSettingEl)
|
||||
.setName("Maximum file size")
|
||||
.setDesc("(MB) If this is set, changes to local and remote files that are larger than this will be skipped. If the file becomes smaller again, a newer one will be used.")
|
||||
@@ -2050,7 +2038,7 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
|
||||
const vaultName = new Setting(containerPluginSettings)
|
||||
.setName("Device name")
|
||||
.setDesc("Unique name between all synchronized devices")
|
||||
.setDesc("Unique name between all synchronized devices. To edit this setting, please disable customization sync once.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("desktop")
|
||||
.setValue(this.plugin.deviceAndVaultName)
|
||||
@@ -2173,6 +2161,15 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
.setButtonText("Fetch")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.vaultAccess.vaultCreate(FLAGMD_REDFLAG3_HR, "");
|
||||
this.plugin.performAppReload();
|
||||
})
|
||||
).addButton((button) =>
|
||||
button
|
||||
.setButtonText("Fetch w/o restarting")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await rebuildDB("localOnly");
|
||||
})
|
||||
@@ -2232,10 +2229,21 @@ ${stringifyYaml(pluginConfig)}`;
|
||||
.setButtonText("Rebuild")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await this.plugin.vaultAccess.vaultCreate(FLAGMD_REDFLAG2_HR, "");
|
||||
this.plugin.performAppReload();
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Rebuild w/o restarting")
|
||||
.setWarning()
|
||||
.setDisabled(false)
|
||||
.onClick(async () => {
|
||||
await rebuildDB("rebuildBothByThisDevice");
|
||||
})
|
||||
)
|
||||
|
||||
applyDisplayEnabled();
|
||||
addScreenElement("70", containerMaintenanceEl);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SerializedFileAccess } from "./SerializedFileAccess";
|
||||
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
||||
import { Logger } from "./lib/src/logger";
|
||||
import { shouldBeIgnored } from "./lib/src/path";
|
||||
import type { KeyedQueueProcessor } from "./lib/src/processor";
|
||||
import type { QueueProcessor } from "./lib/src/processor";
|
||||
import { LOG_LEVEL_NOTICE, type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||
import { delay } from "./lib/src/utils";
|
||||
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo } from "./types";
|
||||
@@ -19,7 +19,7 @@ type LiveSyncForStorageEventManager = Plugin &
|
||||
vaultAccess: SerializedFileAccess
|
||||
} & {
|
||||
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
||||
fileEventQueue: KeyedQueueProcessor<FileEventItem, any>,
|
||||
fileEventQueue: QueueProcessor<FileEventItem, any>,
|
||||
isFileSizeExceeded: (size: number) => boolean;
|
||||
};
|
||||
|
||||
@@ -133,8 +133,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
path: file.path,
|
||||
size: file.stat.size
|
||||
} as FileInfo : file as InternalFileInfo;
|
||||
|
||||
this.plugin.fileEventQueue.enqueueWithKey(`file-${fileInfo.path}`, {
|
||||
this.plugin.fileEventQueue.enqueue({
|
||||
type,
|
||||
args: {
|
||||
file: fileInfo,
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 29e23f5763...0d217242a8
394
src/main.ts
394
src/main.ts
@@ -4,7 +4,7 @@ import { type Diff, DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, diff_match_patch, stri
|
||||
import { Notice, Plugin, TFile, addIcon, TFolder, normalizePath, TAbstractFile, Editor, MarkdownView, type RequestUrlParam, type RequestUrlResponse, requestUrl, type MarkdownFileInfo } from "./deps";
|
||||
import { type EntryDoc, type LoadedEntry, type ObsidianLiveSyncSettings, type diff_check_result, type diff_result_leaf, type EntryBody, LOG_LEVEL, VER, DEFAULT_SETTINGS, type diff_result, FLAGMD_REDFLAG, SYNCINFO_ID, SALT_OF_PASSPHRASE, type ConfigPassphraseStore, type CouchDBConnection, FLAGMD_REDFLAG2, FLAGMD_REDFLAG3, PREFIXMD_LOGFILE, type DatabaseConnectingStatus, type EntryHasPath, type DocumentID, type FilePathWithPrefix, type FilePath, type AnyEntry, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_URGENT, LOG_LEVEL_VERBOSE, type SavingEntry, MISSING_OR_ERROR, NOT_CONFLICTED, AUTO_MERGED, CANCELLED, LEAVE_TO_SUBSEQUENT, FLAGMD_REDFLAG2_HR, FLAGMD_REDFLAG3_HR, } from "./lib/src/types";
|
||||
import { type InternalFileInfo, type CacheData, type FileEventItem, FileWatchEventQueueMax } from "./types";
|
||||
import { arrayToChunkedArray, createBlob, fireAndForget, getDocData, isDocContentSame, isObjectDifferent, readContent, sendValue } from "./lib/src/utils";
|
||||
import { arrayToChunkedArray, createBlob, delay, determineTypeFromBlob, fireAndForget, getDocData, isAnyNote, isDocContentSame, isObjectDifferent, readContent, sendValue, throttle } from "./lib/src/utils";
|
||||
import { Logger, setGlobalLogFunction } from "./lib/src/logger";
|
||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||
import { ConflictResolveModal } from "./ConflictResolveModal";
|
||||
@@ -31,7 +31,7 @@ import { GlobalHistoryView, VIEW_TYPE_GLOBAL_HISTORY } from "./GlobalHistoryView
|
||||
import { LogPaneView, VIEW_TYPE_LOG } from "./LogPaneView";
|
||||
import { LRUCache } from "./lib/src/LRUCache";
|
||||
import { SerializedFileAccess } from "./SerializedFileAccess.js";
|
||||
import { KeyedQueueProcessor, QueueProcessor, type QueueItemWithKey } from "./lib/src/processor.js";
|
||||
import { QueueProcessor } from "./lib/src/processor.js";
|
||||
import { reactive, reactiveSource } from "./lib/src/reactive.js";
|
||||
import { initializeStores } from "./stores.js";
|
||||
|
||||
@@ -312,8 +312,9 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
this.replicator = new LiveSyncDBReplicator(this);
|
||||
}
|
||||
async onResetDatabase(db: LiveSyncLocalDB): Promise<void> {
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
localStorage.removeItem(lsKey);
|
||||
const kvDBKey = "queued-files"
|
||||
this.kvDB.del(kvDBKey);
|
||||
// localStorage.removeItem(lsKey);
|
||||
await this.kvDB.destroy();
|
||||
this.kvDB = await OpenKeyValueDatabase(db.dbname + "-livesync-kv");
|
||||
this.replicator = new LiveSyncDBReplicator(this);
|
||||
@@ -381,7 +382,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const notesList = notes.map(e => e.dispPath);
|
||||
const target = await this.askSelectString("File to view History", notesList);
|
||||
if (target) {
|
||||
const targetId = notes.find(e => e.dispPath == target);
|
||||
const targetId = notes.find(e => e.dispPath == target)!;
|
||||
this.showHistory(targetId.path, targetId.id);
|
||||
}
|
||||
}
|
||||
@@ -399,7 +400,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
}
|
||||
const target = await this.askSelectString("File to resolve conflict", notesList);
|
||||
if (target) {
|
||||
const targetItem = notes.find(e => e.dispPath == target);
|
||||
const targetItem = notes.find(e => e.dispPath == target)!;
|
||||
this.resolveConflicted(targetItem.path);
|
||||
await this.conflictCheckQueue.waitForPipeline();
|
||||
return true;
|
||||
@@ -426,7 +427,7 @@ export default class ObsidianLiveSyncPlugin extends Plugin
|
||||
const limit = Date.now() - (86400 * 1000 * limitDays);
|
||||
const notes: { path: string, mtime: number, ttl: number, doc: PouchDB.Core.ExistingDocument<EntryDoc & PouchDB.Core.AllDocsMeta> }[] = [];
|
||||
for await (const doc of this.localDatabase.findAllDocs({ conflicts: true })) {
|
||||
if (doc.type == "newnote" || doc.type == "plain") {
|
||||
if (isAnyNote(doc)) {
|
||||
if (doc.deleted && (doc.mtime - limit) < 0) {
|
||||
notes.push({ path: this.getPath(doc), mtime: doc.mtime, ttl: (doc.mtime - limit) / 1000 / 86400, doc: doc });
|
||||
}
|
||||
@@ -535,7 +536,7 @@ Click anywhere to stop counting down.
|
||||
this.registerWatchEvents();
|
||||
await this.realizeSettingSyncMode();
|
||||
this.swapSaveCommand();
|
||||
if (this.settings.syncOnStart) {
|
||||
if (!this.settings.liveSync && this.settings.syncOnStart) {
|
||||
this.replicator.openReplication(this.settings, false, false);
|
||||
}
|
||||
this.scanStat();
|
||||
@@ -691,7 +692,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
name: "Show history",
|
||||
callback: () => {
|
||||
const file = this.getActiveFile();
|
||||
if (file) this.showHistory(file, null);
|
||||
if (file) this.showHistory(file, undefined);
|
||||
}
|
||||
});
|
||||
this.addCommand({
|
||||
@@ -763,7 +764,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
const ret = this.extractSettingFromWholeText(doc);
|
||||
return ret.body != "";
|
||||
}
|
||||
this.checkAndApplySettingFromMarkdown(ctx.file.path, false);
|
||||
if (ctx.file) this.checkAndApplySettingFromMarkdown(ctx.file.path, false);
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1007,7 +1008,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
}
|
||||
this.deviceAndVaultName = localStorage.getItem(lsKey) || "";
|
||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||
this.fileEventQueue.delay = this.settings.batchSave ? 5000 : 100;
|
||||
this.fileEventQueue.delay = (!this.settings.liveSync && this.settings.batchSave) ? 5000 : 100;
|
||||
}
|
||||
|
||||
async saveSettingData() {
|
||||
@@ -1039,7 +1040,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
}
|
||||
await this.saveData(settings);
|
||||
this.localDatabase.settings = this.settings;
|
||||
this.fileEventQueue.delay = this.settings.batchSave ? 5000 : 100;
|
||||
this.fileEventQueue.delay = (!this.settings.liveSync && this.settings.batchSave) ? 5000 : 100;
|
||||
this.ignoreFiles = this.settings.ignoreFiles.split(",").map(e => e.trim());
|
||||
if (this.settings.settingSyncFile != "") {
|
||||
fireAndForget(() => this.saveSettingToMarkdown(this.settings.settingSyncFile));
|
||||
@@ -1084,7 +1085,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
async checkAndApplySettingFromMarkdown(filename: string, automated?: boolean) {
|
||||
if (automated && !this.settings.notifyAllSettingSyncFile) {
|
||||
if (!this.settings.settingSyncFile || this.settings.settingSyncFile != filename) {
|
||||
Logger(`Setting file (${filename}) is not matched to the current configuration. skipped.`, LOG_LEVEL_VERBOSE);
|
||||
Logger(`Setting file (${filename}) is not matched to the current configuration. skipped.`, LOG_LEVEL_DEBUG);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1147,7 +1148,7 @@ Note: We can always able to read V1 format. It will be progressively converted.
|
||||
})
|
||||
}
|
||||
generateSettingForMarkdown(settings?: ObsidianLiveSyncSettings, keepCredential?: boolean): Partial<ObsidianLiveSyncSettings> {
|
||||
const saveData = { ...(settings ? settings : this.settings) };
|
||||
const saveData = { ...(settings ? settings : this.settings) } as Partial<ObsidianLiveSyncSettings>;
|
||||
delete saveData.encryptedCouchDBConnection;
|
||||
delete saveData.encryptedPassphrase;
|
||||
if (!saveData.writeCredentialsForSettingSync && !keepCredential) {
|
||||
@@ -1237,9 +1238,13 @@ We can perform a command in this file.
|
||||
_this.performCommand('editor:save-file');
|
||||
};
|
||||
}
|
||||
hasFocus = true;
|
||||
isLastHidden = false;
|
||||
registerWatchEvents() {
|
||||
this.registerEvent(this.app.workspace.on("file-open", this.watchWorkspaceOpen));
|
||||
this.registerDomEvent(document, "visibilitychange", this.watchWindowVisibility);
|
||||
this.registerDomEvent(window, "focus", () => this.setHasFocus(true));
|
||||
this.registerDomEvent(window, "blur", () => this.setHasFocus(false));
|
||||
this.registerDomEvent(window, "online", this.watchOnline);
|
||||
this.registerDomEvent(window, "offline", this.watchOnline);
|
||||
}
|
||||
@@ -1255,15 +1260,30 @@ We can perform a command in this file.
|
||||
await this.syncAllFiles();
|
||||
}
|
||||
}
|
||||
setHasFocus(hasFocus: boolean) {
|
||||
this.hasFocus = hasFocus;
|
||||
this.watchWindowVisibility();
|
||||
}
|
||||
watchWindowVisibility() {
|
||||
scheduleTask("watch-window-visibility", 500, () => fireAndForget(() => this.watchWindowVisibilityAsync()));
|
||||
scheduleTask("watch-window-visibility", 100, () => fireAndForget(() => this.watchWindowVisibilityAsync()));
|
||||
}
|
||||
|
||||
async watchWindowVisibilityAsync() {
|
||||
if (this.settings.suspendFileWatching) return;
|
||||
if (!this.settings.isConfigured) return;
|
||||
if (!this.isReady) return;
|
||||
|
||||
if (this.isLastHidden && !this.hasFocus) {
|
||||
// NO OP while non-focused after made hidden;
|
||||
return;
|
||||
}
|
||||
|
||||
const isHidden = document.hidden;
|
||||
if (this.isLastHidden === isHidden) {
|
||||
return;
|
||||
}
|
||||
this.isLastHidden = isHidden;
|
||||
|
||||
await this.applyBatchChange();
|
||||
if (isHidden) {
|
||||
this.replicator.closeReplication();
|
||||
@@ -1283,12 +1303,12 @@ We can perform a command in this file.
|
||||
}
|
||||
|
||||
cancelRelativeEvent(item: FileEventItem) {
|
||||
this.fileEventQueue.modifyQueue((items) => [...items.filter(e => e.entity.key != item.key)])
|
||||
this.fileEventQueue.modifyQueue((items) => [...items.filter(e => e.key != item.key)])
|
||||
}
|
||||
|
||||
queueNextFileEvent(items: QueueItemWithKey<FileEventItem>[], newItem: QueueItemWithKey<FileEventItem>): QueueItemWithKey<FileEventItem>[] {
|
||||
queueNextFileEvent(items: FileEventItem[], newItem: FileEventItem): FileEventItem[] {
|
||||
if (this.settings.batchSave && !this.settings.liveSync) {
|
||||
const file = newItem.entity.args.file;
|
||||
const file = newItem.args.file;
|
||||
// if the latest event is the same type, omit that
|
||||
// a.md MODIFY <- this should be cancelled when a.md MODIFIED
|
||||
// b.md MODIFY <- this should be cancelled when b.md MODIFIED
|
||||
@@ -1300,16 +1320,16 @@ We can perform a command in this file.
|
||||
while (i >= 0) {
|
||||
i--;
|
||||
if (i < 0) break L1;
|
||||
if (items[i].entity.args.file.path != file.path) {
|
||||
if (items[i].args.file.path != file.path) {
|
||||
continue L1;
|
||||
}
|
||||
if (items[i].entity.type != newItem.entity.type) break L1;
|
||||
if (items[i].type != newItem.type) break L1;
|
||||
items.remove(items[i]);
|
||||
}
|
||||
}
|
||||
items.push(newItem);
|
||||
// When deleting or renaming, the queue must be flushed once before processing subsequent processes to prevent unexpected race condition.
|
||||
if (newItem.entity.type == "DELETE" || newItem.entity.type == "RENAME") {
|
||||
if (newItem.type == "DELETE" || newItem.type == "RENAME") {
|
||||
this.fileEventQueue.requestNextFlush();
|
||||
}
|
||||
return items;
|
||||
@@ -1363,7 +1383,7 @@ We can perform a command in this file.
|
||||
pendingFileEventCount = reactiveSource(0);
|
||||
processingFileEventCount = reactiveSource(0);
|
||||
fileEventQueue =
|
||||
new KeyedQueueProcessor(
|
||||
new QueueProcessor(
|
||||
(items: FileEventItem[]) => this.handleFileEvent(items[0]),
|
||||
{ suspended: true, batchSize: 1, concurrentLimit: 5, delay: 100, yieldThreshold: FileWatchEventQueueMax, totalRemainingReactiveSource: this.pendingFileEventCount, processingEntitiesReactiveSource: this.processingFileEventCount }
|
||||
).replaceEnqueueProcessor((items, newItem) => this.queueNextFileEvent(items, newItem));
|
||||
@@ -1404,13 +1424,12 @@ We can perform a command in this file.
|
||||
getFilePath(file: TAbstractFile): string {
|
||||
if (file instanceof TFolder) {
|
||||
if (file.isRoot()) return "";
|
||||
return this.getFilePath(file.parent) + "/" + file.name;
|
||||
return this.getFilePath(file.parent!) + "/" + file.name;
|
||||
}
|
||||
if (file instanceof TFile) {
|
||||
return this.getFilePath(file.parent) + "/" + file.name;
|
||||
return this.getFilePath(file.parent!) + "/" + file.name;
|
||||
}
|
||||
|
||||
return this.getFilePath(file.parent) + "/" + file.name;
|
||||
return this.getFilePath(file.parent!) + "/" + file.name;
|
||||
}
|
||||
|
||||
async watchVaultRenameAsync(file: TFile, oldFile: any, cache?: CacheData) {
|
||||
@@ -1543,7 +1562,7 @@ We can perform a command in this file.
|
||||
await this.deleteVaultItem(file);
|
||||
} else {
|
||||
// Conflict has been resolved at this time,
|
||||
await this.pullFile(path, null, true);
|
||||
await this.pullFile(path, undefined, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1552,8 +1571,8 @@ We can perform a command in this file.
|
||||
|
||||
const doc = existDoc;
|
||||
|
||||
if (doc.datatype != "newnote" && doc.datatype != "plain") {
|
||||
Logger(msg + "ERROR, Invalid datatype: " + path + "(" + doc.datatype + ")", LOG_LEVEL_NOTICE);
|
||||
if (!isAnyNote(doc)) {
|
||||
Logger(msg + "ERROR, Invalid type: " + path + "(" + (doc as any)?.type || "type missing" + ")", LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
// if (!force && localMtime >= docMtime) return;
|
||||
@@ -1600,11 +1619,13 @@ We can perform a command in this file.
|
||||
await this.vaultAccess.delete(file, true);
|
||||
}
|
||||
Logger(`xxx <- STORAGE (deleted) ${file.path}`);
|
||||
Logger(`files: ${dir.children.length}`);
|
||||
if (dir.children.length == 0) {
|
||||
if (!this.settings.doNotDeleteFolder) {
|
||||
Logger(`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`);
|
||||
await this.deleteVaultItem(dir);
|
||||
if (dir) {
|
||||
Logger(`files: ${dir.children.length}`);
|
||||
if (dir.children.length == 0) {
|
||||
if (!this.settings.doNotDeleteFolder) {
|
||||
Logger(`All files under the parent directory (${dir.path}) have been deleted, so delete this one.`);
|
||||
await this.deleteVaultItem(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1621,21 +1642,32 @@ We can perform a command in this file.
|
||||
this.conflictCheckQueue.enqueue(path);
|
||||
}
|
||||
|
||||
_saveQueuedFiles = throttle(() => {
|
||||
const saveData = this.replicationResultProcessor._queue.filter(e => e !== undefined && e !== null).map((e) => e?._id ?? "" as string) as string[];
|
||||
const kvDBKey = "queued-files"
|
||||
// localStorage.setItem(lsKey, saveData);
|
||||
fireAndForget(() => this.kvDB.set(kvDBKey, saveData));
|
||||
}, 100);
|
||||
saveQueuedFiles() {
|
||||
const saveData = JSON.stringify(this.replicationResultProcessor._queue.map((e) => e._id));
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
localStorage.setItem(lsKey, saveData);
|
||||
this._saveQueuedFiles();
|
||||
}
|
||||
async loadQueuedFiles() {
|
||||
if (this.settings.suspendParseReplicationResult) return;
|
||||
if (!this.settings.isConfigured) return;
|
||||
const lsKey = "obsidian-livesync-queuefiles-" + this.getVaultName();
|
||||
const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
|
||||
const kvDBKey = "queued-files"
|
||||
// const ids = [...new Set(JSON.parse(localStorage.getItem(lsKey) || "[]"))] as string[];
|
||||
const ids = [...new Set(await this.kvDB.get<string[]>(kvDBKey) ?? [])];
|
||||
const batchSize = 100;
|
||||
const chunkedIds = arrayToChunkedArray(ids, batchSize);
|
||||
for await (const idsBatch of chunkedIds) {
|
||||
const ret = await this.localDatabase.allDocsRaw<EntryDoc>({ keys: idsBatch, include_docs: true, limit: 100 });
|
||||
this.replicationResultProcessor.enqueueAll(ret.rows.map(doc => doc.doc));
|
||||
const docs = ret.rows.filter(e => e.doc).map(e => e.doc) as PouchDB.Core.ExistingDocument<EntryDoc>[];
|
||||
const errors = ret.rows.filter(e => !e.doc && !e.value.deleted);
|
||||
if (errors.length > 0) {
|
||||
Logger("Some queued processes were not resurrected");
|
||||
Logger(JSON.stringify(errors), LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
this.replicationResultProcessor.enqueueAll(docs);
|
||||
await this.replicationResultProcessor.waitForPipeline();
|
||||
}
|
||||
|
||||
@@ -1643,12 +1675,11 @@ We can perform a command in this file.
|
||||
|
||||
databaseQueueCount = reactiveSource(0);
|
||||
databaseQueuedProcessor = new QueueProcessor(async (docs: EntryBody[]) => {
|
||||
const dbDoc = docs[0];
|
||||
const dbDoc = docs[0] as LoadedEntry; // It has no `data`
|
||||
const path = this.getPath(dbDoc);
|
||||
// If `Read chunks online` is disabled, chunks should be transferred before here.
|
||||
// However, in some cases, chunks are after that. So, if missing chunks exist, we have to wait for them.
|
||||
const datatype = (!("type" in dbDoc) || dbDoc.type == "notes") ? "newnote" : dbDoc.type;
|
||||
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc, datatype, data: [] }, {}, false, true, true);
|
||||
const doc = await this.localDatabase.getDBEntryFromMeta({ ...dbDoc }, {}, false, true, true);
|
||||
if (!doc) {
|
||||
Logger(`Something went wrong while gathering content of ${path} (${dbDoc._id.substring(0, 8)}, ${dbDoc._rev?.substring(0, 10)}) `, LOG_LEVEL_NOTICE)
|
||||
return;
|
||||
@@ -1658,34 +1689,43 @@ We can perform a command in this file.
|
||||
const filename = this.getPathWithoutPrefix(doc);
|
||||
this.isTargetFile(filename).then((ret) => ret ? this.addOnHiddenFileSync.procInternalFile(filename) : Logger(`Skipped (Not target:${filename})`, LOG_LEVEL_VERBOSE));
|
||||
} else if (isValidPath(this.getPath(doc))) {
|
||||
this.storageApplyingProcessor.enqueueWithKey(doc.path, doc);
|
||||
this.storageApplyingProcessor.enqueue(doc);
|
||||
} else {
|
||||
Logger(`Skipped: ${doc._id.substring(0, 8)}`, LOG_LEVEL_VERBOSE);
|
||||
}
|
||||
return;
|
||||
}, { suspended: true, batchSize: 1, concurrentLimit: 10, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.databaseQueueCount }).startPipeline();
|
||||
}, { suspended: true, batchSize: 1, concurrentLimit: 10, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.databaseQueueCount }).replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter(e => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
}).startPipeline();
|
||||
|
||||
storageApplyingCount = reactiveSource(0);
|
||||
storageApplyingProcessor = new KeyedQueueProcessor(async (docs: LoadedEntry[]) => {
|
||||
storageApplyingProcessor = new QueueProcessor(async (docs: LoadedEntry[]) => {
|
||||
const entry = docs[0];
|
||||
const path = this.getPath(entry);
|
||||
Logger(`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, LOG_LEVEL_VERBOSE);
|
||||
const targetFile = this.vaultAccess.getAbstractFileByPath(this.getPathWithoutPrefix(entry));
|
||||
if (targetFile instanceof TFolder) {
|
||||
Logger(`${this.getPath(entry)} is already exist as the folder`);
|
||||
} else {
|
||||
await this.processEntryDoc(entry, targetFile instanceof TFile ? targetFile : undefined);
|
||||
Logger(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`);
|
||||
}
|
||||
await serialized(entry.path, async () => {
|
||||
const path = this.getPath(entry);
|
||||
Logger(`Processing ${path} (${entry._id.substring(0, 8)}: ${entry._rev?.substring(0, 5)}) :Started...`, LOG_LEVEL_VERBOSE);
|
||||
const targetFile = this.vaultAccess.getAbstractFileByPath(this.getPathWithoutPrefix(entry));
|
||||
if (targetFile instanceof TFolder) {
|
||||
Logger(`${this.getPath(entry)} is already exist as the folder`);
|
||||
} else {
|
||||
await this.processEntryDoc(entry, targetFile instanceof TFile ? targetFile : undefined);
|
||||
Logger(`Processing ${path} (${entry._id.substring(0, 8)} :${entry._rev?.substring(0, 5)}) : Done`);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}, { suspended: true, batchSize: 1, concurrentLimit: 2, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.storageApplyingCount }).startPipeline()
|
||||
}, { suspended: true, batchSize: 1, concurrentLimit: 6, yieldThreshold: 1, delay: 0, totalRemainingReactiveSource: this.storageApplyingCount }).replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter(e => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
}).startPipeline()
|
||||
|
||||
|
||||
replicationResultCount = reactiveSource(0);
|
||||
replicationResultProcessor = new QueueProcessor(async (docs: PouchDB.Core.ExistingDocument<EntryDoc>[]) => {
|
||||
if (this.settings.suspendParseReplicationResult) return;
|
||||
const change = docs[0];
|
||||
if (!change) return;
|
||||
if (isChunk(change._id)) {
|
||||
// SendSignal?
|
||||
// this.parseIncomingChunk(change);
|
||||
@@ -1710,7 +1750,7 @@ We can perform a command in this file.
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (change.type == "plain" || change.type == "newnote") {
|
||||
if (isAnyNote(change)) {
|
||||
if (this.databaseQueuedProcessor._isSuspended) {
|
||||
Logger(`Processing scheduled: ${change.path}`, LOG_LEVEL_INFO);
|
||||
}
|
||||
@@ -1722,16 +1762,19 @@ We can perform a command in this file.
|
||||
this.databaseQueuedProcessor.enqueue(change);
|
||||
}
|
||||
return;
|
||||
}, { batchSize: 1, suspended: true, concurrentLimit: 100, delay: 0, totalRemainingReactiveSource: this.replicationResultCount }).startPipeline().onUpdateProgress(() => {
|
||||
}, { batchSize: 1, suspended: true, concurrentLimit: 100, delay: 0, totalRemainingReactiveSource: this.replicationResultCount }).replaceEnqueueProcessor((queue, newItem) => {
|
||||
const q = queue.filter(e => e._id != newItem._id);
|
||||
return [...q, newItem];
|
||||
}).startPipeline().onUpdateProgress(() => {
|
||||
this.saveQueuedFiles();
|
||||
});
|
||||
//---> Sync
|
||||
parseReplicationResult(docs: Array<PouchDB.Core.ExistingDocument<EntryDoc>>) {
|
||||
if (this.settings.suspendParseReplicationResult) {
|
||||
if (this.settings.suspendParseReplicationResult && !this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.suspend()
|
||||
}
|
||||
this.replicationResultProcessor.enqueueAll(docs);
|
||||
if (!this.settings.suspendParseReplicationResult) {
|
||||
if (!this.settings.suspendParseReplicationResult && this.replicationResultProcessor.isSuspended) {
|
||||
this.replicationResultProcessor.resume()
|
||||
}
|
||||
}
|
||||
@@ -1761,8 +1804,33 @@ We can perform a command in this file.
|
||||
lastMessage = "";
|
||||
|
||||
observeForLogs() {
|
||||
const padSpaces = `\u{2007}`.repeat(10);
|
||||
// const emptyMark = `\u{2003}`;
|
||||
const rerenderTimer = new Map<string, [ReturnType<typeof setTimeout>, number]>;
|
||||
const tick = reactiveSource(0);
|
||||
function padLeftSp(num: number, mark: string) {
|
||||
const numLen = `${num}`.length + 1;
|
||||
const [timer, len] = rerenderTimer.get(mark) ?? [undefined, numLen];
|
||||
if (num || timer) {
|
||||
if (num) {
|
||||
if (timer) clearTimeout(timer);
|
||||
rerenderTimer.set(mark, [setTimeout(async () => {
|
||||
rerenderTimer.delete(mark);
|
||||
await delay(100);
|
||||
tick.value = tick.value + 1;
|
||||
}, 3000), Math.max(len, numLen)]);
|
||||
}
|
||||
return ` ${mark}${`${padSpaces}${num}`.slice(-(len))}`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
// const logStore
|
||||
const queueCountLabel = reactive(() => {
|
||||
// For invalidating
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _ = tick.value;
|
||||
const dbCount = this.databaseQueueCount.value;
|
||||
const replicationCount = this.replicationResultCount.value;
|
||||
const storageApplyingCount = this.storageApplyingCount.value;
|
||||
@@ -1770,13 +1838,13 @@ We can perform a command in this file.
|
||||
const pluginScanCount = pluginScanningCount.value;
|
||||
const hiddenFilesCount = hiddenFilesEventCount.value + hiddenFilesProcessingCount.value;
|
||||
const conflictProcessCount = this.conflictProcessQueueCount.value;
|
||||
const labelReplication = replicationCount ? `📥 ${replicationCount} ` : "";
|
||||
const labelDBCount = dbCount ? `📄 ${dbCount} ` : "";
|
||||
const labelStorageCount = storageApplyingCount ? `💾 ${storageApplyingCount}` : "";
|
||||
const labelChunkCount = chunkCount ? `🧩${chunkCount} ` : "";
|
||||
const labelPluginScanCount = pluginScanCount ? `🔌${pluginScanCount} ` : "";
|
||||
const labelHiddenFilesCount = hiddenFilesCount ? `⚙️${hiddenFilesCount} ` : "";
|
||||
const labelConflictProcessCount = conflictProcessCount ? `🔩${conflictProcessCount} ` : "";
|
||||
const labelReplication = padLeftSp(replicationCount, `📥`);
|
||||
const labelDBCount = padLeftSp(dbCount, `📄`);
|
||||
const labelStorageCount = padLeftSp(storageApplyingCount, `💾`);
|
||||
const labelChunkCount = padLeftSp(chunkCount, `🧩`);
|
||||
const labelPluginScanCount = padLeftSp(pluginScanCount, `🔌`);
|
||||
const labelHiddenFilesCount = padLeftSp(hiddenFilesCount, `⚙️`)
|
||||
const labelConflictProcessCount = padLeftSp(conflictProcessCount, `🔩`);
|
||||
return `${labelReplication}${labelDBCount}${labelStorageCount}${labelChunkCount}${labelPluginScanCount}${labelHiddenFilesCount}${labelConflictProcessCount}`;
|
||||
})
|
||||
const requestingStatLabel = reactive(() => {
|
||||
@@ -1821,11 +1889,15 @@ We can perform a command in this file.
|
||||
return { w, sent, pushLast, arrived, pullLast };
|
||||
})
|
||||
const waitingLabel = reactive(() => {
|
||||
// For invalidating
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _ = tick.value;
|
||||
const e = this.pendingFileEventCount.value;
|
||||
const proc = this.processingFileEventCount.value;
|
||||
const pend = e - proc;
|
||||
const labelProc = proc != 0 ? `⏳${proc} ` : "";
|
||||
const labelPend = pend != 0 ? ` 🛫${pend}` : "";
|
||||
const labelProc = padLeftSp(proc, `⏳`);
|
||||
const labelPend = padLeftSp(pend, `🛫`);
|
||||
return `${labelProc}${labelPend}`;
|
||||
})
|
||||
const statusLineLabel = reactive(() => {
|
||||
@@ -1834,7 +1906,7 @@ We can perform a command in this file.
|
||||
const waiting = waitingLabel.value;
|
||||
const networkActivity = requestingStatLabel.value;
|
||||
return {
|
||||
message: `${networkActivity}Sync: ${w} ↑${sent}${pushLast} ↓${arrived}${pullLast}${waiting} ${queued}`,
|
||||
message: `${networkActivity}Sync: ${w} ↑ ${sent}${pushLast} ↓ ${arrived}${pullLast}${waiting}${queued}`,
|
||||
};
|
||||
})
|
||||
const statusBarLabels = reactive(() => {
|
||||
@@ -1845,31 +1917,20 @@ We can perform a command in this file.
|
||||
message, status
|
||||
}
|
||||
})
|
||||
let last = 0;
|
||||
const applyToDisplay = () => {
|
||||
|
||||
const applyToDisplay = throttle(() => {
|
||||
const v = statusBarLabels.value;
|
||||
const now = Date.now();
|
||||
if (now - last < 10) {
|
||||
scheduleTask("applyToDisplay", 20, () => applyToDisplay());
|
||||
return;
|
||||
}
|
||||
this.applyStatusBarText(v.message, v.status);
|
||||
last = now;
|
||||
}
|
||||
|
||||
}, 20);
|
||||
statusBarLabels.onChanged(applyToDisplay);
|
||||
}
|
||||
|
||||
applyStatusBarText(message: string, log: string) {
|
||||
const newMsg = message;
|
||||
const newLog = log;
|
||||
// scheduleTask("update-display", 50, () => {
|
||||
const newMsg = message.replace(/\n/g, "\\A ");
|
||||
const newLog = log.replace(/\n/g, "\\A ");
|
||||
|
||||
this.statusBar?.setText(newMsg.split("\n")[0]);
|
||||
// const selector = `.CodeMirror-wrap,` +
|
||||
// `.markdown-preview-view.cm-s-obsidian,` +
|
||||
// `.markdown-source-view.cm-s-obsidian,` +
|
||||
// `.canvas-wrapper,` +
|
||||
// `.empty-state`
|
||||
// ;
|
||||
if (this.settings.showStatusOnEditor) {
|
||||
const root = activeDocument.documentElement;
|
||||
root.style.setProperty("--sls-log-text", "'" + (newMsg + "\\A " + newLog) + "'");
|
||||
@@ -1877,13 +1938,12 @@ We can perform a command in this file.
|
||||
// const root = activeDocument.documentElement;
|
||||
// root.style.setProperty("--log-text", "'" + (newMsg + "\\A " + newLog) + "'");
|
||||
}
|
||||
// }, true);
|
||||
|
||||
|
||||
scheduleTask("log-hide", 3000, () => { this.statusLog.value = "" });
|
||||
}
|
||||
|
||||
async replicate(showMessage?: boolean) {
|
||||
async replicate(showMessage: boolean = false) {
|
||||
if (!this.isReady) return;
|
||||
if (isLockAcquired("cleanup")) {
|
||||
Logger("Database cleaning up is in process. replication has been cancelled", LOG_LEVEL_NOTICE);
|
||||
@@ -1956,7 +2016,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
return ret;
|
||||
}
|
||||
|
||||
async initializeDatabase(showingNotice?: boolean, reopenDatabase = true) {
|
||||
async initializeDatabase(showingNotice: boolean = false, reopenDatabase = true) {
|
||||
this.isReady = false;
|
||||
if ((!reopenDatabase) || await this.openDatabase()) {
|
||||
if (this.localDatabase.isReady) {
|
||||
@@ -1974,17 +2034,17 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
}
|
||||
|
||||
async replicateAllToServer(showingNotice?: boolean) {
|
||||
async replicateAllToServer(showingNotice: boolean = false) {
|
||||
if (!this.isReady) return false;
|
||||
await Promise.all(this.addOns.map(e => e.beforeReplicate(showingNotice)));
|
||||
return await this.replicator.replicateAllToServer(this.settings, showingNotice);
|
||||
}
|
||||
async replicateAllFromServer(showingNotice?: boolean) {
|
||||
async replicateAllFromServer(showingNotice: boolean = false) {
|
||||
if (!this.isReady) return false;
|
||||
return await this.replicator.replicateAllFromServer(this.settings, showingNotice);
|
||||
}
|
||||
|
||||
async markRemoteLocked(lockByClean?: boolean) {
|
||||
async markRemoteLocked(lockByClean: boolean = false) {
|
||||
return await this.replicator.markRemoteLocked(this.settings, true, lockByClean);
|
||||
}
|
||||
|
||||
@@ -2053,9 +2113,15 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
|
||||
const syncFiles = filesStorage.filter((e) => onlyInStorageNames.indexOf(e.path) == -1);
|
||||
Logger("Updating database by new files");
|
||||
const processStatus = {} as Record<string, string>;
|
||||
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
const updateLog = throttle((key: string, msg: string) => {
|
||||
processStatus[key] = msg;
|
||||
const log = Object.values(processStatus).join("\n");
|
||||
Logger(log, logLevel, "syncAll");
|
||||
}, 25);
|
||||
|
||||
const initProcess = [];
|
||||
const logLevel = showingNotice ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
||||
const runAll = async<T>(procedureName: string, objects: T[], callback: (arg: T) => Promise<void>) => {
|
||||
if (objects.length == 0) {
|
||||
Logger(`${procedureName}: Nothing to do`);
|
||||
@@ -2077,12 +2143,14 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
failed++;
|
||||
}
|
||||
if ((success + failed) % step == 0) {
|
||||
Logger(`${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`, logLevel, `log-${procedureName}`);
|
||||
const msg = `${procedureName}: DONE:${success}, FAILED:${failed}, LAST:${processor._queue.length}`;
|
||||
updateLog(procedureName, msg);
|
||||
}
|
||||
return;
|
||||
}, { batchSize: 1, concurrentLimit: 10, delay: 0, suspended: true }, objects)
|
||||
await processor.waitForPipeline();
|
||||
Logger(`${procedureName} All done: DONE:${success}, FAILED:${failed}`, logLevel, `log-${procedureName}`);
|
||||
const msg = `${procedureName} All done: DONE:${success}, FAILED:${failed}`;
|
||||
updateLog(procedureName, msg)
|
||||
}
|
||||
initProcess.push(runAll("UPDATE DATABASE", onlyInStorage, async (e) => {
|
||||
if (!this.isFileSizeExceeded(e.stat.size)) {
|
||||
@@ -2096,7 +2164,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const w = await this.localDatabase.getDBEntryMeta(e, {}, true);
|
||||
if (w && !(w.deleted || w._deleted)) {
|
||||
if (!this.isFileSizeExceeded(w.size)) {
|
||||
await this.pullFile(e, filesStorage, false, null, false);
|
||||
await this.pullFile(e, filesStorage, false, undefined, false);
|
||||
fireAndForget(() => this.checkAndApplySettingFromMarkdown(e, true));
|
||||
Logger(`Check or pull from db:${e} OK`);
|
||||
} else {
|
||||
@@ -2116,7 +2184,6 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const id = await this.path2id(getPathFromTFile(file));
|
||||
const pair: FileDocPair = { file, id };
|
||||
return [pair];
|
||||
// processSyncFile.enqueue(pair);
|
||||
}
|
||||
, { batchSize: 1, concurrentLimit: 10, delay: 0, suspended: true }, syncFiles);
|
||||
processPrepareSyncFile
|
||||
@@ -2138,10 +2205,18 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}, { batchSize: 1, concurrentLimit: 5, delay: 10, suspended: false }
|
||||
))
|
||||
|
||||
processPrepareSyncFile.startPipeline();
|
||||
initProcess.push(async () => {
|
||||
await processPrepareSyncFile.waitForPipeline();
|
||||
})
|
||||
const allSyncFiles = syncFiles.length;
|
||||
let lastRemain = allSyncFiles;
|
||||
const step = 25;
|
||||
const remainLog = (remain: number) => {
|
||||
if (lastRemain - remain > step) {
|
||||
const msg = ` CHECK AND SYNC: ${remain} / ${allSyncFiles}`;
|
||||
updateLog("sync", msg);
|
||||
lastRemain = remain;
|
||||
}
|
||||
}
|
||||
processPrepareSyncFile.startPipeline().onUpdateProgress(() => remainLog(processPrepareSyncFile.totalRemaining + processPrepareSyncFile.nowProcessing))
|
||||
initProcess.push(processPrepareSyncFile.waitForPipeline());
|
||||
await Promise.all(initProcess);
|
||||
|
||||
// this.setStatusBarText(`NOW TRACKING!`);
|
||||
@@ -2414,11 +2489,11 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const conflictedRevNo = Number(conflictedRev.split("-")[0]);
|
||||
//Search
|
||||
const revFrom = (await this.localDatabase.getRaw<EntryDoc>(await this.path2id(path), { revs_info: true }));
|
||||
const commonBase = revFrom._revs_info.filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
|
||||
const commonBase = (revFrom._revs_info || []).filter(e => e.status == "available" && Number(e.rev.split("-")[0]) < conflictedRevNo).first()?.rev ?? "";
|
||||
let p = undefined;
|
||||
if (commonBase) {
|
||||
if (isSensibleMargeApplicable(path)) {
|
||||
const result = await this.mergeSensibly(path, commonBase, test._rev, conflictedRev);
|
||||
const result = await this.mergeSensibly(path, commonBase, test._rev!, conflictedRev);
|
||||
if (result) {
|
||||
p = result.filter(e => e[0] != DIFF_DELETE).map((e) => e[1]).join("");
|
||||
// can be merged.
|
||||
@@ -2428,7 +2503,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
} else if (isObjectMargeApplicable(path)) {
|
||||
// can be merged.
|
||||
const result = await this.mergeObject(path, commonBase, test._rev, conflictedRev);
|
||||
const result = await this.mergeObject(path, commonBase, test._rev!, conflictedRev);
|
||||
if (result) {
|
||||
Logger(`Object merge:${path}`, LOG_LEVEL_INFO);
|
||||
p = result;
|
||||
@@ -2457,7 +2532,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
}
|
||||
// should be one or more conflicts;
|
||||
const leftLeaf = await this.getConflictedDoc(path, test._rev);
|
||||
const leftLeaf = await this.getConflictedDoc(path, test._rev!);
|
||||
const rightLeaf = await this.getConflictedDoc(path, conflicts[0]);
|
||||
if (leftLeaf == false) {
|
||||
// what's going on..
|
||||
@@ -2467,7 +2542,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
if (rightLeaf == false) {
|
||||
// Conflicted item could not load, delete this.
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: conflicts[0] });
|
||||
await this.pullFile(path, null, true);
|
||||
await this.pullFile(path, undefined, true);
|
||||
Logger(`could not get old revisions, automatically used newer one:${path}`, LOG_LEVEL_NOTICE);
|
||||
return AUTO_MERGED;
|
||||
}
|
||||
@@ -2483,7 +2558,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
loser = rightLeaf;
|
||||
}
|
||||
await this.localDatabase.deleteDBEntry(path, { rev: loser.rev });
|
||||
await this.pullFile(path, null, true);
|
||||
await this.pullFile(path, undefined, true);
|
||||
Logger(`Automatically merged (${isSame ? "same," : ""}${isBinary ? "binary," : ""}${alwaysNewer ? "alwaysNewer" : ""}) :${path}`, LOG_LEVEL_NOTICE);
|
||||
return AUTO_MERGED;
|
||||
}
|
||||
@@ -2501,38 +2576,39 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
|
||||
conflictProcessQueueCount = reactiveSource(0);
|
||||
conflictResolveQueue =
|
||||
new KeyedQueueProcessor(async (entries: { filename: FilePathWithPrefix }[]) => {
|
||||
const entry = entries[0];
|
||||
const filename = entry.filename;
|
||||
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
|
||||
if (conflictCheckResult === MISSING_OR_ERROR || conflictCheckResult === NOT_CONFLICTED || conflictCheckResult === CANCELLED) {
|
||||
// nothing to do.
|
||||
return;
|
||||
}
|
||||
if (conflictCheckResult === AUTO_MERGED) {
|
||||
//auto resolved, but need check again;
|
||||
if (this.settings.syncAfterMerge && !this.suspended) {
|
||||
//Wait for the running replication, if not running replication, run it once.
|
||||
await shareRunningResult(`replication`, () => this.replicate());
|
||||
}
|
||||
Logger("conflict:Automatically merged, but we have to check it again");
|
||||
this.conflictCheckQueue.enqueue(filename);
|
||||
return;
|
||||
}
|
||||
if (this.settings.showMergeDialogOnlyOnActive) {
|
||||
const af = this.getActiveFile();
|
||||
if (af && af.path != filename) {
|
||||
Logger(`${filename} is conflicted. Merging process has been postponed to the file have got opened.`, LOG_LEVEL_NOTICE);
|
||||
new QueueProcessor(async (filenames: FilePathWithPrefix[]) => {
|
||||
const filename = filenames[0];
|
||||
await serialized(`conflict-resolve:${filename}`, async () => {
|
||||
const conflictCheckResult = await this.checkConflictAndPerformAutoMerge(filename);
|
||||
if (conflictCheckResult === MISSING_OR_ERROR || conflictCheckResult === NOT_CONFLICTED || conflictCheckResult === CANCELLED) {
|
||||
// nothing to do.
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger("conflict:Manual merge required!");
|
||||
await this.resolveConflictByUI(filename, conflictCheckResult);
|
||||
if (conflictCheckResult === AUTO_MERGED) {
|
||||
//auto resolved, but need check again;
|
||||
if (this.settings.syncAfterMerge && !this.suspended) {
|
||||
//Wait for the running replication, if not running replication, run it once.
|
||||
await shareRunningResult(`replication`, () => this.replicate());
|
||||
}
|
||||
Logger("conflict:Automatically merged, but we have to check it again");
|
||||
this.conflictCheckQueue.enqueue(filename);
|
||||
return;
|
||||
}
|
||||
if (this.settings.showMergeDialogOnlyOnActive) {
|
||||
const af = this.getActiveFile();
|
||||
if (af && af.path != filename) {
|
||||
Logger(`${filename} is conflicted. Merging process has been postponed to the file have got opened.`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger("conflict:Manual merge required!");
|
||||
await this.resolveConflictByUI(filename, conflictCheckResult);
|
||||
});
|
||||
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false }).replaceEnqueueProcessor(
|
||||
(queue, newEntity) => {
|
||||
const filename = newEntity.entity.filename;
|
||||
const filename = newEntity;
|
||||
sendValue("cancel-resolve-conflict:" + filename, true);
|
||||
const newQueue = [...queue].filter(e => e.key != newEntity.key);
|
||||
const newQueue = [...queue].filter(e => e != newEntity);
|
||||
return [...newQueue, newEntity];
|
||||
});
|
||||
|
||||
@@ -2544,10 +2620,9 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const file = this.vaultAccess.getAbstractFileByPath(filename);
|
||||
// if (!file) return;
|
||||
// if (!(file instanceof TFile)) return;
|
||||
if ((file instanceof TFolder)) return;
|
||||
if ((file instanceof TFolder)) return [];
|
||||
// Check again?
|
||||
|
||||
return [{ key: filename, entity: { filename } }];
|
||||
return [filename];
|
||||
// this.conflictResolveQueue.enqueueWithKey(filename, { filename, file });
|
||||
}, {
|
||||
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, pipeTo: this.conflictResolveQueue, totalRemainingReactiveSource: this.conflictProcessQueueCount
|
||||
@@ -2561,16 +2636,16 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
if (selected === CANCELLED) {
|
||||
// Cancelled by UI, or another conflict.
|
||||
Logger(`Merge: Cancelled ${filename}`, LOG_LEVEL_INFO);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const testDoc = await this.localDatabase.getDBEntry(filename, { conflicts: true }, false, false, true);
|
||||
if (testDoc === false) {
|
||||
Logger(`Merge: Could not read ${filename} from the local database`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!testDoc._conflicts) {
|
||||
Logger(`Merge: Nothing to do ${filename}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const toDelete = selected;
|
||||
const toKeep = conflictCheckResult.left.rev != toDelete ? conflictCheckResult.left.rev : conflictCheckResult.right.rev;
|
||||
@@ -2592,11 +2667,11 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
Logger(`Merge: Changes has been concatenated: ${filename}`);
|
||||
} else if (typeof toDelete === "string") {
|
||||
await this.localDatabase.deleteDBEntry(filename, { rev: toDelete });
|
||||
await this.pullFile(filename, null, true, toKeep);
|
||||
await this.pullFile(filename, undefined, true, toKeep);
|
||||
Logger(`Conflict resolved:${filename}`);
|
||||
} else {
|
||||
Logger(`Merge: Something went wrong: ${filename}, (${toDelete})`, LOG_LEVEL_NOTICE);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// In here, some merge has been processed.
|
||||
// So we have to run replication if configured.
|
||||
@@ -2605,6 +2680,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
// And, check it again.
|
||||
this.conflictCheckQueue.enqueue(filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
async pullFile(filename: FilePathWithPrefix, fileList?: TFile[], force?: boolean, rev?: string, waitForReady = true) {
|
||||
@@ -2612,7 +2688,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
if (!await this.isTargetFile(filename)) return;
|
||||
if (targetFile == null) {
|
||||
//have to create;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : undefined, false, waitForReady);
|
||||
if (doc === false) {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
@@ -2621,7 +2697,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
} else if (targetFile instanceof TFile) {
|
||||
//normal case
|
||||
const file = targetFile;
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : null, false, waitForReady);
|
||||
const doc = await this.localDatabase.getDBEntry(filename, rev ? { rev: rev } : undefined, false, waitForReady);
|
||||
if (doc === false) {
|
||||
Logger(`${filename} Skipped`);
|
||||
return;
|
||||
@@ -2661,7 +2737,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
case TARGET_IS_NEW:
|
||||
if (!this.isFileSizeExceeded(doc.size)) {
|
||||
Logger("STORAGE <- DB :" + file.path);
|
||||
const docx = await this.localDatabase.getDBEntry(getPathFromTFile(file), null, false, false, true);
|
||||
const docx = await this.localDatabase.getDBEntry(getPathFromTFile(file), undefined, false, false, true);
|
||||
if (docx != false) {
|
||||
await this.processEntryDoc(docx, file);
|
||||
} else {
|
||||
@@ -2673,7 +2749,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
break;
|
||||
case EVEN:
|
||||
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL_VERBOSE);
|
||||
Logger("STORAGE == DB :" + file.path + "", LOG_LEVEL_DEBUG);
|
||||
break;
|
||||
default:
|
||||
Logger("STORAGE ?? DB :" + file.path + " Something got weird");
|
||||
@@ -2687,22 +2763,12 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
return true;
|
||||
}
|
||||
// let content: Blob;
|
||||
// let datatype: "plain" | "newnote" = "newnote";
|
||||
const isPlain = isPlainText(file.name);
|
||||
const possiblyLarge = !isPlain;
|
||||
// if (!cache) {
|
||||
if (possiblyLarge) Logger(`Reading : ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
const content = createBlob(await this.vaultAccess.vaultReadAuto(file));
|
||||
const datatype = isPlain ? "plain" : "newnote";
|
||||
// }
|
||||
// else if (cache instanceof ArrayBuffer) {
|
||||
// Logger(`Cache Reading: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
// content = createBinaryBlob(cache);
|
||||
// datatype = "newnote"
|
||||
// } else {
|
||||
// content = createTextBlob(cache);
|
||||
// datatype = "plain";
|
||||
// }
|
||||
const datatype = determineTypeFromBlob(content);
|
||||
if (possiblyLarge) Logger(`Processing: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
const fullPath = getPathFromTFile(file);
|
||||
const id = await this.path2id(fullPath);
|
||||
@@ -2724,7 +2790,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const old = await this.localDatabase.getDBEntry(fullPath, null, false, false);
|
||||
const old = await this.localDatabase.getDBEntry(fullPath, undefined, false, false);
|
||||
if (old !== false) {
|
||||
const oldData = { data: old.data, deleted: old._deleted || old.deleted };
|
||||
const newData = { data: d.data, deleted: d._deleted || d.deleted };
|
||||
@@ -2800,7 +2866,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const id = await this.path2id(path);
|
||||
const doc = await this.localDatabase.getRaw<AnyEntry>(id, { conflicts: true });
|
||||
// If there is no conflict, return with false.
|
||||
if (!("_conflicts" in doc)) return false;
|
||||
if (!("_conflicts" in doc) || doc._conflicts === undefined) return false;
|
||||
if (doc._conflicts.length == 0) return false;
|
||||
Logger(`Hidden file conflicted:${this.getPath(doc)}`);
|
||||
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
||||
@@ -2833,7 +2899,7 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
}
|
||||
async getIgnoreFile(path: string) {
|
||||
if (this.ignoreFileCache.has(path)) {
|
||||
return this.ignoreFileCache.get(path);
|
||||
return this.ignoreFileCache.get(path) ?? false;
|
||||
} else {
|
||||
return await this.readIgnoreFile(path);
|
||||
}
|
||||
@@ -2909,9 +2975,9 @@ Or if you are sure know what had been happened, we can unlock the database from
|
||||
const fragment = createFragment((doc) => {
|
||||
|
||||
const [beforeText, afterText] = dialogText.split("{HERE}", 2);
|
||||
doc.createEl("span", null, (a) => {
|
||||
doc.createEl("span", undefined, (a) => {
|
||||
a.appendText(beforeText);
|
||||
a.appendChild(a.createEl("a", null, (anchor) => {
|
||||
a.appendChild(a.createEl("a", undefined, (anchor) => {
|
||||
anchorCallback(anchor);
|
||||
}));
|
||||
|
||||
|
||||
@@ -451,7 +451,11 @@ export function isMarkedAsSameChanges(file: TFile | AnyEntry | string, mtimes: n
|
||||
return EVEN;
|
||||
}
|
||||
}
|
||||
export function compareFileFreshness(baseFile: TFile | AnyEntry, checkTarget: TFile | AnyEntry): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
|
||||
export function compareFileFreshness(baseFile: TFile | AnyEntry | undefined, checkTarget: TFile | AnyEntry | undefined): typeof BASE_IS_NEW | typeof TARGET_IS_NEW | typeof EVEN {
|
||||
if (baseFile === undefined && checkTarget == undefined) return EVEN;
|
||||
if (baseFile == undefined) return TARGET_IS_NEW;
|
||||
if (checkTarget == undefined) return BASE_IS_NEW;
|
||||
|
||||
const modifiedBase = baseFile instanceof TFile ? baseFile?.stat?.mtime ?? 0 : baseFile?.mtime ?? 0;
|
||||
const modifiedTarget = checkTarget instanceof TFile ? checkTarget?.stat?.mtime ?? 0 : checkTarget?.mtime ?? 0;
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@
|
||||
.canvas-wrapper::before,
|
||||
.empty-state::before {
|
||||
content: var(--sls-log-text, "");
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-variant-emoji: emoji;
|
||||
tab-size: 4;
|
||||
text-align: right;
|
||||
white-space: pre-wrap;
|
||||
position: absolute;
|
||||
|
||||
66
updates.md
66
updates.md
@@ -10,43 +10,37 @@ Note: we got a very performance improvement.
|
||||
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
|
||||
|
||||
#### Version history
|
||||
- 0.22.14:
|
||||
- New feature:
|
||||
- We can disable the status bar in the setting dialogue.
|
||||
- Improved:
|
||||
- Now some files are handled as correct data type.
|
||||
- Customisation sync now uses the digest of each file for better performance.
|
||||
- The status in the Editor now works performant.
|
||||
- Refactored:
|
||||
- Common functions have been ready and the codebase has been organised.
|
||||
- Stricter type checking following TypeScript updates.
|
||||
- Remove old iOS workaround for simplicity and performance.
|
||||
- 0.22.13:
|
||||
- 0.22.17:
|
||||
- Fixed:
|
||||
- Error handling on booting now works fine.
|
||||
- Replication is now started automatically in LiveSync mode.
|
||||
- Batch database update is now disabled in LiveSync mode.
|
||||
- No longer automatically reconnection while off-focused.
|
||||
- Status saves are thinned out.
|
||||
- Now Self-hosted LiveSync waits for all files between the local database and storage to be surely checked.
|
||||
- Improved:
|
||||
- Now using HTTP for the remote database URI warns of an error (on mobile) or notice (on desktop).
|
||||
- The job scheduler is now more robust and stable.
|
||||
- The status indicator no longer flickers and keeps zero for a while.
|
||||
- No longer meaningless frequent updates of status indicators.
|
||||
- Now we can configure regular expression filters in handy UI. Thank you so much, @eth-p!
|
||||
- `Fetch` or `Rebuild everything` is now more safely performed.
|
||||
- Minor things
|
||||
- Some utility function has been added.
|
||||
- Customisation sync now less wrong messages.
|
||||
- Digging the weeds for eradication of type errors.
|
||||
- 0.22.16:
|
||||
- Fixed:
|
||||
- Fixed the issue that binary files were sometimes corrupted.
|
||||
- Fixed customisation sync data could be corrupted.
|
||||
- Improved:
|
||||
- Now the remote database costs lower memory.
|
||||
- This release requires a brief wait on the first synchronisation, to track the latest changeset again.
|
||||
- Description added for the `Device name`.
|
||||
- Refactored:
|
||||
- Dependencies have been polished.
|
||||
- 0.22.12:
|
||||
- Changed:
|
||||
- The default settings has been changed.
|
||||
- Many type-errors have been resolved.
|
||||
- Obsolete file has been deleted.
|
||||
- 0.22.15:
|
||||
- Improved:
|
||||
- Default and preferred settings are applied on completion of the wizard.
|
||||
- Fixed:
|
||||
- Now Initialisation `Fetch` will be performed smoothly and there will be fewer conflicts.
|
||||
- No longer stuck while Handling transferred or initialised documents.
|
||||
- 0.22.11:
|
||||
- Fixed:
|
||||
- `Verify and repair all files` is no longer broken.
|
||||
- New feature:
|
||||
- Now `Verify and repair all files` is able to...
|
||||
- Restore if the file only in the local database.
|
||||
- Show the history.
|
||||
- Improved:
|
||||
- Performance improved.
|
||||
- 0.22.10
|
||||
- Fixed:
|
||||
- No longer unchanged hidden files and customisations are saved and transferred now.
|
||||
- File integrity of vault history indicates the integrity correctly.
|
||||
- Improved:
|
||||
- In the report, the schema of the remote database URI is now printed.
|
||||
- Faster start-up by removing too many logs which indicates normality
|
||||
- By streamlined scanning of customised synchronisation extra phases have been deleted.
|
||||
... To continue on to `updates_old.md`.
|
||||
@@ -10,6 +10,45 @@ Note: we got a very performance improvement.
|
||||
Note at 0.22.2: **Now, to rescue mobile devices, Maximum file size is set to 50 by default**. Please configure the limit as you need. If you do not want to limit the sizes, set zero manually, please.
|
||||
|
||||
#### Version history
|
||||
- 0.22.14:
|
||||
- New feature:
|
||||
- We can disable the status bar in the setting dialogue.
|
||||
- Improved:
|
||||
- Now some files are handled as correct data type.
|
||||
- Customisation sync now uses the digest of each file for better performance.
|
||||
- The status in the Editor now works performant.
|
||||
- Refactored:
|
||||
- Common functions have been ready and the codebase has been organised.
|
||||
- Stricter type checking following TypeScript updates.
|
||||
- Remove old iOS workaround for simplicity and performance.
|
||||
- 0.22.13:
|
||||
- Improved:
|
||||
- Now using HTTP for the remote database URI warns of an error (on mobile) or notice (on desktop).
|
||||
- Refactored:
|
||||
- Dependencies have been polished.
|
||||
- 0.22.12:
|
||||
- Changed:
|
||||
- The default settings has been changed.
|
||||
- Improved:
|
||||
- Default and preferred settings are applied on completion of the wizard.
|
||||
- Fixed:
|
||||
- Now Initialisation `Fetch` will be performed smoothly and there will be fewer conflicts.
|
||||
- No longer stuck while Handling transferred or initialised documents.
|
||||
- 0.22.11:
|
||||
- Fixed:
|
||||
- `Verify and repair all files` is no longer broken.
|
||||
- New feature:
|
||||
- Now `Verify and repair all files` is able to...
|
||||
- Restore if the file only in the local database.
|
||||
- Show the history.
|
||||
- Improved:
|
||||
- Performance improved.
|
||||
- 0.22.10
|
||||
- Fixed:
|
||||
- No longer unchanged hidden files and customisations are saved and transferred now.
|
||||
- File integrity of vault history indicates the integrity correctly.
|
||||
- Improved:
|
||||
- In the report, the schema of the remote database URI is now printed.
|
||||
- 0.22.9
|
||||
- Fixed:
|
||||
- Fixed a bug on `fetch chunks on demand` that could not fetch the chunks on demand.
|
||||
|
||||
Reference in New Issue
Block a user