mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-08 00:31:54 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8969b1800a | ||
|
|
2c8e026e29 | ||
|
|
a6c27eab3d | ||
|
|
9b5c57d540 | ||
|
|
c251c596e8 | ||
|
|
61188cfaef | ||
|
|
97d944fd75 | ||
|
|
d3dc1e7328 | ||
|
|
45304af369 | ||
|
|
7f422d58f2 | ||
|
|
c2491fdfad | ||
|
|
06a6e391e8 | ||
|
|
f99475f6b7 | ||
|
|
109fc00b9d | ||
|
|
c071d822e1 | ||
|
|
d2de5b4710 | ||
|
|
cf5ecd8922 | ||
|
|
5802ed31be |
25
README.md
25
README.md
@@ -59,14 +59,23 @@ Synchronization status is shown in statusbar.
|
|||||||
|
|
||||||
- Status
|
- Status
|
||||||
- ⏹️ Stopped
|
- ⏹️ Stopped
|
||||||
- 💤 LiveSync enabled. Waiting for changes.
|
- 💤 LiveSync enabled. Waiting for changes
|
||||||
- ⚡️ Synchronization in progress.
|
- ⚡️ Synchronization in progress
|
||||||
- ⚠ An error occurred.
|
- ⚠ An error occurred
|
||||||
- ↑ Uploaded chunks and metadata
|
- Statistical indicator
|
||||||
- ↓ Downloaded chunks and metadata
|
- ↑ Uploaded chunks and metadata
|
||||||
- ⏳ Number of pending processes
|
- ↓ Downloaded chunks and metadata
|
||||||
- 🧩 Number of files waiting for their chunks.
|
- Progress indicator
|
||||||
If you have deleted or renamed files, please wait until ⏳ icon disappears.
|
- 📥 Unprocessed transferred items
|
||||||
|
- 📄 Working database operation
|
||||||
|
- 💾 Working write storage processes
|
||||||
|
- ⏳ Working read storage processes
|
||||||
|
- 🛫 Pending read storage processes
|
||||||
|
- ⚙️ Working or pending storage processes of hidden files
|
||||||
|
- 🧩 Waiting chunks
|
||||||
|
- 🔌 Working Customisation items (Configuration, snippets and plug-ins)
|
||||||
|
|
||||||
|
To prevent file and database corruption, please wait until all progress indicators have disappeared. Especially in case of if you have deleted or renamed files.
|
||||||
|
|
||||||
|
|
||||||
## Hints
|
## Hints
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.21.2",
|
"version": "0.22.2",
|
||||||
"minAppVersion": "0.9.12",
|
"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.",
|
"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",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.21.2",
|
"version": "0.22.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.21.2",
|
"version": "0.22.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.21.2",
|
"version": "0.22.2",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"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",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -7,14 +7,16 @@ import { ICXHeader, PERIODIC_PLUGIN_SWEEP, } from "./types";
|
|||||||
import { createTextBlob, delay, getDocData } from "./lib/src/utils";
|
import { createTextBlob, delay, getDocData } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { WrappedNotice } from "./lib/src/wrapper";
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
import { readString, crc32CKHash, decodeBinary, arrayBufferToBase64 } from "./lib/src/strbin";
|
import { readString, decodeBinary, arrayBufferToBase64, sha1 } from "./lib/src/strbin";
|
||||||
import { serialized } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { stripAllPrefixes } from "./lib/src/path";
|
import { stripAllPrefixes } from "./lib/src/path";
|
||||||
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
import { PeriodicProcessor, askYesNo, disposeMemoObject, memoIfNotExist, memoObject, retrieveMemoObject, scheduleTask } from "./utils";
|
||||||
import { PluginDialogModal } from "./dialogs";
|
import { PluginDialogModal } from "./dialogs";
|
||||||
import { JsonResolveModal } from "./JsonResolveModal";
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
import { pipeGeneratorToGenerator, processAllGeneratorTasksWithConcurrencyLimit } from './lib/src/task';
|
import { QueueProcessor } from './lib/src/processor';
|
||||||
|
import { pluginScanningCount } from './lib/src/stores';
|
||||||
|
import type ObsidianLiveSyncPlugin from './main';
|
||||||
|
|
||||||
const d = "\u200b";
|
const d = "\u200b";
|
||||||
const d2 = "\n";
|
const d2 = "\n";
|
||||||
@@ -162,6 +164,16 @@ export type PluginDataEx = {
|
|||||||
mtime: number,
|
mtime: number,
|
||||||
};
|
};
|
||||||
export class ConfigSync extends LiveSyncCommands {
|
export class ConfigSync extends LiveSyncCommands {
|
||||||
|
constructor(plugin: ObsidianLiveSyncPlugin) {
|
||||||
|
super(plugin);
|
||||||
|
pluginScanningCount.onChanged((e) => {
|
||||||
|
const total = e.value;
|
||||||
|
pluginIsEnumerating.set(total != 0);
|
||||||
|
if (total == 0) {
|
||||||
|
Logger(`Processing configurations done`, LOG_LEVEL_INFO, "get-plugins");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
confirmPopup: WrappedNotice = null;
|
confirmPopup: WrappedNotice = null;
|
||||||
get kvDB() {
|
get kvDB() {
|
||||||
return this.plugin.kvDB;
|
return this.plugin.kvDB;
|
||||||
@@ -270,7 +282,7 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
for (const file of data.files) {
|
for (const file of data.files) {
|
||||||
const work = { ...file };
|
const work = { ...file };
|
||||||
const tempStr = getDocData(work.data);
|
const tempStr = getDocData(work.data);
|
||||||
work.data = [crc32CKHash(tempStr)];
|
work.data = [await sha1(tempStr)];
|
||||||
xFiles.push(work);
|
xFiles.push(work);
|
||||||
}
|
}
|
||||||
return ({
|
return ({
|
||||||
@@ -302,65 +314,65 @@ export class ConfigSync extends LiveSyncCommands {
|
|||||||
this.plugin.saveSettingData();
|
this.plugin.saveSettingData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pluginScanProcessor = new QueueProcessor(async (v: AnyEntry[]) => {
|
||||||
|
const plugin = v[0];
|
||||||
|
const path = plugin.path || this.getPath(plugin);
|
||||||
|
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
|
||||||
|
if (oldEntry && oldEntry.mtime == plugin.mtime) return;
|
||||||
|
try {
|
||||||
|
const pluginData = await this.loadPluginData(path);
|
||||||
|
if (pluginData) {
|
||||||
|
return [pluginData];
|
||||||
|
}
|
||||||
|
// Failed to load
|
||||||
|
return;
|
||||||
|
|
||||||
|
} catch (ex) {
|
||||||
|
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 300, yieldThreshold: 10 }).pipeTo(
|
||||||
|
new QueueProcessor(
|
||||||
|
(pluginDataList) => {
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
, { suspended: true, batchSize: 1000, concurrentLimit: 10, delay: 200, yieldThreshold: 25, totalRemainingReactiveSource: pluginScanningCount })).startPipeline().root.onIdle(() => {
|
||||||
|
Logger(`All files enumerated`, LOG_LEVEL_INFO, "get-plugins");
|
||||||
|
this.createMissingConfigurationEntry();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
async updatePluginList(showMessage: boolean, updatedDocumentPath?: FilePathWithPrefix): Promise<void> {
|
||||||
const logLevel = showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO;
|
|
||||||
// pluginList.set([]);
|
// pluginList.set([]);
|
||||||
if (!this.settings.usePluginSync) {
|
if (!this.settings.usePluginSync) {
|
||||||
|
this.pluginScanProcessor.clearQueue();
|
||||||
this.pluginList = [];
|
this.pluginList = [];
|
||||||
pluginList.set(this.pluginList)
|
pluginList.set(this.pluginList)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.resolve(); // Just to prevent warning.
|
try {
|
||||||
scheduleTask("update-plugin-list-task", 200, async () => {
|
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
||||||
await serialized("update-plugin-list", async () => {
|
const plugins = updatedDocumentPath ?
|
||||||
try {
|
this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) :
|
||||||
const updatedDocumentId = updatedDocumentPath ? await this.path2id(updatedDocumentPath) : "";
|
this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true });
|
||||||
const plugins = updatedDocumentPath ?
|
for await (const v of plugins) {
|
||||||
this.localDatabase.findEntries(updatedDocumentId, updatedDocumentId + "\u{10ffff}", { include_docs: true, key: updatedDocumentId, limit: 1 }) :
|
const path = v.path || this.getPath(v);
|
||||||
this.localDatabase.findEntries(ICXHeader + "", `${ICXHeader}\u{10ffff}`, { include_docs: true });
|
if (updatedDocumentPath && updatedDocumentPath != path) continue;
|
||||||
let count = 0;
|
this.pluginScanProcessor.enqueue(v);
|
||||||
pluginIsEnumerating.set(true);
|
}
|
||||||
for await (const v of processAllGeneratorTasksWithConcurrencyLimit(20, pipeGeneratorToGenerator(plugins, async plugin => {
|
} finally {
|
||||||
const path = plugin.path || this.getPath(plugin);
|
|
||||||
if (updatedDocumentPath && updatedDocumentPath != path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const oldEntry = (this.pluginList.find(e => e.documentPath == path));
|
|
||||||
if (oldEntry && oldEntry.mtime == plugin.mtime) return false;
|
|
||||||
try {
|
|
||||||
count++;
|
|
||||||
if (count % 10 == 0) Logger(`Enumerating files... ${count}`, logLevel, "get-plugins");
|
|
||||||
Logger(`plugin-${path}`, LOG_LEVEL_VERBOSE);
|
|
||||||
return this.loadPluginData(path);
|
|
||||||
// return entries;
|
|
||||||
} catch (ex) {
|
|
||||||
//TODO
|
|
||||||
Logger(`Something happened at enumerating customization :${path}`, LOG_LEVEL_NOTICE);
|
|
||||||
console.warn(ex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}))) {
|
|
||||||
if ("ok" in v) {
|
|
||||||
if (v.ok !== false) {
|
|
||||||
let newList = [...this.pluginList];
|
|
||||||
const item = v.ok;
|
|
||||||
newList = newList.filter(x => x.documentPath != item.documentPath);
|
|
||||||
newList.push(item)
|
|
||||||
if (updatedDocumentPath != "") newList = newList.filter(e => e.documentPath != updatedDocumentPath);
|
|
||||||
this.pluginList = newList;
|
|
||||||
pluginList.set(newList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logger(`All files enumerated`, logLevel, "get-plugins");
|
|
||||||
pluginIsEnumerating.set(false);
|
|
||||||
this.createMissingConfigurationEntry();
|
|
||||||
} finally {
|
|
||||||
pluginIsEnumerating.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pluginIsEnumerating.set(false);
|
pluginIsEnumerating.set(false);
|
||||||
});
|
}
|
||||||
|
pluginIsEnumerating.set(false);
|
||||||
// return entries;
|
// return entries;
|
||||||
}
|
}
|
||||||
async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) {
|
async compareUsingDisplayData(dataA: PluginDataExDisplay, dataB: PluginDataExDisplay) {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { normalizePath, type PluginManifest } from "./deps";
|
import { normalizePath, type PluginManifest } from "./deps";
|
||||||
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry } from "./lib/src/types";
|
import { type EntryDoc, type LoadedEntry, type InternalFileEntry, type FilePathWithPrefix, type FilePath, LOG_LEVEL_INFO, LOG_LEVEL_NOTICE, LOG_LEVEL_VERBOSE, MODE_SELECTIVE, MODE_PAUSED, type SavingEntry } from "./lib/src/types";
|
||||||
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
import { type InternalFileInfo, ICHeader, ICHeaderEnd } from "./types";
|
||||||
import { Parallels, createBinaryBlob, delay, isDocContentSame } from "./lib/src/utils";
|
import { createBinaryBlob, isDocContentSame, sendSignal } from "./lib/src/utils";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
import { PouchDB } from "./lib/src/pouchdb-browser.js";
|
||||||
import { scheduleTask, isInternalMetadata, PeriodicProcessor } from "./utils";
|
import { isInternalMetadata, PeriodicProcessor } from "./utils";
|
||||||
import { WrappedNotice } from "./lib/src/wrapper";
|
import { WrappedNotice } from "./lib/src/wrapper";
|
||||||
import { decodeBinary, encodeBinary } from "./lib/src/strbin";
|
import { decodeBinary, encodeBinary } from "./lib/src/strbin";
|
||||||
import { serialized } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import { JsonResolveModal } from "./JsonResolveModal";
|
import { JsonResolveModal } from "./JsonResolveModal";
|
||||||
import { LiveSyncCommands } from "./LiveSyncCommands";
|
import { LiveSyncCommands } from "./LiveSyncCommands";
|
||||||
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
import { addPrefix, stripAllPrefixes } from "./lib/src/path";
|
||||||
|
import { KeyedQueueProcessor, QueueProcessor } from "./lib/src/processor";
|
||||||
|
import { hiddenFilesEventCount, hiddenFilesProcessingCount } from "./lib/src/stores";
|
||||||
|
|
||||||
export class HiddenFileSync extends LiveSyncCommands {
|
export class HiddenFileSync extends LiveSyncCommands {
|
||||||
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.settings.syncInternalFiles && this.localDatabase.isReady && await this.syncInternalFilesAndDatabase("push", false));
|
periodicInternalFileScanProcessor: PeriodicProcessor = new PeriodicProcessor(this.plugin, async () => this.settings.syncInternalFiles && this.localDatabase.isReady && await this.syncInternalFilesAndDatabase("push", false));
|
||||||
@@ -75,22 +77,17 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
procInternalFiles: string[] = [];
|
|
||||||
async execInternalFile() {
|
|
||||||
await serialized("execInternal", async () => {
|
|
||||||
const w = [...this.procInternalFiles];
|
|
||||||
this.procInternalFiles = [];
|
|
||||||
Logger(`Applying hidden ${w.length} files change...`);
|
|
||||||
await this.syncInternalFilesAndDatabase("pull", false, false, w);
|
|
||||||
Logger(`Applying hidden ${w.length} files changed`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
procInternalFile(filename: string) {
|
procInternalFile(filename: string) {
|
||||||
this.procInternalFiles.push(filename);
|
this.internalFileProcessor.enqueueWithKey(filename, filename);
|
||||||
scheduleTask("procInternal", 500, async () => {
|
|
||||||
await this.execInternalFile();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
internalFileProcessor = new KeyedQueueProcessor<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 }
|
||||||
|
);
|
||||||
|
|
||||||
recentProcessedInternalFiles = [] as string[];
|
recentProcessedInternalFiles = [] as string[];
|
||||||
async watchVaultRawEventsAsync(path: FilePath) {
|
async watchVaultRawEventsAsync(path: FilePath) {
|
||||||
@@ -137,25 +134,34 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
async resolveConflictOnInternalFiles() {
|
async resolveConflictOnInternalFiles() {
|
||||||
// Scan all conflicted internal files
|
// Scan all conflicted internal files
|
||||||
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
|
const conflicted = this.localDatabase.findEntries(ICHeader, ICHeaderEnd, { conflicts: true });
|
||||||
for await (const doc of conflicted) {
|
this.conflictResolutionProcessor.suspend();
|
||||||
if (!("_conflicts" in doc))
|
try {
|
||||||
continue;
|
for await (const doc of conflicted) {
|
||||||
if (isInternalMetadata(doc._id)) {
|
if (!("_conflicts" in doc))
|
||||||
await this.resolveConflictOnInternalFile(doc.path);
|
continue;
|
||||||
|
if (isInternalMetadata(doc._id)) {
|
||||||
|
this.conflictResolutionProcessor.enqueue(doc.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
Logger("something went wrong on resolving all conflicted internal files");
|
||||||
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
|
await this.conflictResolutionProcessor.startPipeline().waitForPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolveConflictOnInternalFile(path: FilePathWithPrefix): Promise<boolean> {
|
conflictResolutionProcessor = new QueueProcessor(async (paths: FilePathWithPrefix[]) => {
|
||||||
|
const path = paths[0];
|
||||||
|
sendSignal(`cancel-internal-conflict:${path}`);
|
||||||
try {
|
try {
|
||||||
// Retrieve data
|
// Retrieve data
|
||||||
const id = await this.path2id(path, ICHeader);
|
const id = await this.path2id(path, ICHeader);
|
||||||
const doc = await this.localDatabase.getRaw(id, { conflicts: true });
|
const doc = await this.localDatabase.getRaw(id, { conflicts: true });
|
||||||
// If there is no conflict, return with false.
|
// If there is no conflict, return with false.
|
||||||
if (!("_conflicts" in doc))
|
if (!("_conflicts" in doc))
|
||||||
return false;
|
return;
|
||||||
if (doc._conflicts.length == 0)
|
if (doc._conflicts.length == 0)
|
||||||
return false;
|
return;
|
||||||
Logger(`Hidden file conflicted:${path}`);
|
Logger(`Hidden file conflicted:${path}`);
|
||||||
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
const conflicts = doc._conflicts.sort((a, b) => Number(a.split("-")[0]) - Number(b.split("-")[0]));
|
||||||
const revA = doc._rev;
|
const revA = doc._rev;
|
||||||
@@ -180,21 +186,12 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
await this.storeInternalFileToDatabase({ path: filename, ...stat });
|
||||||
await this.extractInternalFileFromDatabase(filename);
|
await this.extractInternalFileFromDatabase(filename);
|
||||||
await this.localDatabase.removeRaw(id, revB);
|
await this.localDatabase.removeRaw(id, revB);
|
||||||
return this.resolveConflictOnInternalFile(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
Logger(`Object merge is not applicable.`, LOG_LEVEL_VERBOSE);
|
||||||
}
|
}
|
||||||
|
return [{ path, revA, revB }];
|
||||||
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
|
||||||
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
|
||||||
if (docAMerge != false && docBMerge != false) {
|
|
||||||
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
|
|
||||||
await delay(200);
|
|
||||||
// Again for other conflicted revisions.
|
|
||||||
return this.resolveConflictOnInternalFile(path);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB });
|
const revBDoc = await this.localDatabase.getRaw(id, { rev: revB });
|
||||||
// determine which revision should been deleted.
|
// determine which revision should been deleted.
|
||||||
@@ -208,12 +205,31 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
await this.localDatabase.removeRaw(id, delRev);
|
await this.localDatabase.removeRaw(id, delRev);
|
||||||
Logger(`Older one has been deleted:${path}`);
|
Logger(`Older one has been deleted:${path}`);
|
||||||
// check the file again
|
// check the file again
|
||||||
return this.resolveConflictOnInternalFile(path);
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
return;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
Logger(`Failed to resolve conflict (Hidden): ${path}`);
|
||||||
Logger(ex, LOG_LEVEL_VERBOSE);
|
Logger(ex, LOG_LEVEL_VERBOSE);
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
suspended: false, batchSize: 1, concurrentLimit: 5, delay: 10, keepResultUntilDownstreamConnected: true, yieldThreshold: 10,
|
||||||
|
pipeTo: new QueueProcessor(async (results) => {
|
||||||
|
const { path, revA, revB } = results[0]
|
||||||
|
const docAMerge = await this.localDatabase.getDBEntry(path, { rev: revA });
|
||||||
|
const docBMerge = await this.localDatabase.getDBEntry(path, { rev: revB });
|
||||||
|
if (docAMerge != false && docBMerge != false) {
|
||||||
|
if (await this.showJSONMergeDialogAndMerge(docAMerge, docBMerge)) {
|
||||||
|
// Again for other conflicted revisions.
|
||||||
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, { suspended: false, batchSize: 1, concurrentLimit: 1, delay: 10, keepResultUntilDownstreamConnected: false, yieldThreshold: 10 })
|
||||||
|
})
|
||||||
|
|
||||||
|
queueConflictCheck(path: FilePathWithPrefix) {
|
||||||
|
this.conflictResolutionProcessor.enqueue(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
//TODO: Tidy up. Even though it is experimental feature, So dirty...
|
||||||
@@ -278,28 +294,38 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
acc[stripAllPrefixes(this.getPath(cur))] = cur;
|
acc[stripAllPrefixes(this.getPath(cur))] = cur;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [key: string]: InternalFileEntry; });
|
}, {} as { [key: string]: InternalFileEntry; });
|
||||||
const para = Parallels();
|
await new QueueProcessor(async (filenames: FilePath[]) => {
|
||||||
for (const filename of allFileNames) {
|
const filename = filenames[0];
|
||||||
processed++;
|
processed++;
|
||||||
if (processed % 100 == 0) {
|
if (processed % 100 == 0) {
|
||||||
Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal");
|
Logger(`Hidden file: ${processed}/${fileCount}`, logLevel, "sync_internal");
|
||||||
}
|
}
|
||||||
if (!filename) continue;
|
if (!filename) return;
|
||||||
if (ignorePatterns.some(e => filename.match(e)))
|
if (ignorePatterns.some(e => filename.match(e)))
|
||||||
continue;
|
return;
|
||||||
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
if (await this.plugin.isIgnoredByIgnoreFiles(filename)) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
const fileOnStorage = filename in filesMap ? filesMap[filename] : undefined;
|
||||||
const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined;
|
const fileOnDatabase = filename in filesOnDBMap ? filesOnDBMap[filename] : undefined;
|
||||||
|
|
||||||
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 };
|
return [{
|
||||||
|
filename,
|
||||||
await para.wait(5);
|
fileOnStorage,
|
||||||
const proc = (async (xFileOnStorage: InternalFileInfo, xFileOnDatabase: InternalFileEntry) => {
|
fileOnDatabase,
|
||||||
|
}]
|
||||||
|
|
||||||
|
}, { suspended: true, batchSize: 1, concurrentLimit: 10, delay: 0, totalRemainingReactiveSource: hiddenFilesProcessingCount })
|
||||||
|
.pipeTo(new QueueProcessor(async (params) => {
|
||||||
|
const
|
||||||
|
{
|
||||||
|
filename,
|
||||||
|
fileOnStorage: xFileOnStorage,
|
||||||
|
fileOnDatabase: xFileOnDatabase
|
||||||
|
} = params[0];
|
||||||
if (xFileOnStorage && xFileOnDatabase) {
|
if (xFileOnStorage && xFileOnDatabase) {
|
||||||
|
const cache = filename in caches ? caches[filename] : { storageMtime: 0, docMtime: 0 };
|
||||||
// Both => Synchronize
|
// Both => Synchronize
|
||||||
if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) {
|
if ((direction != "pullForce" && direction != "pushForce") && xFileOnDatabase.mtime == cache.docMtime && xFileOnStorage.mtime == cache.storageMtime) {
|
||||||
return;
|
return;
|
||||||
@@ -340,11 +366,12 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
throw new Error("Invalid state on hidden file sync");
|
throw new Error("Invalid state on hidden file sync");
|
||||||
// Something corrupted?
|
// Something corrupted?
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}, { suspended: true, batchSize: 1, concurrentLimit: 5, delay: 0 }))
|
||||||
|
.root
|
||||||
|
.enqueueAll(allFileNames)
|
||||||
|
.startPipeline().waitForPipeline();
|
||||||
|
|
||||||
});
|
|
||||||
para.add(proc(fileOnStorage, fileOnDatabase))
|
|
||||||
}
|
|
||||||
await para.all();
|
|
||||||
await this.kvDB.set("diff-caches-internal", caches);
|
await this.kvDB.set("diff-caches-internal", caches);
|
||||||
|
|
||||||
// When files has been retrieved from the database. they must be reloaded.
|
// When files has been retrieved from the database. they must be reloaded.
|
||||||
@@ -436,7 +463,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
type: "newnote",
|
type: "newnote",
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
if (isDocContentSame(old.data, content) && !forceWrite) {
|
if (await isDocContentSame(old.data, content) && !forceWrite) {
|
||||||
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
// Logger(`STORAGE --> DB:${file.path}: (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -560,7 +587,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
} else {
|
} else {
|
||||||
const contentBin = await this.plugin.vaultAccess.adapterReadBinary(filename);
|
const contentBin = await this.plugin.vaultAccess.adapterReadBinary(filename);
|
||||||
const content = await encodeBinary(contentBin);
|
const content = await encodeBinary(contentBin);
|
||||||
if (isDocContentSame(content, fileOnDB.data) && !force) {
|
if (await isDocContentSame(content, fileOnDB.data) && !force) {
|
||||||
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
// Logger(`STORAGE <-- DB:${filename}: skipped (hidden) Not changed`, LOG_LEVEL_VERBOSE);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -587,7 +614,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
|
|
||||||
|
|
||||||
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
showJSONMergeDialogAndMerge(docA: LoadedEntry, docB: LoadedEntry): Promise<boolean> {
|
||||||
return serialized("conflict:merge-data", () => new Promise((res) => {
|
return new Promise((res) => {
|
||||||
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
Logger("Opening data-merging dialog", LOG_LEVEL_VERBOSE);
|
||||||
const docs = [docA, docB];
|
const docs = [docA, docB];
|
||||||
const path = stripAllPrefixes(docA.path);
|
const path = stripAllPrefixes(docA.path);
|
||||||
@@ -641,7 +668,7 @@ export class HiddenFileSync extends LiveSyncCommands {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.open();
|
modal.open();
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
async scanInternalFiles(): Promise<InternalFileInfo[]> {
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export class PluginAndTheirSettings extends LiveSyncCommands {
|
|||||||
if (old !== false) {
|
if (old !== false) {
|
||||||
const oldData = { data: old.data, deleted: old._deleted };
|
const oldData = { data: old.data, deleted: old._deleted };
|
||||||
const newData = { data: d.data, deleted: d._deleted };
|
const newData = { data: d.data, deleted: d._deleted };
|
||||||
if (isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) {
|
if (await isDocContentSame(oldData.data, newData.data) && oldData.deleted == newData.deleted) {
|
||||||
Logger(`Nothing changed:${m.name}`);
|
Logger(`Nothing changed:${m.name}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,7 +321,6 @@ Of course, we are able to disable these features.`
|
|||||||
this.plugin.settings.suspendFileWatching = false;
|
this.plugin.settings.suspendFileWatching = false;
|
||||||
await this.plugin.syncAllFiles(true);
|
await this.plugin.syncAllFiles(true);
|
||||||
await this.plugin.loadQueuedFiles();
|
await this.plugin.loadQueuedFiles();
|
||||||
this.plugin.procQueuedFiles();
|
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,41 @@
|
|||||||
import { App, Modal } from "./deps";
|
import { App, Modal } from "./deps";
|
||||||
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT } from "diff-match-patch";
|
||||||
import { type diff_result } from "./lib/src/types";
|
import { CANCELLED, LEAVE_TO_SUBSEQUENT, RESULT_TIMED_OUT, type diff_result } from "./lib/src/types";
|
||||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||||
|
import { delay, sendValue, waitForValue } from "./lib/src/utils";
|
||||||
|
|
||||||
|
export type MergeDialogResult = typeof LEAVE_TO_SUBSEQUENT | typeof CANCELLED | string;
|
||||||
export class ConflictResolveModal extends Modal {
|
export class ConflictResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
|
||||||
result: diff_result;
|
result: diff_result;
|
||||||
filename: string;
|
filename: string;
|
||||||
callback: (remove_rev: string) => Promise<void>;
|
|
||||||
|
|
||||||
constructor(app: App, filename: string, diff: diff_result, callback: (remove_rev: string) => Promise<void>) {
|
response: MergeDialogResult = CANCELLED;
|
||||||
|
isClosed = false;
|
||||||
|
consumed = false;
|
||||||
|
|
||||||
|
constructor(app: App, filename: string, diff: diff_result) {
|
||||||
super(app);
|
super(app);
|
||||||
this.result = diff;
|
this.result = diff;
|
||||||
this.callback = callback;
|
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
// Send cancel signal for the previous merge dialogue
|
||||||
|
// if not there, simply be ignored.
|
||||||
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
// Send cancel signal for the previous merge dialogue
|
||||||
|
// if not there, simply be ignored.
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, true);
|
||||||
|
setTimeout(async () => {
|
||||||
|
const forceClose = await waitForValue("cancel-resolve-conflict:" + this.filename);
|
||||||
|
// debugger;
|
||||||
|
if (forceClose) {
|
||||||
|
this.sendResponse(CANCELLED);
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
// sendValue("close-resolve-conflict:" + this.filename, false);
|
||||||
this.titleEl.setText("Conflicting changes");
|
this.titleEl.setText("Conflicting changes");
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
contentEl.createEl("span", { text: this.filename });
|
contentEl.createEl("span", { text: this.filename });
|
||||||
@@ -44,42 +62,32 @@ export class ConflictResolveModal extends Modal {
|
|||||||
div2.innerHTML = `
|
div2.innerHTML = `
|
||||||
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
<span class='deleted'>A:${date1}</span><br /><span class='added'>B:${date2}</span><br>
|
||||||
`;
|
`;
|
||||||
contentEl.createEl("button", { text: "Keep A" }, (e) => {
|
contentEl.createEl("button", { text: "Keep A" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.right.rev)));
|
||||||
e.addEventListener("click", async () => {
|
contentEl.createEl("button", { text: "Keep B" }, (e) => e.addEventListener("click", () => this.sendResponse(this.result.left.rev)));
|
||||||
const callback = this.callback;
|
contentEl.createEl("button", { text: "Concat both" }, (e) => e.addEventListener("click", () => this.sendResponse(LEAVE_TO_SUBSEQUENT)));
|
||||||
this.callback = null;
|
contentEl.createEl("button", { text: "Not now" }, (e) => e.addEventListener("click", () => this.sendResponse(CANCELLED)));
|
||||||
this.close();
|
}
|
||||||
await callback(this.result.right.rev);
|
|
||||||
});
|
sendResponse(result: MergeDialogResult) {
|
||||||
});
|
this.response = result;
|
||||||
contentEl.createEl("button", { text: "Keep B" }, (e) => {
|
this.close();
|
||||||
e.addEventListener("click", async () => {
|
|
||||||
const callback = this.callback;
|
|
||||||
this.callback = null;
|
|
||||||
this.close();
|
|
||||||
await callback(this.result.left.rev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
contentEl.createEl("button", { text: "Concat both" }, (e) => {
|
|
||||||
e.addEventListener("click", async () => {
|
|
||||||
const callback = this.callback;
|
|
||||||
this.callback = null;
|
|
||||||
this.close();
|
|
||||||
await callback("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
contentEl.createEl("button", { text: "Not now" }, (e) => {
|
|
||||||
e.addEventListener("click", () => {
|
|
||||||
this.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
if (this.callback != null) {
|
if (this.consumed) {
|
||||||
this.callback(null);
|
return;
|
||||||
}
|
}
|
||||||
|
this.consumed = true;
|
||||||
|
sendValue("close-resolve-conflict:" + this.filename, this.response);
|
||||||
|
sendValue("cancel-resolve-conflict:" + this.filename, false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async waitForResult(): Promise<MergeDialogResult> {
|
||||||
|
await delay(100);
|
||||||
|
const r = await waitForValue<MergeDialogResult>("close-resolve-conflict:" + this.filename);
|
||||||
|
if (r === RESULT_TIMED_OUT) return CANCELLED;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,34 @@ import { type DocumentID, type FilePathWithPrefix, type LoadedEntry, LOG_LEVEL_I
|
|||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
import { isErrorOfMissingDoc } from "./lib/src/utils_couchdb";
|
||||||
import { getDocData } from "./lib/src/utils";
|
import { getDocData } from "./lib/src/utils";
|
||||||
import { stripPrefix } from "./lib/src/path";
|
import { isPlainText, stripPrefix } from "./lib/src/path";
|
||||||
|
|
||||||
|
function isImage(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return ["png", "jpg", "jpeg", "gif", "bmp", "webp"].includes(ext);
|
||||||
|
}
|
||||||
|
function isComparableText(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return isPlainText(path) || ["md", "mdx", "txt", "json"].includes(ext);
|
||||||
|
}
|
||||||
|
function isComparableTextDecode(path: string) {
|
||||||
|
const ext = path.split(".").splice(-1)[0].toLowerCase();
|
||||||
|
return ["json"].includes(ext)
|
||||||
|
}
|
||||||
|
function readDocument(w: LoadedEntry) {
|
||||||
|
if (isImage(w.path)) {
|
||||||
|
return new Uint8Array(decodeBinary(w.data));
|
||||||
|
}
|
||||||
|
if (w.data == "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 {
|
||||||
|
return readString(new Uint8Array(decodeBinary(w.data)));
|
||||||
|
} catch (ex) {
|
||||||
|
// NO OP.
|
||||||
|
}
|
||||||
|
return getDocData(w.data);
|
||||||
|
}
|
||||||
export class DocumentHistoryModal extends Modal {
|
export class DocumentHistoryModal extends Modal {
|
||||||
plugin: ObsidianLiveSyncPlugin;
|
plugin: ObsidianLiveSyncPlugin;
|
||||||
range!: HTMLInputElement;
|
range!: HTMLInputElement;
|
||||||
@@ -56,7 +82,6 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
this.range.max = "0";
|
this.range.max = "0";
|
||||||
this.range.value = "";
|
this.range.value = "";
|
||||||
this.range.disabled = true;
|
this.range.disabled = true;
|
||||||
this.showDiff
|
|
||||||
this.contentView.setText(`History of this file was not recorded.`);
|
this.contentView.setText(`History of this file was not recorded.`);
|
||||||
} else {
|
} else {
|
||||||
this.contentView.setText(`Error occurred.`);
|
this.contentView.setText(`Error occurred.`);
|
||||||
@@ -76,6 +101,22 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
const rev = this.revs_info[index];
|
const rev = this.revs_info[index];
|
||||||
await this.showExactRev(rev.rev);
|
await this.showExactRev(rev.rev);
|
||||||
}
|
}
|
||||||
|
BlobURLs = new Map<string, string>();
|
||||||
|
|
||||||
|
revokeURL(key: string) {
|
||||||
|
const v = this.BlobURLs.get(key);
|
||||||
|
if (v) {
|
||||||
|
URL.revokeObjectURL(v);
|
||||||
|
}
|
||||||
|
this.BlobURLs.set(key, undefined);
|
||||||
|
}
|
||||||
|
generateBlobURL(key: string, data: Uint8Array) {
|
||||||
|
this.revokeURL(key);
|
||||||
|
const v = URL.createObjectURL(new Blob([data], { endings: "transparent", type: "application/octet-stream" }));
|
||||||
|
this.BlobURLs.set(key, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
async showExactRev(rev: string) {
|
async showExactRev(rev: string) {
|
||||||
const db = this.plugin.localDatabase;
|
const db = this.plugin.localDatabase;
|
||||||
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
const w = await db.getDBEntry(this.file, { rev: rev }, false, false, true);
|
||||||
@@ -88,42 +129,67 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
} else {
|
} else {
|
||||||
this.currentDoc = w;
|
this.currentDoc = w;
|
||||||
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
this.info.innerHTML = `Modified:${new Date(w.mtime).toLocaleString()}`;
|
||||||
let result = "";
|
let result = undefined;
|
||||||
const w1data = w.datatype == "plain" ? getDocData(w.data) : readString(new Uint8Array(decodeBinary(w.data)));
|
const w1data = readDocument(w);
|
||||||
this.currentDeleted = !!w.deleted;
|
this.currentDeleted = !!w.deleted;
|
||||||
this.currentText = w1data;
|
// this.currentText = w1data;
|
||||||
if (this.showDiff) {
|
if (this.showDiff) {
|
||||||
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
const prevRevIdx = this.revs_info.length - 1 - ((this.range.value as any) / 1 - 1);
|
||||||
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
if (prevRevIdx >= 0 && prevRevIdx < this.revs_info.length) {
|
||||||
const oldRev = this.revs_info[prevRevIdx].rev;
|
const oldRev = this.revs_info[prevRevIdx].rev;
|
||||||
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
const w2 = await db.getDBEntry(this.file, { rev: oldRev }, false, false, true);
|
||||||
if (w2 != false) {
|
if (w2 != false) {
|
||||||
const dmp = new diff_match_patch();
|
if (typeof w1data == "string") {
|
||||||
const w2data = w2.datatype == "plain" ? getDocData(w2.data) : readString(new Uint8Array(decodeBinary(w2.data)));
|
result = "";
|
||||||
const diff = dmp.diff_main(w2data, w1data);
|
const dmp = new diff_match_patch();
|
||||||
dmp.diff_cleanupSemantic(diff);
|
const w2data = readDocument(w2) as string;
|
||||||
for (const v of diff) {
|
const diff = dmp.diff_main(w2data, w1data);
|
||||||
const x1 = v[0];
|
dmp.diff_cleanupSemantic(diff);
|
||||||
const x2 = v[1];
|
for (const v of diff) {
|
||||||
if (x1 == DIFF_DELETE) {
|
const x1 = v[0];
|
||||||
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
const x2 = v[1];
|
||||||
} else if (x1 == DIFF_EQUAL) {
|
if (x1 == DIFF_DELETE) {
|
||||||
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
result += "<span class='history-deleted'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
} else if (x1 == DIFF_INSERT) {
|
} else if (x1 == DIFF_EQUAL) {
|
||||||
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
result += "<span class='history-normal'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
|
} else if (x1 == DIFF_INSERT) {
|
||||||
|
result += "<span class='history-added'>" + escapeStringToHTML(x2) + "</span>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
result = result.replace(/\n/g, "<br>");
|
||||||
|
} else if (isImage(this.file)) {
|
||||||
|
const src = this.generateBlobURL("base", w1data);
|
||||||
|
const overlay = this.generateBlobURL("overlay", readDocument(w2) as Uint8Array);
|
||||||
|
result =
|
||||||
|
`<div class='ls-imgdiff-wrap'>
|
||||||
|
<div class='overlay'>
|
||||||
|
<img class='img-base' src="${src}">
|
||||||
|
<img class='img-overlay' src='${overlay}'>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
this.contentView.removeClass("op-pre");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result = result.replace(/\n/g, "<br>");
|
}
|
||||||
} else {
|
if (result == undefined) {
|
||||||
result = escapeStringToHTML(w1data);
|
if (typeof w1data != "string") {
|
||||||
|
if (isImage(this.file)) {
|
||||||
|
const src = this.generateBlobURL("base", w1data);
|
||||||
|
result =
|
||||||
|
`<div class='ls-imgdiff-wrap'>
|
||||||
|
<div class='overlay'>
|
||||||
|
<img class='img-base' src="${src}">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
this.contentView.removeClass("op-pre");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = escapeStringToHTML(w1data);
|
result = escapeStringToHTML(w1data);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
result = escapeStringToHTML(w1data);
|
|
||||||
}
|
}
|
||||||
|
if (result == undefined) result = typeof w1data == "string" ? escapeStringToHTML(w1data) : "Binary file";
|
||||||
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
this.contentView.innerHTML = (this.currentDeleted ? "(At this revision, the file has been deleted)\n" : "") + result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,5 +283,9 @@ export class DocumentHistoryModal extends Modal {
|
|||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
contentEl.empty();
|
contentEl.empty();
|
||||||
|
this.BlobURLs.forEach(value => {
|
||||||
|
console.log(value);
|
||||||
|
if (value) URL.revokeObjectURL(value);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,10 +66,7 @@
|
|||||||
|
|
||||||
for (const revInfo of reversedRevs) {
|
for (const revInfo of reversedRevs) {
|
||||||
if (revInfo.status == "available") {
|
if (revInfo.status == "available") {
|
||||||
const doc =
|
const doc = (!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev) ? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true) : await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
|
||||||
(!isPlain && showDiffInfo) || (checkStorageDiff && revInfo.rev == docA._rev)
|
|
||||||
? await db.getDBEntry(path, { rev: revInfo.rev }, false, false, true)
|
|
||||||
: await db.getDBEntryMeta(path, { rev: revInfo.rev }, true);
|
|
||||||
if (doc === false) continue;
|
if (doc === false) continue;
|
||||||
const rev = revInfo.rev;
|
const rev = revInfo.rev;
|
||||||
|
|
||||||
@@ -112,11 +109,11 @@
|
|||||||
let result = false;
|
let result = false;
|
||||||
if (isPlainText(docA.path)) {
|
if (isPlainText(docA.path)) {
|
||||||
const data = await plugin.vaultAccess.adapterRead(abs);
|
const data = await plugin.vaultAccess.adapterRead(abs);
|
||||||
result = isDocContentSame(data, doc.data);
|
result = await isDocContentSame(data, doc.data);
|
||||||
} else {
|
} else {
|
||||||
const data = await plugin.vaultAccess.adapterReadBinary(abs);
|
const data = await plugin.vaultAccess.adapterReadBinary(abs);
|
||||||
const dataEEncoded = createBinaryBlob(data);
|
const dataEEncoded = createBinaryBlob(data);
|
||||||
result = isDocContentSame(dataEEncoded, doc.data);
|
result = await isDocContentSame(dataEEncoded, doc.data);
|
||||||
}
|
}
|
||||||
if (result) {
|
if (result) {
|
||||||
diffDetail += " ⚖️";
|
diffDetail += " ⚖️";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { App, Modal } from "./deps";
|
import { App, Modal } from "./deps";
|
||||||
import { type FilePath, type LoadedEntry } from "./lib/src/types";
|
import { type FilePath, type LoadedEntry } from "./lib/src/types";
|
||||||
import JsonResolvePane from "./JsonResolvePane.svelte";
|
import JsonResolvePane from "./JsonResolvePane.svelte";
|
||||||
|
import { waitForSignal } from "./lib/src/utils";
|
||||||
|
|
||||||
export class JsonResolveModal extends Modal {
|
export class JsonResolveModal extends Modal {
|
||||||
// result: Array<[number, string]>;
|
// result: Array<[number, string]>;
|
||||||
@@ -20,6 +21,7 @@ export class JsonResolveModal extends Modal {
|
|||||||
this.nameA = nameA;
|
this.nameA = nameA;
|
||||||
this.nameB = nameB;
|
this.nameB = nameB;
|
||||||
this.defaultSelect = defaultSelect;
|
this.defaultSelect = defaultSelect;
|
||||||
|
waitForSignal(`cancel-internal-conflict:${filename}`).then(() => this.close());
|
||||||
}
|
}
|
||||||
async UICallback(keepRev: string, mergedStr?: string) {
|
async UICallback(keepRev: string, mergedStr?: string) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { App, Modal } from "./deps";
|
import { App, Modal } from "./deps";
|
||||||
import { logMessageStore } from "./lib/src/stores";
|
import type { ReactiveInstance, } from "./lib/src/reactive";
|
||||||
|
import { logMessages } from "./lib/src/stores";
|
||||||
import { escapeStringToHTML } from "./lib/src/strbin";
|
import { escapeStringToHTML } from "./lib/src/strbin";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
|
|
||||||
@@ -21,14 +22,16 @@ export class LogDisplayModal extends Modal {
|
|||||||
div.addClass("op-scrollable");
|
div.addClass("op-scrollable");
|
||||||
div.addClass("op-pre");
|
div.addClass("op-pre");
|
||||||
this.logEl = div;
|
this.logEl = div;
|
||||||
this.unsubscribe = logMessageStore.observe((e) => {
|
function updateLog(logs: ReactiveInstance<string[]>) {
|
||||||
|
const e = logs.value;
|
||||||
let msg = "";
|
let msg = "";
|
||||||
for (const v of e) {
|
for (const v of e) {
|
||||||
msg += escapeStringToHTML(v) + "<br>";
|
msg += escapeStringToHTML(v) + "<br>";
|
||||||
}
|
}
|
||||||
this.logEl.innerHTML = msg;
|
this.logEl.innerHTML = msg;
|
||||||
})
|
}
|
||||||
logMessageStore.invalidate();
|
logMessages.onChanged(updateLog);
|
||||||
|
this.unsubscribe = () => logMessages.offChanged(updateLog);
|
||||||
}
|
}
|
||||||
onClose() {
|
onClose() {
|
||||||
const { contentEl } = this;
|
const { contentEl } = this;
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from "svelte";
|
||||||
import { logMessageStore } from "./lib/src/stores";
|
import { logMessages } from "./lib/src/stores";
|
||||||
|
import type { ReactiveInstance } from "./lib/src/reactive";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
|
|
||||||
let unsubscribe: () => void;
|
let unsubscribe: () => void;
|
||||||
let messages = [] as string[];
|
let messages = [] as string[];
|
||||||
let wrapRight = false;
|
let wrapRight = false;
|
||||||
let autoScroll = true;
|
let autoScroll = true;
|
||||||
let suspended = false;
|
let suspended = false;
|
||||||
|
function updateLog(logs: ReactiveInstance<string[]>) {
|
||||||
|
const e = logs.value;
|
||||||
|
if (!suspended) {
|
||||||
|
messages = [...e];
|
||||||
|
setTimeout(() => {
|
||||||
|
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
unsubscribe = logMessageStore.observe((e) => {
|
logMessages.onChanged(updateLog);
|
||||||
if (!suspended) {
|
Logger("Log window opened");
|
||||||
messages = [...e];
|
unsubscribe = () => logMessages.offChanged(updateLog);
|
||||||
if (autoScroll) {
|
|
||||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
logMessageStore.invalidate();
|
|
||||||
setTimeout(() => {
|
|
||||||
if (scroll) scroll.scrollTop = scroll.scrollHeight;
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (unsubscribe) unsubscribe();
|
if (unsubscribe) unsubscribe();
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { delay } from "./lib/src/utils";
|
|||||||
import { Semaphore } from "./lib/src/semaphore";
|
import { Semaphore } from "./lib/src/semaphore";
|
||||||
import { versionNumberString2Number } from "./lib/src/strbin";
|
import { versionNumberString2Number } from "./lib/src/strbin";
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb.js";
|
import { checkSyncInfo, isCloudantURI } from "./lib/src/utils_couchdb";
|
||||||
import { testCrypt } from "./lib/src/e2ee_v2";
|
import { testCrypt } from "./lib/src/e2ee_v2";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import ObsidianLiveSyncPlugin from "./main";
|
||||||
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
import { askYesNo, performRebuildDB, requestToCouchDB, scheduleTask } from "./utils";
|
||||||
|
import type { ButtonComponent } from "obsidian";
|
||||||
|
|
||||||
|
|
||||||
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
||||||
@@ -34,6 +35,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
containerEl.empty();
|
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.createEl("h2", { text: "Settings for Self-hosted LiveSync." });
|
||||||
containerEl.addClass("sls-setting");
|
containerEl.addClass("sls-setting");
|
||||||
containerEl.removeClass("isWizard");
|
containerEl.removeClass("isWizard");
|
||||||
@@ -746,8 +751,20 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.showStatusOnEditor).onChange(async (value) => {
|
||||||
this.plugin.settings.showStatusOnEditor = value;
|
this.plugin.settings.showStatusOnEditor = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
if (this.plugin.settings.showStatusOnEditor) {
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Show status as icons only")
|
||||||
|
.setDesc("")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.showOnlyIconsOnEditor).onChange(async (value) => {
|
||||||
|
this.plugin.settings.showOnlyIconsOnEditor = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
containerGeneralSettingsEl.createEl("h4", { text: "Logging" });
|
containerGeneralSettingsEl.createEl("h4", { text: "Logging" });
|
||||||
new Setting(containerGeneralSettingsEl)
|
new Setting(containerGeneralSettingsEl)
|
||||||
@@ -807,6 +824,53 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
text.inputEl.setAttribute("type", "number");
|
text.inputEl.setAttribute("type", "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
containerGeneralSettingsEl.createEl("h4", { text: "Share settings via markdown" });
|
||||||
|
let settingSyncFile = this.plugin.settings.settingSyncFile;
|
||||||
|
let buttonApplyFilename: ButtonComponent;
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Filename")
|
||||||
|
.setDesc("If you set this, all settings are saved in a markdown file. You will also be notified when new settings were arrived. You can set different files by the platform.")
|
||||||
|
.addText((text) => {
|
||||||
|
text.setPlaceholder("livesync/setting.md")
|
||||||
|
.setValue(settingSyncFile)
|
||||||
|
.onChange((value) => {
|
||||||
|
settingSyncFile = value;
|
||||||
|
if (settingSyncFile == this.plugin.settings.settingSyncFile) {
|
||||||
|
buttonApplyFilename.removeCta()
|
||||||
|
buttonApplyFilename.setDisabled(true);
|
||||||
|
} else {
|
||||||
|
buttonApplyFilename.setCta()
|
||||||
|
buttonApplyFilename.setDisabled(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).addButton(button => {
|
||||||
|
button.setButtonText("Apply")
|
||||||
|
.onClick(async () => {
|
||||||
|
this.plugin.settings.settingSyncFile = settingSyncFile;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
})
|
||||||
|
buttonApplyFilename = button;
|
||||||
|
})
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Write credentials in the file")
|
||||||
|
.setDesc("(Not recommended) If set, credentials will be stored in the file.")
|
||||||
|
.addToggle(toggle => {
|
||||||
|
toggle.setValue(this.plugin.settings.writeCredentialsForSettingSync)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.writeCredentialsForSettingSync = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
new Setting(containerGeneralSettingsEl)
|
||||||
|
.setName("Notify all setting files")
|
||||||
|
.addToggle(toggle => {
|
||||||
|
toggle.setValue(this.plugin.settings.notifyAllSettingSyncFile)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.notifyAllSettingSyncFile = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" });
|
containerGeneralSettingsEl.createEl("h4", { text: "Advanced Confidentiality" });
|
||||||
|
|
||||||
@@ -1104,7 +1168,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
);
|
);
|
||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Postpone resolution of unopened files")
|
.setName("Postpone resolution of inactive files")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addToggle((toggle) =>
|
.addToggle((toggle) =>
|
||||||
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
|
toggle.setValue(this.plugin.settings.checkConflictOnlyOnOpen).onChange(async (value) => {
|
||||||
@@ -1112,6 +1176,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
new Setting(containerSyncSettingEl)
|
||||||
|
.setName("Postpone manual resolution of inactive files")
|
||||||
|
.setClass("wizardHidden")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle.setValue(this.plugin.settings.showMergeDialogOnlyOnActive).onChange(async (value) => {
|
||||||
|
this.plugin.settings.showMergeDialogOnlyOnActive = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
|
containerSyncSettingEl.createEl("h4", { text: "Compatibility" }).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Always resolve conflict manually")
|
.setName("Always resolve conflict manually")
|
||||||
@@ -1217,7 +1290,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
let skipPatternTextArea: TextAreaComponent = null;
|
let skipPatternTextArea: TextAreaComponent = null;
|
||||||
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
const defaultSkipPattern = "\\/node_modules\\/, \\/\\.git\\/, \\/obsidian-livesync\\/";
|
||||||
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$";
|
const defaultSkipPatternXPlat = defaultSkipPattern + ",\\/workspace$ ,\\/workspace.json$,\\/workspace-mobile.json$";
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Folders and files to ignore")
|
.setName("Folders and files to ignore")
|
||||||
.setDesc(
|
.setDesc(
|
||||||
@@ -1301,11 +1374,35 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
});
|
});
|
||||||
|
|
||||||
containerSyncSettingEl.createEl("h4", {
|
containerSyncSettingEl.createEl("h4", {
|
||||||
text: sanitizeHTMLToDom(`Synchronization target filters`),
|
text: sanitizeHTMLToDom(`Targets`),
|
||||||
}).addClass("wizardHidden");
|
}).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Regular expression to ignore files")
|
.setName("Synchronising files")
|
||||||
.setDesc("If this is set, any changes to local and remote files that match this will be skipped.")
|
.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 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")
|
.setClass("wizardHidden")
|
||||||
.addTextArea((text) => {
|
.addTextArea((text) => {
|
||||||
text
|
text
|
||||||
@@ -1328,29 +1425,22 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Regular expression for restricting synchronization targets")
|
.setName("Maximum file size")
|
||||||
.setDesc("If this is set, changes to local and remote files that only match this will be processed.")
|
.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.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addTextArea((text) => {
|
.addText((text) => {
|
||||||
text
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.syncOnlyRegEx)
|
.setValue(this.plugin.settings.syncMaxSizeInMB + "")
|
||||||
.setPlaceholder("\\.md$|\\.txt")
|
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let isValidRegExp = false;
|
let v = Number(value);
|
||||||
try {
|
if (isNaN(v) || v < 1) {
|
||||||
new RegExp(value);
|
v = 0;
|
||||||
isValidRegExp = true;
|
|
||||||
} catch (_) {
|
|
||||||
// NO OP.
|
|
||||||
}
|
}
|
||||||
if (isValidRegExp || value.trim() == "") {
|
this.plugin.settings.syncMaxSizeInMB = v;
|
||||||
this.plugin.settings.syncOnlyRegEx = value;
|
await this.plugin.saveSettings();
|
||||||
await this.plugin.saveSettings();
|
});
|
||||||
}
|
text.inputEl.setAttribute("type", "number");
|
||||||
})
|
});
|
||||||
return text;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("(Beta) Use ignore files")
|
.setName("(Beta) Use ignore files")
|
||||||
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
|
.setDesc("If this is set, changes to local files which are matched by the ignore files will be skipped. Remote changes are determined using local ignore files.")
|
||||||
@@ -1391,15 +1481,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
}).addClass("wizardHidden");
|
}).addClass("wizardHidden");
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Batch size")
|
.setName("Batch size")
|
||||||
.setDesc("Number of change feed items to process at a time. Defaults to 50.")
|
.setDesc("Number of change feed items to process at a time. Defaults to 50. Minimum is 2.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.batch_size + "")
|
.setValue(this.plugin.settings.batch_size + "")
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let v = Number(value);
|
let v = Number(value);
|
||||||
if (isNaN(v) || v < 10) {
|
if (isNaN(v) || v < 2) {
|
||||||
v = 10;
|
v = 2;
|
||||||
}
|
}
|
||||||
this.plugin.settings.batch_size = v;
|
this.plugin.settings.batch_size = v;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -1409,15 +1499,15 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
new Setting(containerSyncSettingEl)
|
new Setting(containerSyncSettingEl)
|
||||||
.setName("Batch limit")
|
.setName("Batch limit")
|
||||||
.setDesc("Number of batches to process at a time. Defaults to 40. This along with batch size controls how many docs are kept in memory at a time.")
|
.setDesc("Number of batches to process at a time. Defaults to 40. Minimum is 2. This along with batch size controls how many docs are kept in memory at a time.")
|
||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder("")
|
text.setPlaceholder("")
|
||||||
.setValue(this.plugin.settings.batches_limit + "")
|
.setValue(this.plugin.settings.batches_limit + "")
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
let v = Number(value);
|
let v = Number(value);
|
||||||
if (isNaN(v) || v < 10) {
|
if (isNaN(v) || v < 2) {
|
||||||
v = 10;
|
v = 2;
|
||||||
}
|
}
|
||||||
this.plugin.settings.batches_limit = v;
|
this.plugin.settings.batches_limit = v;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
@@ -1514,8 +1604,10 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
pluginConfig.encryptedPassphrase = REDACTED;
|
pluginConfig.encryptedPassphrase = REDACTED;
|
||||||
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
pluginConfig.encryptedCouchDBConnection = REDACTED;
|
||||||
pluginConfig.pluginSyncExtendedSetting = {};
|
pluginConfig.pluginSyncExtendedSetting = {};
|
||||||
|
const obsidianInfo = navigator.userAgent;
|
||||||
const msgConfig = `----remote config----
|
const msgConfig = `---- Obsidian info ----
|
||||||
|
${obsidianInfo}
|
||||||
|
---- remote config ----
|
||||||
${stringifyYaml(responseConfig)}
|
${stringifyYaml(responseConfig)}
|
||||||
---- Plug-in config ---
|
---- Plug-in config ---
|
||||||
version:${manifestVersion}
|
version:${manifestVersion}
|
||||||
@@ -1632,7 +1724,7 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
if ((await this.plugin.localDatabase.putRaw(doc)).ok) {
|
||||||
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
Logger(`Old ${docName} has been deleted`, LOG_LEVEL_NOTICE);
|
||||||
}
|
}
|
||||||
await this.plugin.showIfConflicted(docName as FilePathWithPrefix);
|
await this.plugin.queueConflictCheck(docName as FilePathWithPrefix);
|
||||||
} else {
|
} else {
|
||||||
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
|
Logger(`Converting ${docName} Failed!`, LOG_LEVEL_NOTICE);
|
||||||
Logger(ret, LOG_LEVEL_VERBOSE);
|
Logger(ret, LOG_LEVEL_VERBOSE);
|
||||||
@@ -1813,7 +1905,7 @@ ${stringifyYaml(pluginConfig)}`;
|
|||||||
.setClass("wizardHidden")
|
.setClass("wizardHidden")
|
||||||
.addDropdown((dropdown) =>
|
.addDropdown((dropdown) =>
|
||||||
dropdown
|
dropdown
|
||||||
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)" } as Record<HashAlgorithm, string>)
|
.addOptions({ "": "Old Algorithm", "xxhash32": "xxhash32 (Fast)", "xxhash64": "xxhash64 (Fastest)", "sha1": "Fallback (Without WebAssembly)" } as Record<HashAlgorithm, string>)
|
||||||
.setValue(this.plugin.settings.hashAlg)
|
.setValue(this.plugin.settings.hashAlg)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.hashAlg = value as HashAlgorithm;
|
this.plugin.settings.hashAlg = value as HashAlgorithm;
|
||||||
|
|||||||
@@ -27,8 +27,10 @@
|
|||||||
async function requestReload() {
|
async function requestReload() {
|
||||||
await addOn.reloadPluginList(true);
|
await addOn.reloadPluginList(true);
|
||||||
}
|
}
|
||||||
|
let allTerms = [] as string[];
|
||||||
pluginList.subscribe((e) => {
|
pluginList.subscribe((e) => {
|
||||||
list = e;
|
list = e;
|
||||||
|
allTerms = unique(list.map((e) => e.term));
|
||||||
});
|
});
|
||||||
pluginIsEnumerating.subscribe((e) => {
|
pluginIsEnumerating.subscribe((e) => {
|
||||||
loading = e;
|
loading = e;
|
||||||
@@ -172,7 +174,7 @@
|
|||||||
.filter((e) => `${e.category}/${e.name}` == key)
|
.filter((e) => `${e.category}/${e.name}` == key)
|
||||||
.map((e) => e.files)
|
.map((e) => e.files)
|
||||||
.flat()
|
.flat()
|
||||||
.map((e) => e.filename)
|
.map((e) => e.filename),
|
||||||
);
|
);
|
||||||
automaticList.set(key, mode);
|
automaticList.set(key, mode);
|
||||||
automaticListDisp = automaticList;
|
automaticListDisp = automaticList;
|
||||||
@@ -218,6 +220,16 @@
|
|||||||
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
.sort((a, b) => (a.displayName ?? a.name).localeCompare(b.displayName ?? b.name))
|
||||||
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
.reduce((p, c) => ({ ...p, [c.category]: unique(c.category in p ? [...p[c.category], c.displayName ?? c.name] : [c.displayName ?? c.name]) }), {} as Record<string, string[]>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deleteTerm = "";
|
||||||
|
|
||||||
|
async function deleteAllItems(term: string) {
|
||||||
|
const deleteItems = list.filter((e) => e.term == term);
|
||||||
|
for (const item of deleteItems) {
|
||||||
|
await deleteData(item);
|
||||||
|
}
|
||||||
|
addOn.reloadPluginList(true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -322,6 +334,29 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if isMaintenanceMode}
|
||||||
|
<div class="list">
|
||||||
|
<div>
|
||||||
|
<h3>Maintenance Commands</h3>
|
||||||
|
<div class="maintenancerow">
|
||||||
|
<label for="">Delete All of </label>
|
||||||
|
<select bind:value={deleteTerm}>
|
||||||
|
{#each allTerms as term}
|
||||||
|
<option value={term}>{term}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="status"
|
||||||
|
on:click={(evt) => {
|
||||||
|
deleteAllItems(deleteTerm);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
<label><span>Hide not applicable items</span><input type="checkbox" bind:checked={hideEven} /></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -423,4 +458,13 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 3em;
|
min-height: 3em;
|
||||||
}
|
}
|
||||||
|
.maintenancerow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.maintenancerow label {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "./deps";
|
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "./deps";
|
||||||
import { serialized } from "./lib/src/lock";
|
import { serialized } from "./lib/src/lock";
|
||||||
import type { FilePath } from "./lib/src/types";
|
import type { FilePath } from "./lib/src/types";
|
||||||
|
import { createBinaryBlob, isDocContentSame } from "./lib/src/utils";
|
||||||
|
import type { InternalFileInfo } from "./types";
|
||||||
function getFileLockKey(file: TFile | TFolder | string) {
|
function getFileLockKey(file: TFile | TFolder | string) {
|
||||||
return `fl:${typeof (file) == "string" ? file : file.path}`;
|
return `fl:${typeof (file) == "string" ? file : file.path}`;
|
||||||
}
|
}
|
||||||
@@ -65,9 +67,22 @@ export class SerializedFileAccess {
|
|||||||
|
|
||||||
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
|
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions) {
|
||||||
if (typeof (data) === "string") {
|
if (typeof (data) === "string") {
|
||||||
return await serialized(getFileLockKey(file), () => this.app.vault.modify(file, data, options));
|
return await serialized(getFileLockKey(file), async () => {
|
||||||
|
const oldData = await this.app.vault.read(file);
|
||||||
|
if (data === oldData) return false
|
||||||
|
await this.app.vault.modify(file, data, options)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return await serialized(getFileLockKey(file), () => this.app.vault.modifyBinary(file, toArrayBuffer(data), options));
|
return await serialized(getFileLockKey(file), async () => {
|
||||||
|
const oldData = await this.app.vault.readBinary(file);
|
||||||
|
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.app.vault.modifyBinary(file, toArrayBuffer(data), options)
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions): Promise<TFile> {
|
async vaultCreate(path: string, data: string | ArrayBuffer | Uint8Array, options?: DataWriteOptions): Promise<TFile> {
|
||||||
@@ -107,8 +122,8 @@ export class SerializedFileAccess {
|
|||||||
this.touchedFiles.unshift(key);
|
this.touchedFiles.unshift(key);
|
||||||
this.touchedFiles = this.touchedFiles.slice(0, 100);
|
this.touchedFiles = this.touchedFiles.slice(0, 100);
|
||||||
}
|
}
|
||||||
recentlyTouched(file: TFile) {
|
recentlyTouched(file: TFile | InternalFileInfo) {
|
||||||
const key = `${file.path}-${file.stat.mtime}-${file.stat.size}`;
|
const key = file instanceof TFile ? `${file.path}-${file.stat.mtime}-${file.stat.size}` : `${file.path}-${file.mtime}-${file.size}`;
|
||||||
if (this.touchedFiles.indexOf(key) == -1) return false;
|
if (this.touchedFiles.indexOf(key) == -1) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import type { SerializedFileAccess } from "./SerializedFileAccess";
|
import type { SerializedFileAccess } from "./SerializedFileAccess";
|
||||||
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
import { Plugin, TAbstractFile, TFile, TFolder } from "./deps";
|
||||||
|
import { Logger } from "./lib/src/logger";
|
||||||
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
import { isPlainText, shouldBeIgnored } from "./lib/src/path";
|
||||||
import { getGlobalStore } from "./lib/src/store";
|
import type { KeyedQueueProcessor } from "./lib/src/processor";
|
||||||
import { type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
import { LOG_LEVEL_NOTICE, type FilePath, type ObsidianLiveSyncSettings } from "./lib/src/types";
|
||||||
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo, type queueItem } from "./types";
|
import { type FileEventItem, type FileEventType, type FileInfo, type InternalFileInfo } from "./types";
|
||||||
|
|
||||||
|
|
||||||
export abstract class StorageEventManager {
|
export abstract class StorageEventManager {
|
||||||
abstract fetchEvent(): FileEventItem | false;
|
abstract beginWatch(): void;
|
||||||
abstract cancelRelativeEvent(item: FileEventItem): void;
|
|
||||||
abstract getQueueLength(): number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type LiveSyncForStorageEventManager = Plugin &
|
type LiveSyncForStorageEventManager = Plugin &
|
||||||
@@ -19,19 +18,19 @@ type LiveSyncForStorageEventManager = Plugin &
|
|||||||
vaultAccess: SerializedFileAccess
|
vaultAccess: SerializedFileAccess
|
||||||
} & {
|
} & {
|
||||||
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
isTargetFile: (file: string | TAbstractFile) => Promise<boolean>,
|
||||||
procFileEvent: (applyBatch?: boolean) => Promise<any>,
|
fileEventQueue: KeyedQueueProcessor<FileEventItem, any>,
|
||||||
|
isFileSizeExceeded: (size: number) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class StorageEventManagerObsidian extends StorageEventManager {
|
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||||
plugin: LiveSyncForStorageEventManager;
|
plugin: LiveSyncForStorageEventManager;
|
||||||
queuedFilesStore = getGlobalStore("queuedFiles", { queuedItems: [] as queueItem[], fileEventItems: [] as FileEventItem[] });
|
|
||||||
|
|
||||||
watchedFileEventQueue = [] as FileEventItem[];
|
|
||||||
|
|
||||||
constructor(plugin: LiveSyncForStorageEventManager) {
|
constructor(plugin: LiveSyncForStorageEventManager) {
|
||||||
super();
|
super();
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
beginWatch() {
|
||||||
|
const plugin = this.plugin;
|
||||||
this.watchVaultChange = this.watchVaultChange.bind(this);
|
this.watchVaultChange = this.watchVaultChange.bind(this);
|
||||||
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
this.watchVaultCreate = this.watchVaultCreate.bind(this);
|
||||||
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
this.watchVaultDelete = this.watchVaultDelete.bind(this);
|
||||||
@@ -43,6 +42,7 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
|
plugin.registerEvent(plugin.app.vault.on("create", this.watchVaultCreate));
|
||||||
//@ts-ignore : Internal API
|
//@ts-ignore : Internal API
|
||||||
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
|
plugin.registerEvent(plugin.app.vault.on("raw", this.watchVaultRawEvents));
|
||||||
|
plugin.fileEventQueue.startPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||||
@@ -90,7 +90,6 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
}
|
}
|
||||||
// Cache file and waiting to can be proceed.
|
// Cache file and waiting to can be proceed.
|
||||||
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
async appendWatchEvent(params: { type: FileEventType, file: TAbstractFile | InternalFileInfo, oldPath?: string }[], ctx?: any) {
|
||||||
let forcePerform = false;
|
|
||||||
for (const param of params) {
|
for (const param of params) {
|
||||||
if (shouldBeIgnored(param.file.path)) {
|
if (shouldBeIgnored(param.file.path)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -99,6 +98,11 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
const type = param.type;
|
const type = param.type;
|
||||||
const file = param.file;
|
const file = param.file;
|
||||||
const oldPath = param.oldPath;
|
const oldPath = param.oldPath;
|
||||||
|
const size = file instanceof TFile ? file.stat.size : (file as InternalFileInfo)?.size ?? 0;
|
||||||
|
if (this.plugin.isFileSizeExceeded(size) && (type == "CREATE" || type == "CHANGED")) {
|
||||||
|
Logger(`The storage file has been changed but exceeds the maximum size. Skipping: ${param.file.path}`, LOG_LEVEL_NOTICE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (file instanceof TFolder) continue;
|
if (file instanceof TFolder) continue;
|
||||||
if (!await this.plugin.isTargetFile(file.path)) continue;
|
if (!await this.plugin.isTargetFile(file.path)) continue;
|
||||||
if (this.plugin.settings.suspendFileWatching) continue;
|
if (this.plugin.settings.suspendFileWatching) continue;
|
||||||
@@ -116,33 +120,6 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
if (!cache) cache = await this.plugin.vaultAccess.vaultRead(file);
|
if (!cache) cache = await this.plugin.vaultAccess.vaultRead(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type == "DELETE" || type == "RENAME") {
|
|
||||||
forcePerform = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (this.plugin.settings.batchSave && !this.plugin.settings.liveSync) {
|
|
||||||
// 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
|
|
||||||
// a.md MODIFY
|
|
||||||
// a.md CREATE
|
|
||||||
// :
|
|
||||||
let i = this.watchedFileEventQueue.length;
|
|
||||||
L1:
|
|
||||||
while (i >= 0) {
|
|
||||||
i--;
|
|
||||||
if (i < 0) break L1;
|
|
||||||
if (this.watchedFileEventQueue[i].args.file.path != file.path) {
|
|
||||||
continue L1;
|
|
||||||
}
|
|
||||||
if (this.watchedFileEventQueue[i].type != type) break L1;
|
|
||||||
this.watchedFileEventQueue.remove(this.watchedFileEventQueue[i]);
|
|
||||||
//this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
|
||||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileInfo = file instanceof TFile ? {
|
const fileInfo = file instanceof TFile ? {
|
||||||
ctime: file.stat.ctime,
|
ctime: file.stat.ctime,
|
||||||
mtime: file.stat.mtime,
|
mtime: file.stat.mtime,
|
||||||
@@ -150,7 +127,8 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
path: file.path,
|
path: file.path,
|
||||||
size: file.stat.size
|
size: file.stat.size
|
||||||
} as FileInfo : file as InternalFileInfo;
|
} as FileInfo : file as InternalFileInfo;
|
||||||
this.watchedFileEventQueue.push({
|
|
||||||
|
this.plugin.fileEventQueue.enqueueWithKey(`file-${fileInfo.path}`, {
|
||||||
type,
|
type,
|
||||||
args: {
|
args: {
|
||||||
file: fileInfo,
|
file: fileInfo,
|
||||||
@@ -161,21 +139,5 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
|||||||
key: atomicKey
|
key: atomicKey
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// this.queuedFilesStore.set({ queuedItems: this.queuedFiles, fileEventItems: this.watchedFileEventQueue });
|
|
||||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
|
||||||
this.plugin.procFileEvent(forcePerform);
|
|
||||||
}
|
|
||||||
fetchEvent(): FileEventItem | false {
|
|
||||||
if (this.watchedFileEventQueue.length == 0) return false;
|
|
||||||
const item = this.watchedFileEventQueue.shift();
|
|
||||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
cancelRelativeEvent(item: FileEventItem) {
|
|
||||||
this.watchedFileEventQueue = [...this.watchedFileEventQueue].filter(e => e.key != item.key);
|
|
||||||
this.queuedFilesStore.apply((value) => ({ ...value, fileEventItems: this.watchedFileEventQueue }));
|
|
||||||
}
|
|
||||||
getQueueLength() {
|
|
||||||
return this.watchedFileEventQueue.length;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
2
src/lib
2
src/lib
Submodule src/lib updated: 7e79c27035...ee376a80a5
1421
src/main.ts
1421
src/main.ts
File diff suppressed because it is too large
Load Diff
67
src/utils.ts
67
src/utils.ts
@@ -1,12 +1,15 @@
|
|||||||
import { normalizePath, TFile, Platform, TAbstractFile, App, Plugin, type RequestUrlParam, requestUrl } from "./deps";
|
import { normalizePath, Platform, TAbstractFile, App, type RequestUrlParam, requestUrl } from "./deps";
|
||||||
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
|
import { path2id_base, id2path_base, isValidFilenameInLinux, isValidFilenameInDarwin, isValidFilenameInWidows, isValidFilenameInAndroid, stripAllPrefixes } from "./lib/src/path";
|
||||||
|
|
||||||
import { Logger } from "./lib/src/logger";
|
import { Logger } from "./lib/src/logger";
|
||||||
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
import { LOG_LEVEL_VERBOSE, type AnyEntry, type DocumentID, type EntryHasPath, type FilePath, type FilePathWithPrefix } from "./lib/src/types";
|
||||||
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
|
import { CHeader, ICHeader, ICHeaderLength, ICXHeader, PSCHeader } from "./types";
|
||||||
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
import { InputStringDialog, PopoverSelectString } from "./dialogs";
|
||||||
import ObsidianLiveSyncPlugin from "./main";
|
import type ObsidianLiveSyncPlugin from "./main";
|
||||||
import { writeString } from "./lib/src/strbin";
|
import { writeString } from "./lib/src/strbin";
|
||||||
|
import { fireAndForget } from "./lib/src/utils";
|
||||||
|
|
||||||
|
export { scheduleTask, setPeriodicTask, cancelTask, cancelAllTasks, cancelPeriodicTask, cancelAllPeriodicTask, } from "./lib/src/task";
|
||||||
|
|
||||||
// For backward compatibility, using the path for determining id.
|
// For backward compatibility, using the path for determining id.
|
||||||
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
// Only CouchDB unacceptable ID (that starts with an underscore) has been prefixed with "/".
|
||||||
@@ -43,49 +46,6 @@ export function getPathFromTFile(file: TAbstractFile) {
|
|||||||
return file.path as FilePath;
|
return file.path as FilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tasks: { [key: string]: ReturnType<typeof setTimeout> } = {};
|
|
||||||
export function scheduleTask(key: string, timeout: number, proc: (() => Promise<any> | void), skipIfTaskExist?: boolean) {
|
|
||||||
if (skipIfTaskExist && key in tasks) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
cancelTask(key);
|
|
||||||
tasks[key] = setTimeout(async () => {
|
|
||||||
delete tasks[key];
|
|
||||||
await proc();
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
export function cancelTask(key: string) {
|
|
||||||
if (key in tasks) {
|
|
||||||
clearTimeout(tasks[key]);
|
|
||||||
delete tasks[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function cancelAllTasks() {
|
|
||||||
for (const v in tasks) {
|
|
||||||
clearTimeout(tasks[v]);
|
|
||||||
delete tasks[v];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const intervals: { [key: string]: ReturnType<typeof setInterval> } = {};
|
|
||||||
export function setPeriodicTask(key: string, timeout: number, proc: (() => Promise<any> | void)) {
|
|
||||||
cancelPeriodicTask(key);
|
|
||||||
intervals[key] = setInterval(async () => {
|
|
||||||
delete intervals[key];
|
|
||||||
await proc();
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
export function cancelPeriodicTask(key: string) {
|
|
||||||
if (key in intervals) {
|
|
||||||
clearInterval(intervals[key]);
|
|
||||||
delete intervals[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export function cancelAllPeriodicTask() {
|
|
||||||
for (const v in intervals) {
|
|
||||||
clearInterval(intervals[v]);
|
|
||||||
delete intervals[v];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const memos: { [key: string]: any } = {};
|
const memos: { [key: string]: any } = {};
|
||||||
export function memoObject<T>(key: string, obj: T): T {
|
export function memoObject<T>(key: string, obj: T): T {
|
||||||
@@ -377,8 +337,8 @@ export const askString = (app: App, title: string, key: string, placeholder: str
|
|||||||
export class PeriodicProcessor {
|
export class PeriodicProcessor {
|
||||||
_process: () => Promise<any>;
|
_process: () => Promise<any>;
|
||||||
_timer?: number;
|
_timer?: number;
|
||||||
_plugin: Plugin;
|
_plugin: ObsidianLiveSyncPlugin;
|
||||||
constructor(plugin: Plugin, process: () => Promise<any>) {
|
constructor(plugin: ObsidianLiveSyncPlugin, process: () => Promise<any>) {
|
||||||
this._plugin = plugin;
|
this._plugin = plugin;
|
||||||
this._process = process;
|
this._process = process;
|
||||||
}
|
}
|
||||||
@@ -392,12 +352,19 @@ export class PeriodicProcessor {
|
|||||||
enable(interval: number) {
|
enable(interval: number) {
|
||||||
this.disable();
|
this.disable();
|
||||||
if (interval == 0) return;
|
if (interval == 0) return;
|
||||||
this._timer = window.setInterval(() => this.process().then(() => { }), interval);
|
this._timer = window.setInterval(() => fireAndForget(async () => {
|
||||||
|
await this.process();
|
||||||
|
if (this._plugin._unloaded) {
|
||||||
|
this.disable();
|
||||||
|
}
|
||||||
|
}), interval);
|
||||||
this._plugin.registerInterval(this._timer);
|
this._plugin.registerInterval(this._timer);
|
||||||
}
|
}
|
||||||
disable() {
|
disable() {
|
||||||
if (this._timer !== undefined) window.clearInterval(this._timer);
|
if (this._timer !== undefined) {
|
||||||
this._timer = undefined;
|
window.clearInterval(this._timer);
|
||||||
|
this._timer = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
50
styles.css
50
styles.css
@@ -85,7 +85,6 @@
|
|||||||
|
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
|
||||||
.sls-header-button {
|
.sls-header-button {
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
}
|
}
|
||||||
@@ -99,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-wrap::before,
|
.CodeMirror-wrap::before,
|
||||||
.cm-s-obsidian>.cm-editor::before,
|
.cm-s-obsidian > .cm-editor::before,
|
||||||
.canvas-wrapper::before {
|
.canvas-wrapper::before {
|
||||||
content: attr(data-log);
|
content: attr(data-log);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -124,7 +123,7 @@
|
|||||||
right: 0px;
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-obsidian>.cm-editor::before {
|
.cm-s-obsidian > .cm-editor::before {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,8 +152,8 @@ div.sls-setting-menu-btn {
|
|||||||
/* width: 100%; */
|
/* width: 100%; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sls-setting-tab:hover~div.sls-setting-menu-btn,
|
.sls-setting-tab:hover ~ div.sls-setting-menu-btn,
|
||||||
.sls-setting-label.selected .sls-setting-tab:checked~div.sls-setting-menu-btn {
|
.sls-setting-label.selected .sls-setting-tab:checked ~ div.sls-setting-menu-btn {
|
||||||
background-color: var(--interactive-accent);
|
background-color: var(--interactive-accent);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
@@ -257,8 +256,8 @@ div.sls-setting-menu-btn {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-input > .setting-item-control >input {
|
.password-input > .setting-item-control > input {
|
||||||
-webkit-text-security: disc;
|
-webkit-text-security: disc;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.ls-mark-cr::after {
|
span.ls-mark-cr::after {
|
||||||
@@ -270,4 +269,39 @@ span.ls-mark-cr::after {
|
|||||||
|
|
||||||
.deleted span.ls-mark-cr::after {
|
.deleted span.ls-mark-cr::after {
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap .overlay {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ls-imgdiff-wrap .overlay .img-base {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.ls-imgdiff-wrap .overlay .img-overlay {
|
||||||
|
-webkit-filter: invert(100%) opacity(50%);
|
||||||
|
filter: invert(100%) opacity(50%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
animation: ls-blink-diff 0.5s cubic-bezier(0.4, 0, 1, 1) infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes ls-blink-diff {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"importHelpers": false,
|
"importHelpers": false,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2018",
|
"es2018",
|
||||||
"DOM",
|
"DOM",
|
||||||
|
|||||||
74
updates.md
74
updates.md
@@ -1,31 +1,59 @@
|
|||||||
### 0.21.0
|
### 0.22.0
|
||||||
The E2EE encryption V2 format has been reverted. That was probably the cause of the glitch.
|
A few years passed since Self-hosted LiveSync was born, and our codebase had been very complicated. This could be patient now, but it should be a tremendous hurt.
|
||||||
Instead, to maintain efficiency, files are treated with Blob until just before saving. Along with this, the old-fashioned encryption format has also been discontinued.
|
Therefore at v0.22.0, for future maintainability, I refined task scheduling logic totally.
|
||||||
There are both forward and backwards compatibilities, with recent versions. However, unfortunately, we lost compatibility with filesystem-livesync or some.
|
|
||||||
It will be addressed soon. Please be patient if you are using filesystem-livesync with E2EE.
|
|
||||||
|
|
||||||
|
Of course, I think this would be our suffering in some cases. However, I would love to ask you for your cooperation and contribution.
|
||||||
|
|
||||||
|
Sorry for being absent so much long. And thank you for your patience!
|
||||||
|
|
||||||
|
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
|
#### Version history
|
||||||
- 0.21.2
|
- 0.22.2
|
||||||
- IMPORTANT NOTICE: **0.21.1 CONTAINS A BUG WHILE REBUILDING THE DATABASE. IF YOU HAVE BEEN REBUILT, PLEASE MAKE SURE THAT ALL FILES ARE SANE.**
|
|
||||||
- This has been fixed in this version.
|
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- No longer files are broken while rebuilding.
|
- Now the results of resolving conflicts are surely synchronised.
|
||||||
- Now, Large binary files can be written correctly on a mobile platform.
|
|
||||||
- Any decoding errors now make zero-byte files.
|
|
||||||
- Modified:
|
- Modified:
|
||||||
- All files are processed sequentially for each.
|
- Some setting items got new clear names. (`Sync Settings` -> `Targets`).
|
||||||
- 0.21.1
|
- New feature:
|
||||||
|
- We can limit the synchronising files by their size. (`Sync Settings` -> `Targets` -> `Maximum file size`).
|
||||||
|
- It depends on the size of the newer one.
|
||||||
|
- At Obsidian 1.5.3 on mobile, we should set this to around 50MB to avoid restarting Obsidian.
|
||||||
|
- Now the settings could be stored in a specific markdown file to synchronise or switch it (`General Setting` -> `Share settings via markdown`).
|
||||||
|
- [Screwdriver](https://github.com/vrtmrz/obsidian-screwdriver) is quite good, but mostly we only need this.
|
||||||
|
- Customisation of the obsoleted device is now able to be deleted at once.
|
||||||
|
- We have to put the maintenance mode in at the Customisation sync dialogue.
|
||||||
|
- 0.22.1
|
||||||
|
- New feature:
|
||||||
|
- We can perform automatic conflict resolution for inactive files, and postpone only manual ones by `Postpone manual resolution of inactive files`.
|
||||||
|
- Now we can see the image in the document history dialogue.
|
||||||
|
- We can see the difference of the image, in the document history dialogue.
|
||||||
|
- And also we can highlight differences.
|
||||||
|
- Improved:
|
||||||
|
- Hidden file sync has been stabilised.
|
||||||
|
- Now automatically reloads the conflict-resolution dialogue when new conflicted revisions have arrived.
|
||||||
- Fixed:
|
- Fixed:
|
||||||
- No more infinity loops on larger files.
|
- No longer periodic process runs after unloading the plug-in.
|
||||||
- Show message on decode error.
|
- Now the modification of binary files is surely stored in the storage.
|
||||||
- Refactored:
|
- 0.22.0
|
||||||
- Fixed to avoid obsolete global variables.
|
- Refined:
|
||||||
- 0.21.0
|
- Task scheduling logics has been rewritten.
|
||||||
- Changes and performance improvements:
|
- Screen updates are also now efficient.
|
||||||
- Now the saving files are processed by Blob.
|
- Possibly many bugs and fragile behaviour has been fixed.
|
||||||
- The V2-Format has been reverted.
|
- Status updates and logging have been thinned out to display.
|
||||||
- New encoding format has been enabled in default.
|
- Fixed:
|
||||||
- WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost.
|
- Remote-chunk-fetching now works with keeping request intervals
|
||||||
|
- New feature:
|
||||||
|
- We can show only the icons in the editor.
|
||||||
|
- Progress indicators have been more meaningful:
|
||||||
|
- 📥 Unprocessed transferred items
|
||||||
|
- 📄 Working database operation
|
||||||
|
- 💾 Working write storage processes
|
||||||
|
- ⏳ Working read storage processes
|
||||||
|
- 🛫 Pending read storage processes
|
||||||
|
- ⚙️ Working or pending storage processes of hidden files
|
||||||
|
- 🧩 Waiting chunks
|
||||||
|
- 🔌 Working Customisation items (Configuration, snippets and plug-ins)
|
||||||
|
|
||||||
|
|
||||||
... To continue on to `updates_old.md`.
|
... To continue on to `updates_old.md`.
|
||||||
@@ -1,3 +1,43 @@
|
|||||||
|
### 0.21.0
|
||||||
|
The E2EE encryption V2 format has been reverted. That was probably the cause of the glitch.
|
||||||
|
Instead, to maintain efficiency, files are treated with Blob until just before saving. Along with this, the old-fashioned encryption format has also been discontinued.
|
||||||
|
There are both forward and backwards compatibilities, with recent versions. However, unfortunately, we lost compatibility with filesystem-livesync or some.
|
||||||
|
It will be addressed soon. Please be patient if you are using filesystem-livesync with E2EE.
|
||||||
|
|
||||||
|
- 0.21.5
|
||||||
|
- Improved:
|
||||||
|
- Now all revisions will be shown only its first a few letters.
|
||||||
|
- Now ID of the documents is shown in the log with the first 8 letters.
|
||||||
|
- Fixed:
|
||||||
|
- Check before modifying files has been implemented.
|
||||||
|
- Content change detection has been improved.
|
||||||
|
- 0.21.4
|
||||||
|
- This release had been skipped.
|
||||||
|
- 0.21.3
|
||||||
|
- Implemented:
|
||||||
|
- Now we can use SHA1 for hash function as fallback.
|
||||||
|
- 0.21.2
|
||||||
|
- IMPORTANT NOTICE: **0.21.1 CONTAINS A BUG WHILE REBUILDING THE DATABASE. IF YOU HAVE BEEN REBUILT, PLEASE MAKE SURE THAT ALL FILES ARE SANE.**
|
||||||
|
- This has been fixed in this version.
|
||||||
|
- Fixed:
|
||||||
|
- No longer files are broken while rebuilding.
|
||||||
|
- Now, Large binary files can be written correctly on a mobile platform.
|
||||||
|
- Any decoding errors now make zero-byte files.
|
||||||
|
- Modified:
|
||||||
|
- All files are processed sequentially for each.
|
||||||
|
- 0.21.1
|
||||||
|
- Fixed:
|
||||||
|
- No more infinity loops on larger files.
|
||||||
|
- Show message on decode error.
|
||||||
|
- Refactored:
|
||||||
|
- Fixed to avoid obsolete global variables.
|
||||||
|
- 0.21.0
|
||||||
|
- Changes and performance improvements:
|
||||||
|
- Now the saving files are processed by Blob.
|
||||||
|
- The V2-Format has been reverted.
|
||||||
|
- New encoding format has been enabled in default.
|
||||||
|
- WARNING: Since this version, the compatibilities with older Filesystem LiveSync have been lost.
|
||||||
|
|
||||||
## 0.20.0
|
## 0.20.0
|
||||||
At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation.
|
At 0.20.0, Self-hosted LiveSync has changed the binary file format and encrypting format, for efficient synchronisation.
|
||||||
The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices.
|
The dialogue will be shown and asks us to decide whether to keep v1 or use v2. Once we have enabled v2, all subsequent edits will be saved in v2. Therefore, devices running 0.19 or below cannot understand this and they might say that decryption error. Please update all devices.
|
||||||
|
|||||||
Reference in New Issue
Block a user