Compare commits

..
10 Commits
Author SHA1 Message Date
fyears 9aa1bd821e bump to 0.5.12
Release A New Version / build (16.x) (push) Failing after 45s
2024-06-29 18:34:11 +08:00
fyears a967d6ffd9 move onedrive full to below, and add separator 2024-06-29 18:33:41 +08:00
fyears a2ce95441a download at best effort 2024-06-29 00:43:01 +08:00
fyears e74e49538e more logic for potentially the same files in smart conflict 2024-06-29 00:14:53 +08:00
fyears 11c66d7030 fix smart conflict for .obsidian 2024-06-28 23:17:41 +08:00
fyears 56bd4c000e Merge branch 'master' of https://github.com/fyears/remotely-save 2024-06-28 23:06:33 +08:00
fyears a7cc4e2d7e more robust for rclone 2024-06-28 23:06:14 +08:00
fyears f306445e0d save resources 2024-06-28 23:05:22 +08:00
lyiton 9f4de49495 修改一处细节 (#720)
* Update zh_cn.json

* Update zh_cn.json

* Update zh_tw.json
2024-06-24 22:52:25 +08:00
fyears 54c25a70ed add pro tag to onedrive 2024-06-23 18:09:48 +08:00
13 changed files with 295 additions and 78 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.5.11",
"version": "0.5.12",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "remotely-save",
"name": "Remotely Save",
"version": "0.5.11",
"version": "0.5.12",
"minAppVersion": "0.13.21",
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
"author": "fyears",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "remotely-save",
"version": "0.5.11",
"version": "0.5.12",
"description": "This is yet another sync plugin for Obsidian app.",
"scripts": {
"dev2": "node esbuild.config.mjs --watch",
+150 -30
View File
@@ -200,22 +200,141 @@ export function getFileRename(key: string) {
return res;
}
function arraysAreEqual(arr1: ArrayBuffer, arr2: ArrayBuffer) {
if (arr1.byteLength !== arr2.byteLength) {
return false;
}
const u1 = new Uint8Array(arr1);
const u2 = new Uint8Array(arr2);
for (let i = 0; i < u1.byteLength; ++i) {
if (u1[i] !== u2[i]) {
return false;
}
}
return true;
}
/**
* 1. download remote
* 2. compare
* 3. if the same, update local but not upload
* 4. if not the same, rename local and save remote
*/
async function tryDuplicateFileForSameSizes(
key: string,
key2: string,
fsLocal: FakeFs,
fsRemote: FakeFs,
uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) {
console.debug(`tryDuplicateFileForSameSizes: ${key}`);
// 1. download
const remoteContent = await fsRemote.readFile(key);
// 2. compare
const localContent = await fsLocal.readFile(key);
const eq = arraysAreEqual(localContent, remoteContent);
if (eq) {
// 3. if the same, update local but not upload
// read meta of remote, as if we have downloaded the file
console.debug(`tryDuplicateFileForSameSizes: ${key} content equal`);
const entityRemote = await fsRemote.stat(key);
// write
const downloadResultEntity = await fsLocal.writeFile(
key,
remoteContent,
entityRemote.mtimeCli ?? Date.now(),
entityRemote.mtimeCli ?? Date.now()
);
await downloadCallback(downloadResultEntity);
// no uploadCallback here
} else {
// 4. if not the same, rename local and save remote
console.debug(`tryDuplicateFileForSameSizes: ${key} content not equal`);
await fsLocal.rename(key, key2);
const entityRemote = await fsRemote.stat(key);
const downloadResultEntity = await fsLocal.writeFile(
key,
remoteContent,
entityRemote.mtimeCli ?? Date.now(),
entityRemote.mtimeCli ?? Date.now()
);
await downloadCallback(downloadResultEntity);
const entityLocal = await fsLocal.stat(key2); // key2 here!
const uploadResultEntity = await fsRemote.writeFile(
key2, // key2 here!
localContent,
entityLocal.mtimeCli ?? Date.now(),
entityLocal.mtimeCli ?? Date.now()
);
await uploadCallback(uploadResultEntity);
}
}
/**
* local: x.md -> x.dup.md -> upload to remote
* remote: x.md -> download to local -> using original name x.md
*/
export async function duplicateFile(
async function tryDuplicateFileForDiffSizes(
key: string,
left: FakeFs,
right: FakeFs,
uploadCallback: (entity: Entity) => Promise<any>,
downloadCallback: (entity: Entity) => Promise<any>
key2: string,
fsLocal: FakeFs,
fsRemote: FakeFs,
uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) {
console.debug(`tryDuplicateFileForDiffSizes: ${key}`);
await fsLocal.rename(key, key2);
/**
* x.dup.md -> upload to remote
*/
async function f1() {
const k = await copyFile(key2, fsLocal, fsRemote);
await uploadCallback(k.entity);
return k.entity;
}
/**
* x.md -> download to local
*/
async function f2() {
const k = await copyFile(key, fsRemote, fsLocal);
await downloadCallback(k.entity);
return k.entity;
}
const [resUpload, resDownload] = await Promise.all([f1(), f2()]);
return {
upload: resUpload,
download: resDownload,
};
}
export async function tryDuplicateFile(
key: string,
fsLocal: FakeFs,
fsRemote: FakeFs,
uploadCallback: (entity: Entity | undefined) => Promise<any>,
downloadCallback: (entity: Entity | undefined) => Promise<any>
) {
let key2 = getFileRename(key);
let usable = false;
do {
try {
const s = await left.stat(key2);
const s = await fsLocal.stat(key2);
if (s === null || s === undefined) {
throw Error(`not exist $${key2}`);
}
@@ -228,30 +347,31 @@ export async function duplicateFile(
usable = true;
}
} while (!usable);
await left.rename(key, key2);
/**
* x.dup.md -> upload to remote
*/
async function f1() {
const k = await copyFile(key2, left, right);
await uploadCallback(k.entity);
return k.entity;
const localSize = await fsLocal.stat(key);
const remoteSize = await fsRemote.stat(key);
if (
localSize !== undefined &&
remoteSize !== undefined &&
localSize.sizeRaw === remoteSize.sizeRaw
) {
return await tryDuplicateFileForSameSizes(
key,
key2,
fsLocal,
fsRemote,
uploadCallback,
downloadCallback
);
} else {
return await tryDuplicateFileForDiffSizes(
key,
key2,
fsLocal,
fsRemote,
uploadCallback,
downloadCallback
);
}
/**
* x.md -> download to local
*/
async function f2() {
const k = await copyFile(key, right, left);
await downloadCallback(k.entity);
return k.entity;
}
const [resUpload, resDownload] = await Promise.all([f1(), f2()]);
return {
upload: resUpload,
download: resDownload,
};
}
+1 -1
View File
@@ -141,7 +141,7 @@
"modal_proauth_maualinput_conn_fail": "连接失败",
"settings_onedrivefull": "Onedrive(个人版)(Full)设置",
"settings_chooseservice_onedrivefull": "OneDrive(个人版)(Full",
"settings_chooseservice_onedrivefull": "OneDrive(个人版)(FullPRO",
"settings_onedrivefull_disclaimer1": "声明:此插件不是微软或 OneDrive 的官方产品。",
"settings_onedrivefull_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 OneDrive 有不符合预期的访问,请立刻从 https://microsoft.com/consent 删除记录于此插件的连接鉴权。",
"settings_onedrivefull_folder": "我们会在您的 OneDrive 上创建此文件夹并在里面同步:/{{remoteBaseDir}}。",
+1 -1
View File
@@ -141,7 +141,7 @@
"modal_proauth_maualinput_conn_fail": "連線失敗",
"settings_onedrivefull": "Onedrive(個人版)(Full)設定",
"settings_chooseservice_onedrivefull": "OneDrive(個人版)(Full",
"settings_chooseservice_onedrivefull": "OneDrive(個人版)(FullPRO",
"settings_onedrivefull_disclaimer1": "宣告:此外掛不是微軟或 OneDrive 的官方產品。",
"settings_onedrivefull_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 OneDrive 有不符合預期的訪問,請立刻從 https://microsoft.com/consent 刪除記錄於此外掛的連線鑑權。",
"settings_onedrivefull_folder": "我們會在您的 OneDrive 上建立此資料夾並在裡面同步:/{{remoteBaseDir}}。",
+34 -20
View File
@@ -36,7 +36,7 @@ import {
} from "../../src/misc";
import type { Profiler } from "../../src/profiler";
import { checkProRunnableAndFixInplace } from "./account";
import { duplicateFile, isMergable, mergeFile } from "./conflictLogic";
import { isMergable, mergeFile, tryDuplicateFile } from "./conflictLogic";
import {
clearFileContentHistoryByVaultAndProfile,
getFileContentHistoryByVaultAndProfile,
@@ -296,7 +296,8 @@ const getSyncPlanInplace = async (
syncDirection: SyncDirectionType,
profiler: Profiler | undefined,
settings: RemotelySavePluginSettings,
triggerSource: SyncTriggerSourceType
triggerSource: SyncTriggerSourceType,
configDir: string
) => {
profiler?.addIndent();
profiler?.insert("getSyncPlanInplace: enter");
@@ -582,7 +583,11 @@ const getSyncPlanInplace = async (
if (prevSync === undefined) {
// Didn't exist means both are new
if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") {
if (
conflictAction === "keep_newer" ||
(conflictAction === "smart_conflict" &&
key.startsWith(`${configDir}/`))
) {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
@@ -640,7 +645,11 @@ const getSyncPlanInplace = async (
} else {
// Both exist but don't compare means both are modified
if (syncDirection === "bidirectional") {
if (conflictAction === "keep_newer") {
if (
conflictAction === "keep_newer" ||
(conflictAction === "smart_conflict" &&
key.startsWith(`${configDir}/`))
) {
if (
(local.mtimeCli ?? local.mtimeSvr ?? 0) >=
(remote.mtimeCli ?? remote.mtimeSvr ?? 0)
@@ -1342,27 +1351,31 @@ const dispatchOperationToActualV3 = async (
r.key
);
const mtimeCli = (await fsLocal.stat(r.key)).mtimeCli!;
const { upload, download } = await duplicateFile(
await tryDuplicateFile(
r.key,
fsLocal,
fsEncrypt,
async (upload) => {
// TODO: abstract away the dirty hack
fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
profileID,
upload
);
if (upload !== undefined) {
// TODO: abstract away the dirty hack
fullfillMTimeOfRemoteEntityInplace(upload, mtimeCli);
await upsertPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
profileID,
upload
);
}
},
async (download) => {
await upsertPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
profileID,
download
);
if (download !== undefined) {
await upsertPrevSyncRecordByVaultAndProfile(
db,
vaultRandomID,
profileID,
download
);
}
}
);
}
@@ -1712,7 +1725,8 @@ export async function syncer(
settings.syncDirection ?? "bidirectional",
profiler,
settings,
triggerSource
triggerSource,
configDir
);
console.debug(`mixedEntityMappings:`);
console.debug(mixedEntityMappings); // for debugging
+4 -1
View File
@@ -91,7 +91,8 @@ export class FakeFsEncrypt extends FakeFs {
this.password !== "" ? method : "no password"
})`;
if (method === "rclone-base64") {
if (this.password !== "" && method === "rclone-base64") {
// no need to init if no password or not rclone
this.cipherRClone = new rclone.CipherRclone(password, 5);
}
}
@@ -137,6 +138,8 @@ export class FakeFsEncrypt extends FakeFs {
};
}
} else {
// the config has a password
if (this.method === "unknown") {
return {
ok: false,
+86 -13
View File
@@ -264,6 +264,9 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
// another possibile prefix
const FOURTH_COMMON_PREFIX_RAW = `/drive/items/`;
// when to use decode?
const remoteBaseDirEncoded = encodeURIComponent(remoteBaseDir);
if (
x.parentReference === undefined ||
x.parentReference === null ||
@@ -279,6 +282,8 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
SECOND_COMMON_PREFIX_REGEX
);
const matchThirdPrefixRes = fullPathOriginal.match(THIRD_COMMON_PREFIX_REGEX);
// first
if (
matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFirstPrefixRes[0]}${remoteBaseDir}`)
@@ -286,24 +291,68 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (
matchFirstPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchFirstPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchFirstPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// fifth
else if (
matchFifthPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchFifthPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (
matchFifthPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchFifthPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchFifthPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// second
else if (
matchSecondPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchSecondPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (
matchSecondPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchSecondPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchSecondPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// third
else if (
matchThirdPrefixRes !== null &&
fullPathOriginal.startsWith(`${matchThirdPrefixRes[0]}${remoteBaseDir}`)
) {
const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDir}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
} else if (x.parentReference.path.startsWith(FOURTH_COMMON_PREFIX_RAW)) {
} else if (
matchThirdPrefixRes !== null &&
fullPathOriginal.startsWith(
`${matchThirdPrefixRes[0]}${remoteBaseDirEncoded}`
)
) {
const foundPrefix = `${matchThirdPrefixRes[0]}${remoteBaseDirEncoded}`;
key = fullPathOriginal.substring(foundPrefix.length + 1);
}
// fourth
else if (x.parentReference.path.startsWith(FOURTH_COMMON_PREFIX_RAW)) {
// it's something like
// /drive/items/<some_id>!<another_id>:/${remoteBaseDir}/<subfolder>
// with uri encoded!
@@ -321,16 +370,27 @@ const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
key = x.name;
} else {
throw Error(
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToEntityError(
x
)}`
`file/folder with /drive/items/, no idea how to deal with it:
fullPathOriginal=${fullPathOriginal}
matchFirstPrefixRes=${matchFirstPrefixRes}
matchFifthPrefixRes=${matchFifthPrefixRes}
matchSecondPrefixRes=${matchSecondPrefixRes}
matchThirdPrefixRes=${matchThirdPrefixRes}
${constructFromDriveItemToEntityError(x)}`
);
}
} else {
}
// others
else {
throw Error(
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToEntityError(
x
)}`
`file/folder, no idea how to deal with it without known prefix:
fullPathOriginal=${fullPathOriginal}
matchFirstPrefixRes=${matchFirstPrefixRes}
matchFifthPrefixRes=${matchFifthPrefixRes}
matchSecondPrefixRes=${matchSecondPrefixRes}
matchThirdPrefixRes=${matchThirdPrefixRes}
${constructFromDriveItemToEntityError(x)}`
);
}
@@ -926,11 +986,24 @@ export class FakeFsOnedrive extends FakeFs {
).arrayBuffer;
return content;
} else {
// cannot set no-cache here, will have cors error
const content = await (
await fetch(downloadUrl, { cache: "no-store" })
).arrayBuffer();
return content;
// so strange, sometimes (!!!)
// we cannot download the file because of CORS
try {
// cannot set no-cache here, will have cors error
const content = await (
await fetch(downloadUrl, { cache: "no-store" })
).arrayBuffer();
return content;
} catch (e) {
// let's try again to bypass the CORS
const content = (
await requestUrl({
url: downloadUrl,
headers: { "Cache-Control": "no-cache" },
})
).arrayBuffer;
return content;
}
}
}
+1 -1
View File
@@ -269,7 +269,7 @@
"settings_chooseservice_s3": "S3 or compatible",
"settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive for personal (App Folder)",
"settings_chooseservice_onedrive": "OneDrive for personal",
"settings_chooseservice_webdis": "Webdis (HTTP for Redis®)",
"settings_adv": "Advanced Settings",
"settings_concurrency": "Concurrency",
+2 -2
View File
@@ -58,7 +58,7 @@
"statusbar_time_days": "{{time}} 天前",
"statusbar_time_hours": "{{time}} 小时前",
"statusbar_time_minutes": "{{time}} 分钟前",
"statusbar_time_lessminute": "1 分钟内",
"statusbar_time_lessminute": "1 分钟内",
"statusbar_time_now": "刚刚",
"statusbar_syncing": "正在同步",
"statusbar_lastsync_label": "日期:{{date}}",
@@ -268,7 +268,7 @@
"settings_chooseservice_s3": "S3 或兼容 S3 的服务",
"settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive(个人版)App Folder",
"settings_chooseservice_onedrive": "OneDrive(个人版)",
"settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis)",
"settings_adv": "进阶设置",
"settings_concurrency": "并行度",
+2 -2
View File
@@ -57,7 +57,7 @@
"statusbar_time_days": "{{time}} 天前",
"statusbar_time_hours": "{{time}} 小時前",
"statusbar_time_minutes": "{{time}} 分鐘前",
"statusbar_time_lessminute": "1 分鐘內",
"statusbar_time_lessminute": "1 分鐘內",
"statusbar_time_now": "剛剛",
"statusbar_syncing": "正在同步",
"statusbar_lastsync_label": "日期:{{date}}",
@@ -267,7 +267,7 @@
"settings_chooseservice_s3": "S3 或相容 S3 的服務",
"settings_chooseservice_dropbox": "Dropbox",
"settings_chooseservice_webdav": "Webdav",
"settings_chooseservice_onedrive": "OneDrive(個人版)App Folder",
"settings_chooseservice_onedrive": "OneDrive(個人版)",
"settings_chooseservice_webdis": "Webdis (an HTTP interface for Redis®)",
"settings_adv": "進階設定",
"settings_concurrency": "並行度",
+11 -4
View File
@@ -1921,15 +1921,22 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
dropdown.addOption("dropbox", t("settings_chooseservice_dropbox"));
dropdown.addOption("webdav", t("settings_chooseservice_webdav"));
dropdown.addOption("onedrive", t("settings_chooseservice_onedrive"));
dropdown.addOption(
"onedrivefull",
t("settings_chooseservice_onedrivefull")
);
dropdown.addOption("webdis", t("settings_chooseservice_webdis"));
dropdown.addOption("separator line", "-----");
(dropdown.selectEl.lastChild as HTMLElement).setAttribute(
"disabled",
"disabled"
);
dropdown.addOption(
"googledrive",
t("settings_chooseservice_googledrive")
);
dropdown.addOption(
"onedrivefull",
t("settings_chooseservice_onedrivefull")
);
dropdown.addOption("box", t("settings_chooseservice_box"));
dropdown.addOption("pcloud", t("settings_chooseservice_pcloud"));
dropdown.addOption(