mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-03-12 12:58:49 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca5a7ae18c | ||
|
|
a27652ac34 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "0.25.19",
|
||||
"version": "0.25.20",
|
||||
"minAppVersion": "0.9.12",
|
||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.19",
|
||||
"version": "0.25.20",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.19",
|
||||
"version": "0.25.20",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "0.25.19",
|
||||
"version": "0.25.20",
|
||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: f021a9a6f4...21ca077163
@@ -15,10 +15,40 @@ import { TFileToUXFileInfoStub, TFolderToUXFileInfoStub } from "./storageLib/uti
|
||||
import { StorageEventManagerObsidian, type StorageEventManager } from "./storageLib/StorageEventManager";
|
||||
import type { StorageAccess } from "../interfaces/StorageAccess";
|
||||
import { createBlob, type CustomRegExp } from "../../lib/src/common/utils";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock_v2";
|
||||
|
||||
const fileLockPrefix = "file-lock:";
|
||||
|
||||
export class ModuleFileAccessObsidian extends AbstractObsidianModule implements IObsidianModule, StorageAccess {
|
||||
processingFiles: Set<FilePathWithPrefix> = new Set();
|
||||
processWriteFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T> {
|
||||
const path = typeof file === "string" ? file : file.path;
|
||||
return serialized(`${fileLockPrefix}${path}`, async () => {
|
||||
try {
|
||||
this.processingFiles.add(path);
|
||||
return await proc();
|
||||
} finally {
|
||||
this.processingFiles.delete(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
processReadFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T> {
|
||||
const path = typeof file === "string" ? file : file.path;
|
||||
return serialized(`${fileLockPrefix}${path}`, async () => {
|
||||
try {
|
||||
this.processingFiles.add(path);
|
||||
return await proc();
|
||||
} finally {
|
||||
this.processingFiles.delete(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean {
|
||||
const path = typeof file === "string" ? file : file.path;
|
||||
return this.processingFiles.has(path);
|
||||
}
|
||||
vaultAccess!: SerializedFileAccess;
|
||||
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core);
|
||||
vaultManager: StorageEventManager = new StorageEventManagerObsidian(this.plugin, this.core, this);
|
||||
$everyOnload(): Promise<boolean> {
|
||||
this.core.storageAccess = this;
|
||||
return Promise.resolve(true);
|
||||
@@ -42,7 +72,7 @@ export class ModuleFileAccessObsidian extends AbstractObsidianModule implements
|
||||
}
|
||||
|
||||
$everyOnloadStart(): Promise<boolean> {
|
||||
this.vaultAccess = new SerializedFileAccess(this.app, this.plugin);
|
||||
this.vaultAccess = new SerializedFileAccess(this.app, this.plugin, this);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { type App, TFile, type DataWriteOptions, TFolder, TAbstractFile } from "../../../deps.ts";
|
||||
import { serialized } from "octagonal-wheels/concurrency/lock";
|
||||
import { Logger } from "../../../lib/src/common/logger.ts";
|
||||
import { isPlainText } from "../../../lib/src/string_and_binary/path.ts";
|
||||
import type { FilePath, HasSettings, UXFileInfoStub } from "../../../lib/src/common/types.ts";
|
||||
import { createBinaryBlob, isDocContentSame } from "../../../lib/src/common/utils.ts";
|
||||
import type { InternalFileInfo } from "../../../common/types.ts";
|
||||
import { markChangesAreSame } from "../../../common/utils.ts";
|
||||
import { type UXFileInfo } from "../../../lib/src/common/types.ts";
|
||||
|
||||
function getFileLockKey(file: TFile | TFolder | string | UXFileInfo) {
|
||||
return `fl:${typeof file == "string" ? file : file.path}`;
|
||||
}
|
||||
import type { StorageAccess } from "../../interfaces/StorageAccess.ts";
|
||||
function toArrayBuffer(arr: Uint8Array<ArrayBuffer> | ArrayBuffer | DataView<ArrayBuffer>): ArrayBuffer {
|
||||
if (arr instanceof Uint8Array) {
|
||||
return arr.buffer;
|
||||
@@ -21,60 +16,55 @@ function toArrayBuffer(arr: Uint8Array<ArrayBuffer> | ArrayBuffer | DataView<Arr
|
||||
return arr;
|
||||
}
|
||||
|
||||
// function isFile(file: TFile | TFolder | string | UXFileInfo): boolean {
|
||||
// file instanceof TFile;
|
||||
// }
|
||||
|
||||
async function processReadFile<T>(file: TFile | TFolder | string | UXFileInfo, proc: () => Promise<T>) {
|
||||
const ret = await serialized(getFileLockKey(file), () => proc());
|
||||
return ret;
|
||||
}
|
||||
async function processWriteFile<T>(file: TFile | TFolder | string | UXFileInfo, proc: () => Promise<T>) {
|
||||
const ret = await serialized(getFileLockKey(file), () => proc());
|
||||
return ret;
|
||||
}
|
||||
|
||||
export class SerializedFileAccess {
|
||||
app: App;
|
||||
plugin: HasSettings<{ handleFilenameCaseSensitive: boolean }>;
|
||||
constructor(app: App, plugin: (typeof this)["plugin"]) {
|
||||
storageAccess: StorageAccess;
|
||||
constructor(app: App, plugin: SerializedFileAccess["plugin"], storageAccess: StorageAccess) {
|
||||
this.app = app;
|
||||
this.plugin = plugin;
|
||||
this.storageAccess = storageAccess;
|
||||
}
|
||||
|
||||
async tryAdapterStat(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, async () => {
|
||||
return await this.storageAccess.processReadFile(path as FilePath, async () => {
|
||||
if (!(await this.app.vault.adapter.exists(path))) return null;
|
||||
return this.app.vault.adapter.stat(path);
|
||||
});
|
||||
}
|
||||
async adapterStat(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, () => this.app.vault.adapter.stat(path));
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.stat(path));
|
||||
}
|
||||
async adapterExists(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, () => this.app.vault.adapter.exists(path));
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.exists(path));
|
||||
}
|
||||
async adapterRemove(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, () => this.app.vault.adapter.remove(path));
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.remove(path));
|
||||
}
|
||||
|
||||
async adapterRead(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, () => this.app.vault.adapter.read(path));
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.read(path));
|
||||
}
|
||||
async adapterReadBinary(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
return await processReadFile(file, () => this.app.vault.adapter.readBinary(path));
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () =>
|
||||
this.app.vault.adapter.readBinary(path)
|
||||
);
|
||||
}
|
||||
|
||||
async adapterReadAuto(file: TFile | string) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
if (isPlainText(path)) return await processReadFile(file, () => this.app.vault.adapter.read(path));
|
||||
return await processReadFile(file, () => this.app.vault.adapter.readBinary(path));
|
||||
if (isPlainText(path)) {
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.adapter.read(path));
|
||||
}
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () =>
|
||||
this.app.vault.adapter.readBinary(path)
|
||||
);
|
||||
}
|
||||
|
||||
async adapterWrite(
|
||||
@@ -84,35 +74,39 @@ export class SerializedFileAccess {
|
||||
) {
|
||||
const path = file instanceof TFile ? file.path : file;
|
||||
if (typeof data === "string") {
|
||||
return await processWriteFile(file, () => this.app.vault.adapter.write(path, data, options));
|
||||
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||
this.app.vault.adapter.write(path, data, options)
|
||||
);
|
||||
} else {
|
||||
return await processWriteFile(file, () =>
|
||||
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||
this.app.vault.adapter.writeBinary(path, toArrayBuffer(data), options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async vaultCacheRead(file: TFile) {
|
||||
return await processReadFile(file, () => this.app.vault.cachedRead(file));
|
||||
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.cachedRead(file));
|
||||
}
|
||||
|
||||
async vaultRead(file: TFile) {
|
||||
return await processReadFile(file, () => this.app.vault.read(file));
|
||||
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.read(file));
|
||||
}
|
||||
|
||||
async vaultReadBinary(file: TFile) {
|
||||
return await processReadFile(file, () => this.app.vault.readBinary(file));
|
||||
return await this.storageAccess.processReadFile(file.path as FilePath, () => this.app.vault.readBinary(file));
|
||||
}
|
||||
|
||||
async vaultReadAuto(file: TFile) {
|
||||
const path = file.path;
|
||||
if (isPlainText(path)) return await processReadFile(file, () => this.app.vault.read(file));
|
||||
return await processReadFile(file, () => this.app.vault.readBinary(file));
|
||||
if (isPlainText(path)) {
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.read(file));
|
||||
}
|
||||
return await this.storageAccess.processReadFile(path as FilePath, () => this.app.vault.readBinary(file));
|
||||
}
|
||||
|
||||
async vaultModify(file: TFile, data: string | ArrayBuffer | Uint8Array<ArrayBuffer>, options?: DataWriteOptions) {
|
||||
if (typeof data === "string") {
|
||||
return await processWriteFile(file, async () => {
|
||||
return await this.storageAccess.processWriteFile(file.path as FilePath, async () => {
|
||||
const oldData = await this.app.vault.read(file);
|
||||
if (data === oldData) {
|
||||
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
||||
@@ -122,7 +116,7 @@ export class SerializedFileAccess {
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
return await processWriteFile(file, async () => {
|
||||
return await this.storageAccess.processWriteFile(file.path as FilePath, async () => {
|
||||
const oldData = await this.app.vault.readBinary(file);
|
||||
if (await isDocContentSame(createBinaryBlob(oldData), createBinaryBlob(data))) {
|
||||
if (options && options.mtime) markChangesAreSame(file.path, file.stat.mtime, options.mtime);
|
||||
@@ -139,9 +133,13 @@ export class SerializedFileAccess {
|
||||
options?: DataWriteOptions
|
||||
): Promise<TFile> {
|
||||
if (typeof data === "string") {
|
||||
return await processWriteFile(path, () => this.app.vault.create(path, data, options));
|
||||
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||
this.app.vault.create(path, data, options)
|
||||
);
|
||||
} else {
|
||||
return await processWriteFile(path, () => this.app.vault.createBinary(path, toArrayBuffer(data), options));
|
||||
return await this.storageAccess.processWriteFile(path as FilePath, () =>
|
||||
this.app.vault.createBinary(path, toArrayBuffer(data), options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,10 +152,14 @@ export class SerializedFileAccess {
|
||||
}
|
||||
|
||||
async delete(file: TFile | TFolder, force = false) {
|
||||
return await processWriteFile(file, () => this.app.vault.delete(file, force));
|
||||
return await this.storageAccess.processWriteFile(file.path as FilePath, () =>
|
||||
this.app.vault.delete(file, force)
|
||||
);
|
||||
}
|
||||
async trash(file: TFile | TFolder, force = false) {
|
||||
return await processWriteFile(file, () => this.app.vault.trash(file, force));
|
||||
return await this.storageAccess.processWriteFile(file.path as FilePath, () =>
|
||||
this.app.vault.trash(file, force)
|
||||
);
|
||||
}
|
||||
|
||||
isStorageInsensitive(): boolean {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import type { LiveSyncCore } from "../../../main.ts";
|
||||
import { InternalFileToUXFileInfoStub, TFileToUXFileInfoStub } from "./utilObsidian.ts";
|
||||
import ObsidianLiveSyncPlugin from "../../../main.ts";
|
||||
import type { StorageAccess } from "../../interfaces/StorageAccess.ts";
|
||||
// import { InternalFileToUXFileInfo } from "../platforms/obsidian.ts";
|
||||
|
||||
export type FileEvent = {
|
||||
@@ -46,6 +47,7 @@ export abstract class StorageEventManager {
|
||||
export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
plugin: ObsidianLiveSyncPlugin;
|
||||
core: LiveSyncCore;
|
||||
storageAccess: StorageAccess;
|
||||
|
||||
get shouldBatchSave() {
|
||||
return this.core.settings?.batchSave && this.core.settings?.liveSync != true;
|
||||
@@ -56,8 +58,9 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
get batchSaveMaximumDelay(): number {
|
||||
return this.core.settings?.batchSaveMaximumDelay ?? DEFAULT_SETTINGS.batchSaveMaximumDelay;
|
||||
}
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore) {
|
||||
constructor(plugin: ObsidianLiveSyncPlugin, core: LiveSyncCore, storageAccess: StorageAccess) {
|
||||
super();
|
||||
this.storageAccess = storageAccess;
|
||||
this.plugin = plugin;
|
||||
this.core = core;
|
||||
}
|
||||
@@ -88,6 +91,10 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
}
|
||||
const file = info?.file as TFile;
|
||||
if (!file) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// Logger(`Editor change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
if (!this.isWaiting(file.path as FilePath)) {
|
||||
return;
|
||||
}
|
||||
@@ -102,22 +109,35 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
|
||||
watchVaultCreate(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// Logger(`File create skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file);
|
||||
void this.appendQueue([{ type: "CREATE", file: fileInfo }], ctx);
|
||||
}
|
||||
|
||||
watchVaultChange(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// Logger(`File change skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file);
|
||||
void this.appendQueue([{ type: "CHANGED", file: fileInfo }], ctx);
|
||||
}
|
||||
|
||||
watchVaultDelete(file: TAbstractFile, ctx?: any) {
|
||||
if (file instanceof TFolder) return;
|
||||
if (this.storageAccess.isFileProcessing(file.path as FilePath)) {
|
||||
// Logger(`File delete skipped because the file is being processed: ${file.path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
const fileInfo = TFileToUXFileInfoStub(file, true);
|
||||
void this.appendQueue([{ type: "DELETE", file: fileInfo }], ctx);
|
||||
}
|
||||
watchVaultRename(file: TAbstractFile, oldFile: string, ctx?: any) {
|
||||
// vault Rename will not be raised for self-events (Self-hosted LiveSync will not handle 'rename').
|
||||
if (file instanceof TFile) {
|
||||
const fileInfo = TFileToUXFileInfoStub(file);
|
||||
void this.appendQueue(
|
||||
@@ -145,6 +165,10 @@ export class StorageEventManagerObsidian extends StorageEventManager {
|
||||
}
|
||||
// Watch raw events (Internal API)
|
||||
watchVaultRawEvents(path: FilePath) {
|
||||
if (this.storageAccess.isFileProcessing(path)) {
|
||||
// Logger(`Raw file event skipped because the file is being processed: ${path}`, LOG_LEVEL_VERBOSE);
|
||||
return;
|
||||
}
|
||||
// Only for internal files.
|
||||
if (!this.plugin.settings) return;
|
||||
// if (this.plugin.settings.useIgnoreFiles && this.plugin.ignoreFiles.some(e => path.endsWith(e.trim()))) {
|
||||
|
||||
@@ -101,6 +101,23 @@ export function paneRemoteConfig(
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgIfConfigNotPersistent"), ["ob-btn-config-info"]);
|
||||
addResult($msg("obsidianLiveSyncSettingTab.msgConfigCheck"), ["ob-btn-config-head"]);
|
||||
|
||||
const serverBanner = r.headers["server"] ?? r.headers["Server"] ?? "unknown";
|
||||
addResult($msg("obsidianLiveSyncSettingTab.serverVersion", { info: serverBanner }));
|
||||
const versionMatch = serverBanner.match(/CouchDB(\/([0-9.]+))?/);
|
||||
const versionStr = versionMatch ? versionMatch[2] : "0.0.0";
|
||||
const versionParts = `${versionStr}.0.0.0`.split(".");
|
||||
// Compare version string with the target version.
|
||||
// version must be a string like "3.2.1" or "3.10.2", and must be two or three parts.
|
||||
function isGreaterThanOrEqual(version: string) {
|
||||
const targetParts = version.split(".");
|
||||
for (let i = 0; i < targetParts.length; i++) {
|
||||
// compare as number if possible (so 3.10 > 3.2, 3.10.1b > 3.10.1a)
|
||||
const result = versionParts[i].localeCompare(targetParts[i], undefined, { numeric: true });
|
||||
if (result > 0) return true;
|
||||
if (result < 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Admin check
|
||||
// for database creation and deletion
|
||||
if (!(this.editingSettings.couchDB_USER in responseConfig.admins)) {
|
||||
@@ -108,28 +125,31 @@ export function paneRemoteConfig(
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okAdminPrivileges"));
|
||||
}
|
||||
// HTTP user-authorization check
|
||||
if (responseConfig?.chttpd?.require_valid_user != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUser"));
|
||||
addConfigFixButton(
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"),
|
||||
"chttpd/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
if (isGreaterThanOrEqual("3.2.0")) {
|
||||
// HTTP user-authorization check
|
||||
if (responseConfig?.chttpd?.require_valid_user != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUser"));
|
||||
addConfigFixButton(
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUser"),
|
||||
"chttpd/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUser"));
|
||||
}
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUser"));
|
||||
}
|
||||
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth"));
|
||||
addConfigFixButton(
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"),
|
||||
"chttpd_auth/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth"));
|
||||
if (responseConfig?.chttpd_auth?.require_valid_user != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errRequireValidUserAuth"));
|
||||
addConfigFixButton(
|
||||
$msg("obsidianLiveSyncSettingTab.msgSetRequireValidUserAuth"),
|
||||
"chttpd_auth/require_valid_user",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okRequireValidUserAuth"));
|
||||
}
|
||||
}
|
||||
// HTTPD check
|
||||
// Check Authentication header
|
||||
@@ -144,12 +164,26 @@ export function paneRemoteConfig(
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okWwwAuth"));
|
||||
}
|
||||
if (responseConfig?.httpd?.enable_cors != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errEnableCors"));
|
||||
addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true");
|
||||
if (isGreaterThanOrEqual("3.2.0")) {
|
||||
if (responseConfig?.chttpd?.enable_cors != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errEnableCorsChttpd"));
|
||||
addConfigFixButton(
|
||||
$msg("obsidianLiveSyncSettingTab.msgEnableCorsChttpd"),
|
||||
"chttpd/enable_cors",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okEnableCorsChttpd"));
|
||||
}
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okEnableCors"));
|
||||
if (responseConfig?.httpd?.enable_cors != "true") {
|
||||
isSuccessful = false;
|
||||
addResult($msg("obsidianLiveSyncSettingTab.errEnableCors"));
|
||||
addConfigFixButton($msg("obsidianLiveSyncSettingTab.msgEnableCors"), "httpd/enable_cors", "true");
|
||||
} else {
|
||||
addResult($msg("obsidianLiveSyncSettingTab.okEnableCors"));
|
||||
}
|
||||
}
|
||||
// If the server is not cloudant, configure request size
|
||||
if (!isCloudantURI(this.editingSettings.couchDB_URI)) {
|
||||
|
||||
@@ -10,6 +10,10 @@ import type {
|
||||
import type { CustomRegExp } from "../../lib/src/common/utils";
|
||||
|
||||
export interface StorageAccess {
|
||||
processWriteFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T>;
|
||||
processReadFile<T>(file: UXFileInfoStub | FilePathWithPrefix, proc: () => Promise<T>): Promise<T>;
|
||||
isFileProcessing(file: UXFileInfoStub | FilePathWithPrefix): boolean;
|
||||
|
||||
deleteVaultItem(file: FilePathWithPrefix | UXFileInfoStub | UXFolderInfo): Promise<void>;
|
||||
|
||||
writeFileAuto(path: string, data: string | ArrayBuffer, opt?: UXDataWriteOptions): Promise<boolean>;
|
||||
|
||||
32
updates.md
32
updates.md
@@ -8,6 +8,22 @@ I have now rewritten the E2EE code to be more robust and easier to understand. I
|
||||
|
||||
As a result, this is the first time in a while that forward compatibility has been broken. We have also taken the opportunity to change all metadata to use encryption rather than obfuscation. Furthermore, the `Dynamic Iteration Count` setting is now redundant and has been moved to the `Patches` pane in the settings. Thanks to Rabin-Karp, the eden setting is also no longer necessary and has been relocated accordingly. Therefore, v0.25.0 represents a legitimate and correct evolution.
|
||||
|
||||
## 0.25.20
|
||||
|
||||
26th September, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Chunk fetching no longer reports errors when the fetched chunk could not be saved (#710).
|
||||
- Just using the fetched chunk temporarily.
|
||||
- Chunk fetching reports errors when the fetched chunk is surely corrupted (#710, #712).
|
||||
- It no longer detects files that the plug-in has modified.
|
||||
- It may reduce unnecessary file comparisons and unexpected file states.
|
||||
|
||||
### Improved
|
||||
|
||||
- Now checking the remote database configuration respecting the CouchDB version (#714).
|
||||
|
||||
## 0.25.19
|
||||
|
||||
18th September, 2025
|
||||
@@ -78,21 +94,5 @@ As a result, this is the first time in a while that forward compatibility has be
|
||||
- Now informs us about conflicted files as non-recoverable, but noted so.
|
||||
- No longer errors on not-found files.
|
||||
|
||||
## 0.25.13
|
||||
|
||||
1st September, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Conflict resolving dialogue now properly displays the changeset name instead of A or B (#691).
|
||||
|
||||
## 0.25.12
|
||||
|
||||
29th August, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with automatic synchronisation starting (#702).
|
||||
|
||||
Older notes are in
|
||||
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
|
||||
|
||||
@@ -10,6 +10,22 @@ As a result, this is the first time in a while that forward compatibility has be
|
||||
|
||||
---
|
||||
|
||||
## 0.25.13
|
||||
|
||||
1st September, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Conflict resolving dialogue now properly displays the changeset name instead of A or B (#691).
|
||||
|
||||
## 0.25.12
|
||||
|
||||
29th August, 2025
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with automatic synchronisation starting (#702).
|
||||
|
||||
## 0.25.11
|
||||
|
||||
28th August, 2025
|
||||
@@ -20,7 +36,6 @@ As a result, this is the first time in a while that forward compatibility has be
|
||||
- No errors are shown during synchronisations in offline (if not explicitly requested) (#699).
|
||||
- Missing some checking during automatic-synchronisation now works correctly.
|
||||
|
||||
|
||||
## 0.25.10
|
||||
|
||||
26th August, 2025
|
||||
@@ -109,7 +124,6 @@ In next version, insecure chunk detection will be implemented.
|
||||
- Files prefixed `bgWorker.` are now also controls these worker threads. (I know what you want to say... I will rename them).
|
||||
- Removed unused code.
|
||||
|
||||
|
||||
## ~~0.25.5~~ 0.25.6
|
||||
|
||||
(0.25.5 has been withdrawn due to a bug in the `Fetch chunks on demand` feature).
|
||||
@@ -210,7 +224,6 @@ The PBKDF2Salt will be referred to as the `Security Seed`, and it is used to der
|
||||
- The Set-up URI is now encrypted with a new encryption algorithm (mostly the same as `V2`).
|
||||
- The new Set-up URI is not compatible with version 0.24.x or earlier.
|
||||
|
||||
|
||||
## 0.25.0
|
||||
|
||||
### Fixed
|
||||
@@ -428,7 +441,6 @@ However, just to whisper, this is tremendously fast.
|
||||
- Dependent libraries have been updated to the latest version.
|
||||
- Some build processes have been separated to `pre` and `post` processes.
|
||||
|
||||
|
||||
## 0.24.25
|
||||
|
||||
22nd April, 2025
|
||||
@@ -506,8 +518,8 @@ However, just to whisper, this is tremendously fast.
|
||||
|
||||
- No longer conflicted files are handled in the boot-up process. No more
|
||||
unexpected overwriting.
|
||||
- It ignores `Always overwrite with a newer file`, and always be prevented for
|
||||
the safety. Please pick it manually or open the file.
|
||||
- It ignores `Always overwrite with a newer file`, and always be prevented for
|
||||
the safety. Please pick it manually or open the file.
|
||||
- Some log messages on conflict resolution has been corrected.
|
||||
- Automatic merge notifications, displayed on the grounds of `same`, have been
|
||||
degraded to logs.
|
||||
@@ -516,8 +528,8 @@ However, just to whisper, this is tremendously fast.
|
||||
|
||||
- Now we can fetch the remote database with keeping local files completely
|
||||
intact.
|
||||
- In new option, all files are stored into the local database before the
|
||||
fetching, and will be merged automatically or detected as conflicts.
|
||||
- In new option, all files are stored into the local database before the
|
||||
fetching, and will be merged automatically or detected as conflicts.
|
||||
- The dialogue presenting options when performing `Fetch` are now more
|
||||
informative.
|
||||
|
||||
@@ -539,8 +551,8 @@ However, just to whisper, this is tremendously fast.
|
||||
|
||||
- **NOW INDEED AND ACTUALLY** `Compute revisions for chunks` are backed into
|
||||
enabled again. it is necessary for garbage collection of chunks.
|
||||
- As far as existing users are concerned, this will not automatically change,
|
||||
but the Doctor will inform us.
|
||||
- As far as existing users are concerned, this will not automatically change,
|
||||
but the Doctor will inform us.
|
||||
|
||||
## 0.24.19
|
||||
|
||||
@@ -581,7 +593,6 @@ Confession. I got the default values wrong. So scary and sorry.
|
||||
|
||||
## 0.24.16
|
||||
|
||||
|
||||
### Improved
|
||||
|
||||
#### Peer-to-Peer
|
||||
@@ -662,7 +673,6 @@ Sorry for the lack of replies. The ones that were not good are popping up, so I
|
||||
- Displaying replication status of the peer-to-peer synchronisation is separated from the main-log-logic.
|
||||
- Some file names have been changed to be more consistent.
|
||||
|
||||
|
||||
## 0.24.12
|
||||
|
||||
I created a SPA called [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) (well, right... I will think of a name again), which replaces the server when using Peer-to-Peer synchronisation. This is a pseudo-client that appears to other devices as if it were one of the clients. . As with the client, it receives and sends data without storing it as a file.
|
||||
@@ -678,7 +688,6 @@ And, this is just a single web page, without any server-side code. It is a stati
|
||||
- And you can see the actual usage of this on [webpeer](https://github.com/vrtmrz/livesync-commonlib/tree/main/apps/webpeer) that a pseudo client for peer-to-peer synchronisation.
|
||||
- Some UIs have been got isomorphic among Obsidian and web applications (for `webpeer`).
|
||||
|
||||
|
||||
## 0.24.11
|
||||
|
||||
Peer-to-peer synchronisation has been implemented!
|
||||
|
||||
Reference in New Issue
Block a user