Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
577cdde21f | ||
|
|
583b365e72 | ||
|
|
3d213b2be8 | ||
|
|
ee224bf4f2 | ||
|
|
f1bd0b1ce6 | ||
|
|
300ed213af | ||
|
|
4be67ce491 | ||
|
|
98107ba4ea | ||
|
|
b674dd0f10 | ||
|
|
7930509e2a | ||
|
|
1c918d82da | ||
|
|
593fd7471b | ||
|
|
222f386586 | ||
|
|
dcd02457cb | ||
|
|
791c0e8df6 |
@@ -33,6 +33,7 @@ Using the principle of least privilege is crucial for security when allowing a t
|
|||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"s3:HeadObject",
|
"s3:HeadObject",
|
||||||
|
"s3:ListBucket",
|
||||||
"s3:PutObject",
|
"s3:PutObject",
|
||||||
"s3:CopyObject",
|
"s3:CopyObject",
|
||||||
"s3:UploadPart",
|
"s3:UploadPart",
|
||||||
@@ -48,7 +49,10 @@ Using the principle of least privilege is crucial for security when allowing a t
|
|||||||
"s3:DeleteObject",
|
"s3:DeleteObject",
|
||||||
"s3:DeleteObjects"
|
"s3:DeleteObjects"
|
||||||
],
|
],
|
||||||
"Resource": "arn:aws:s3:::my-bucket/*"
|
"Resource": [
|
||||||
|
"arn:aws:s3:::my-bucket",
|
||||||
|
"arn:aws:s3:::my-bucket/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.4.11",
|
"version": "0.4.14",
|
||||||
"minAppVersion": "0.13.21",
|
"minAppVersion": "0.13.21",
|
||||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||||
"author": "fyears",
|
"author": "fyears",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "remotely-save",
|
"id": "remotely-save",
|
||||||
"name": "Remotely Save",
|
"name": "Remotely Save",
|
||||||
"version": "0.4.11",
|
"version": "0.4.14",
|
||||||
"minAppVersion": "0.13.21",
|
"minAppVersion": "0.13.21",
|
||||||
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
"description": "Yet another unofficial plugin allowing users to synchronize notes between local device and the cloud service.",
|
||||||
"author": "fyears",
|
"author": "fyears",
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "remotely-save",
|
"name": "remotely-save",
|
||||||
"version": "0.4.11",
|
"version": "0.4.14",
|
||||||
"description": "This is yet another sync plugin for Obsidian app.",
|
"description": "This is yet another sync plugin for Obsidian app.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev2": "node esbuild.config.mjs --watch",
|
"dev2": "node esbuild.config.mjs --watch",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.14",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@types/chai-as-promised": "^7.1.8",
|
||||||
"@types/jsdom": "^21.1.6",
|
"@types/jsdom": "^21.1.6",
|
||||||
"@types/lodash": "^4.14.202",
|
"@types/lodash": "^4.14.202",
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
"@types/node": "^20.10.4",
|
"@types/node": "^20.10.4",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"builtin-modules": "^3.3.0",
|
"builtin-modules": "^3.3.0",
|
||||||
"chai": "^4.3.10",
|
"chai": "^4.4.1",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"esbuild": "^0.19.9",
|
"esbuild": "^0.19.9",
|
||||||
"esbuild-plugin-inline-worker": "^0.1.1",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"jsdom": "^23.0.1",
|
"jsdom": "^23.0.1",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.4.0",
|
||||||
"npm-check-updates": "^16.14.12",
|
"npm-check-updates": "^16.14.12",
|
||||||
"obsidian": "^1.4.11",
|
"obsidian": "^1.4.11",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
|
|||||||
+5
-2
@@ -204,6 +204,7 @@ export interface Entity {
|
|||||||
sizeRaw: number;
|
sizeRaw: number;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
etag?: string;
|
etag?: string;
|
||||||
|
synthesizedFolder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadedType {
|
export interface UploadedType {
|
||||||
@@ -223,6 +224,8 @@ export interface MixedEntity {
|
|||||||
decisionBranch?: number;
|
decisionBranch?: number;
|
||||||
decision?: DecisionTypeForMixedEntity;
|
decision?: DecisionTypeForMixedEntity;
|
||||||
conflictAction?: ConflictActionType;
|
conflictAction?: ConflictActionType;
|
||||||
|
|
||||||
|
sideNotes?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -269,9 +272,9 @@ export const DEFAULT_LOG_HISTORY_FILE_PREFIX = "log_hist_exported_on_";
|
|||||||
|
|
||||||
export type SyncTriggerSourceType =
|
export type SyncTriggerSourceType =
|
||||||
| "manual"
|
| "manual"
|
||||||
| "auto"
|
|
||||||
| "dry"
|
| "dry"
|
||||||
| "autoOnceInit"
|
| "auto"
|
||||||
|
| "auto_once_init"
|
||||||
| "auto_sync_on_save";
|
| "auto_sync_on_save";
|
||||||
|
|
||||||
export const REMOTELY_SAVE_VERSION_2022 = "0.3.25";
|
export const REMOTELY_SAVE_VERSION_2022 = "0.3.25";
|
||||||
|
|||||||
+14
-4
@@ -73,7 +73,8 @@ const turnSyncPlanToTable = (record: string) => {
|
|||||||
export const exportVaultSyncPlansToFiles = async (
|
export const exportVaultSyncPlansToFiles = async (
|
||||||
db: InternalDBs,
|
db: InternalDBs,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
vaultRandomID: string
|
vaultRandomID: string,
|
||||||
|
howMany: number
|
||||||
) => {
|
) => {
|
||||||
console.info("exporting");
|
console.info("exporting");
|
||||||
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
await mkdirpInVault(DEFAULT_DEBUG_FOLDER, vault);
|
||||||
@@ -82,9 +83,18 @@ export const exportVaultSyncPlansToFiles = async (
|
|||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
md = "No sync plans history found";
|
md = "No sync plans history found";
|
||||||
} else {
|
} else {
|
||||||
md =
|
if (howMany <= 0) {
|
||||||
"Sync plans found:\n\n" +
|
md =
|
||||||
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
"Sync plans found:\n\n" +
|
||||||
|
records.map((x) => "```json\n" + x + "\n```\n").join("\n");
|
||||||
|
} else {
|
||||||
|
md =
|
||||||
|
"Sync plans found:\n\n" +
|
||||||
|
records
|
||||||
|
.map((x) => "```json\n" + x + "\n```\n")
|
||||||
|
.slice(0, howMany)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
const filePath = `${DEFAULT_DEBUG_FOLDER}${DEFAULT_SYNC_PLANS_HISTORY_FILE_PREFIX}${ts}.md`;
|
||||||
|
|||||||
+8
-3
@@ -5,7 +5,7 @@
|
|||||||
"goback": "Go Back",
|
"goback": "Go Back",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"sometext": "Here are some texts.",
|
"sometext": "Here are some texts.",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} already running in stage {{syncStatus}}!",
|
"syncrun_alreadyrunning": "New command {{newTriggerSource}} stops because {{pluginName}} is already running in stage {{syncStatus}}!",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}: syncing from {{triggerSource}}",
|
"syncrun_syncingribbon": "{{pluginName}}: syncing from {{triggerSource}}",
|
||||||
"syncrun_step0": "0/8 Remotely Save is running in dry mode, thus not actual file changes would happen.",
|
"syncrun_step0": "0/8 Remotely Save is running in dry mode, thus not actual file changes would happen.",
|
||||||
"syncrun_step1": "1/8 Remotely Save is preparing ({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save is preparing ({{serviceType}})",
|
||||||
@@ -38,7 +38,10 @@
|
|||||||
"protocol_onedrive_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
"protocol_onedrive_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||||
"command_startsync": "start sync",
|
"command_startsync": "start sync",
|
||||||
"command_drynrun": "start sync (dry run only)",
|
"command_drynrun": "start sync (dry run only)",
|
||||||
"command_exportsyncplans_json": "export sync plans in json format",
|
"command_exportsyncplans_1": "export sync plans (latest 1)",
|
||||||
|
"command_exportsyncplans_5": "export sync plans (latest 5)",
|
||||||
|
"command_exportsyncplans_all": "export sync plans (all)",
|
||||||
|
|
||||||
"command_exportlogsindb": "export logs saved in db",
|
"command_exportlogsindb": "export logs saved in db",
|
||||||
|
|
||||||
"statusbar_time_years": "Synced {{time}} years ago",
|
"statusbar_time_years": "Synced {{time}} years ago",
|
||||||
@@ -292,7 +295,9 @@
|
|||||||
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
"settings_viewconsolelog_desc": "On desktop, please press \"ctrl+shift+i\" or \"cmd+shift+i\" to view the log. On mobile, please install the third-party plugin <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> to export the console log to a note.",
|
||||||
"settings_syncplans": "Export Sync Plans",
|
"settings_syncplans": "Export Sync Plans",
|
||||||
"settings_syncplans_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
|
"settings_syncplans_desc": "Sync plans are created every time after you trigger sync and before the actual sync. Useful to know what would actually happen in those sync. Click the button to export sync plans.",
|
||||||
"settings_syncplans_button_json": "Export",
|
"settings_syncplans_button_1": "Export latest 1",
|
||||||
|
"settings_syncplans_button_5": "Export latest 5",
|
||||||
|
"settings_syncplans_button_all": "Export All",
|
||||||
"settings_syncplans_notice": "Sync plans history exported.",
|
"settings_syncplans_notice": "Sync plans history exported.",
|
||||||
"settings_delsyncplans": "Delete Sync Plans History In DB",
|
"settings_delsyncplans": "Delete Sync Plans History In DB",
|
||||||
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
|
"settings_delsyncplans_desc": "Delete sync plans history in DB.",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"goback": "返回",
|
"goback": "返回",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"sometext": "这里有一段文字。",
|
"sometext": "这里有一段文字。",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} 正处于此阶段:{{syncStatus}}!",
|
"syncrun_alreadyrunning": "{{pluginName}} 正处于此阶段:{{syncStatus}}!中断触发 {{newTriggerSource}}。",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 触发运行",
|
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 触发运行",
|
||||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不会发生实际的文件交换。",
|
||||||
"syncrun_step1": "1/8 Remotely Save 准备同步({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save 准备同步({{serviceType}})",
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
"command_startsync": "开始同步(start sync)",
|
"command_startsync": "开始同步(start sync)",
|
||||||
"command_drynrun": "开始同步(空跑模式)(start sync (dry run only))",
|
"command_drynrun": "开始同步(空跑模式)(start sync (dry run only))",
|
||||||
"command_exportsyncplans_json": "导出同步计划为 json 格式(export sync plans in json format)",
|
"command_exportsyncplans_json": "导出同步计划为 json 格式(export sync plans in json format)",
|
||||||
|
"command_exportsyncplans_1": "导出同步计划(最近 1 次)(export sync plans (latest 1))",
|
||||||
|
"command_exportsyncplans_5": "导出同步计划(最近 5 次)(export sync plans (latest 5))",
|
||||||
|
"command_exportsyncplans_all": "导出同步计划(所有)(export sync plans (all))",
|
||||||
"command_exportlogsindb": "从数据库导出终端日志(export logs saved in db)",
|
"command_exportlogsindb": "从数据库导出终端日志(export logs saved in db)",
|
||||||
|
|
||||||
"statusbar_time_years": "{{time}} 年前同步",
|
"statusbar_time_years": "{{time}} 年前同步",
|
||||||
@@ -291,7 +294,9 @@
|
|||||||
"settings_viewconsolelog_desc": "电脑上,输入“ctrl+shift+i”或“cmd+shift+i”来查看终端输出。手机上,安装第三方插件 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 来导出终端输出到一篇笔记上。",
|
"settings_viewconsolelog_desc": "电脑上,输入“ctrl+shift+i”或“cmd+shift+i”来查看终端输出。手机上,安装第三方插件 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 来导出终端输出到一篇笔记上。",
|
||||||
"settings_syncplans": "导出同步计划",
|
"settings_syncplans": "导出同步计划",
|
||||||
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
|
"settings_syncplans_desc": "每次您启动同步,并在实际上传下载前,插件会生成同步计划。它可以使您知道每次同步发生了什么。点击按钮可以导出同步计划。",
|
||||||
"settings_syncplans_button_json": "导出",
|
"settings_syncplans_button_1": "导出最近 1 次",
|
||||||
|
"settings_syncplans_button_5": "导出最近 5 次",
|
||||||
|
"settings_syncplans_button_all": "导出所有",
|
||||||
"settings_syncplans_notice": "同步计划已导出",
|
"settings_syncplans_notice": "同步计划已导出",
|
||||||
"settings_delsyncplans": "删除数据库里的同步计划历史",
|
"settings_delsyncplans": "删除数据库里的同步计划历史",
|
||||||
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
|
"settings_delsyncplans_desc": "删除数据库里的同步计划历史。",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"goback": "返回",
|
"goback": "返回",
|
||||||
"submit": "提交",
|
"submit": "提交",
|
||||||
"sometext": "這裡有一段文字。",
|
"sometext": "這裡有一段文字。",
|
||||||
"syncrun_alreadyrunning": "{{pluginName}} 正處於此階段:{{syncStatus}}!",
|
"syncrun_alreadyrunning": "{{pluginName}} 正處於此階段:{{syncStatus}}! 中斷觸發 {{newTriggerSource}}。",
|
||||||
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 觸發執行",
|
"syncrun_syncingribbon": "{{pluginName}}:正在由 {{triggerSource}} 觸發執行",
|
||||||
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
"syncrun_step0": "0/8 Remotely Save 在空跑(dry run)模式,不會發生實際的檔案交換。",
|
||||||
"syncrun_step1": "1/8 Remotely Save 準備同步({{serviceType}})",
|
"syncrun_step1": "1/8 Remotely Save 準備同步({{serviceType}})",
|
||||||
@@ -38,7 +38,9 @@
|
|||||||
"protocol_onedrive_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
"protocol_onedrive_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
||||||
"command_startsync": "開始同步(start sync)",
|
"command_startsync": "開始同步(start sync)",
|
||||||
"command_drynrun": "開始同步(空跑模式)(start sync (dry run only))",
|
"command_drynrun": "開始同步(空跑模式)(start sync (dry run only))",
|
||||||
"command_exportsyncplans_json": "匯出同步計劃為 json 格式(export sync plans in json format)",
|
"command_exportsyncplans_1": "匯出同步計劃(最近 1 次)(export sync plans (latest 1))",
|
||||||
|
"command_exportsyncplans_5": "匯出同步計劃(最近 5 次)(export sync plans (latest 5))",
|
||||||
|
"command_exportsyncplans_all": "匯出同步計劃(所有)(export sync plans (all))",
|
||||||
"command_exportlogsindb": "從資料庫匯出終端日誌(export logs saved in db)",
|
"command_exportlogsindb": "從資料庫匯出終端日誌(export logs saved in db)",
|
||||||
|
|
||||||
"statusbar_time_years": "{{time}} 年前同步",
|
"statusbar_time_years": "{{time}} 年前同步",
|
||||||
@@ -291,7 +293,9 @@
|
|||||||
"settings_viewconsolelog_desc": "電腦上,輸入“ctrl+shift+i”或“cmd+shift+i”來檢視終端輸出。手機上,安裝第三方外掛 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 來匯出終端輸出到一篇筆記上。",
|
"settings_viewconsolelog_desc": "電腦上,輸入“ctrl+shift+i”或“cmd+shift+i”來檢視終端輸出。手機上,安裝第三方外掛 <a href='https://obsidian.md/plugins?search=Logstravaganza'>Logstravaganza</a> 來匯出終端輸出到一篇筆記上。",
|
||||||
"settings_syncplans": "匯出同步計劃",
|
"settings_syncplans": "匯出同步計劃",
|
||||||
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
|
"settings_syncplans_desc": "每次您啟動同步,並在實際上傳下載前,外掛會生成同步計劃。它可以使您知道每次同步發生了什麼。點選按鈕可以匯出同步計劃。",
|
||||||
"settings_syncplans_button_json": "匯出",
|
"settings_syncplans_button_1": "匯出最近 1 次",
|
||||||
|
"settings_syncplans_button_5": "匯出最近 5 次",
|
||||||
|
"settings_syncplans_button_all": "匯出所有",
|
||||||
"settings_syncplans_notice": "同步計劃已匯出",
|
"settings_syncplans_notice": "同步計劃已匯出",
|
||||||
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
|
"settings_delsyncplans": "刪除資料庫裡的同步計劃歷史",
|
||||||
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
|
"settings_delsyncplans_desc": "刪除資料庫裡的同步計劃歷史。",
|
||||||
|
|||||||
+3
-3
@@ -382,13 +382,13 @@ export const readAllSyncPlanRecordTextsByVault = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We remove records that are older than 3 days or 100 records.
|
* We remove records that are older than 1 days or 20 records.
|
||||||
* It's a heavy operation, so we shall not place it in the start up.
|
* It's a heavy operation, so we shall not place it in the start up.
|
||||||
* @param db
|
* @param db
|
||||||
*/
|
*/
|
||||||
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
export const clearExpiredSyncPlanRecords = async (db: InternalDBs) => {
|
||||||
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 3; // 3 days
|
const MILLISECONDS_OLD = 1000 * 60 * 60 * 24 * 1; // 1 days
|
||||||
const COUNT_TO_MANY = 100;
|
const COUNT_TO_MANY = 20;
|
||||||
|
|
||||||
const currTs = Date.now();
|
const currTs = Date.now();
|
||||||
const expiredTs = currTs - MILLISECONDS_OLD;
|
const expiredTs = currTs - MILLISECONDS_OLD;
|
||||||
|
|||||||
+105
-48
@@ -9,6 +9,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
requestUrl,
|
requestUrl,
|
||||||
requireApiVersion,
|
requireApiVersion,
|
||||||
|
Events,
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
|
import { createElement, RotateCcw, RefreshCcw, FileText } from "lucide";
|
||||||
@@ -149,6 +150,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
i18n!: I18n;
|
i18n!: I18n;
|
||||||
vaultRandomID!: string;
|
vaultRandomID!: string;
|
||||||
debugServerTemp?: string;
|
debugServerTemp?: string;
|
||||||
|
syncEvent?: Events;
|
||||||
|
appContainerObserver?: MutationObserver;
|
||||||
|
|
||||||
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
async syncRun(triggerSource: SyncTriggerSourceType = "manual") {
|
||||||
const t = (x: TransItemType, vars?: any) => {
|
const t = (x: TransItemType, vars?: any) => {
|
||||||
@@ -165,15 +168,17 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (this.syncStatus !== "idle") {
|
if (this.syncStatus !== "idle") {
|
||||||
// here the notice is shown regardless of triggerSource
|
// really, users don't want to see this in auto mode
|
||||||
new Notice(
|
// so we use getNotice to avoid unnecessary show up
|
||||||
|
getNotice(
|
||||||
t("syncrun_alreadyrunning", {
|
t("syncrun_alreadyrunning", {
|
||||||
pluginName: this.manifest.name,
|
pluginName: this.manifest.name,
|
||||||
syncStatus: this.syncStatus,
|
syncStatus: this.syncStatus,
|
||||||
|
newTriggerSource: triggerSource,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
if (this.currSyncMsg !== undefined && this.currSyncMsg !== "") {
|
||||||
new Notice(this.currSyncMsg);
|
getNotice(this.currSyncMsg);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -376,7 +381,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
realCounter,
|
realCounter,
|
||||||
realTotalCount,
|
realTotalCount,
|
||||||
pathName,
|
pathName,
|
||||||
decision
|
decision,
|
||||||
|
triggerSource
|
||||||
),
|
),
|
||||||
this.db
|
this.db
|
||||||
);
|
);
|
||||||
@@ -416,6 +422,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
this.updateLastSuccessSyncMsg(lastSuccessSyncMillis);
|
this.updateLastSuccessSyncMsg(lastSuccessSyncMillis);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.syncEvent?.trigger("SYNC_DONE");
|
||||||
console.info(
|
console.info(
|
||||||
`${
|
`${
|
||||||
this.manifest.id
|
this.manifest.id
|
||||||
@@ -465,6 +472,8 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
|
|
||||||
this.currSyncMsg = "";
|
this.currSyncMsg = "";
|
||||||
|
|
||||||
|
this.syncEvent = new Events();
|
||||||
|
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
|
||||||
// MUST after loadSettings and before prepareDB
|
// MUST after loadSettings and before prepareDB
|
||||||
@@ -773,14 +782,45 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "export-sync-plans-json",
|
id: "export-sync-plans-1",
|
||||||
name: t("command_exportsyncplans_json"),
|
name: t("command_exportsyncplans_1"),
|
||||||
icon: iconNameLogs,
|
icon: iconNameLogs,
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await exportVaultSyncPlansToFiles(
|
await exportVaultSyncPlansToFiles(
|
||||||
this.db,
|
this.db,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.vaultRandomID
|
this.vaultRandomID,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "export-sync-plans-5",
|
||||||
|
name: t("command_exportsyncplans_5"),
|
||||||
|
icon: iconNameLogs,
|
||||||
|
callback: async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.vaultRandomID,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "export-sync-plans-all",
|
||||||
|
name: t("command_exportsyncplans_all"),
|
||||||
|
icon: iconNameLogs,
|
||||||
|
callback: async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.vaultRandomID,
|
||||||
|
-1
|
||||||
);
|
);
|
||||||
new Notice(t("settings_syncplans_notice"));
|
new Notice(t("settings_syncplans_notice"));
|
||||||
},
|
},
|
||||||
@@ -812,6 +852,10 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
async onunload() {
|
async onunload() {
|
||||||
console.info(`unloading plugin ${this.manifest.id}`);
|
console.info(`unloading plugin ${this.manifest.id}`);
|
||||||
this.syncRibbon = undefined;
|
this.syncRibbon = undefined;
|
||||||
|
if (this.appContainerObserver !== undefined) {
|
||||||
|
this.appContainerObserver.disconnect();
|
||||||
|
this.appContainerObserver = undefined;
|
||||||
|
}
|
||||||
if (this.oauth2Info !== undefined) {
|
if (this.oauth2Info !== undefined) {
|
||||||
this.oauth2Info.helperModal = undefined;
|
this.oauth2Info.helperModal = undefined;
|
||||||
this.oauth2Info = {
|
this.oauth2Info = {
|
||||||
@@ -1100,7 +1144,7 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
) {
|
) {
|
||||||
this.app.workspace.onLayoutReady(() => {
|
this.app.workspace.onLayoutReady(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.syncRun("autoOnceInit");
|
this.syncRun("auto_once_init");
|
||||||
}, this.settings.initRunAfterMilliseconds);
|
}, this.settings.initRunAfterMilliseconds);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1126,52 +1170,64 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
}, scheduleTimeFromNow);
|
}, scheduleTimeFromNow);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.app.workspace.onLayoutReady(() => {
|
const checkCurrFileModified = async (caller: "SYNC" | "FILE_CHANGES") => {
|
||||||
const intervalID = window.setInterval(() => {
|
const currentFile = this.app.workspace.getActiveFile();
|
||||||
const currentFile = this.app.workspace.getActiveFile();
|
|
||||||
|
|
||||||
if (currentFile) {
|
if (currentFile) {
|
||||||
// get the last modified time of the current file
|
// get the last modified time of the current file
|
||||||
// if it has been modified within the last syncOnSaveAfterMilliseconds
|
// if it has modified after lastSuccessSync
|
||||||
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
|
// then schedule a run for syncOnSaveAfterMilliseconds after it was modified
|
||||||
const lastModified = currentFile.stat.mtime;
|
const lastModified = currentFile.stat.mtime;
|
||||||
const currentTime = Date.now();
|
const lastSuccessSyncMillis = await getLastSuccessSyncTimeByVault(
|
||||||
if (
|
this.db,
|
||||||
currentTime - lastModified <
|
this.vaultRandomID
|
||||||
this.settings!.syncOnSaveAfterMilliseconds!
|
);
|
||||||
) {
|
if (
|
||||||
if (
|
this.syncStatus === "idle" &&
|
||||||
!needToRunAgain &&
|
lastModified > lastSuccessSyncMillis &&
|
||||||
!runScheduled &&
|
!runScheduled
|
||||||
this.syncStatus === "idle"
|
) {
|
||||||
) {
|
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
||||||
const scheduleTimeFromNow =
|
} else if (
|
||||||
this.settings!.syncOnSaveAfterMilliseconds! -
|
this.syncStatus === "idle" &&
|
||||||
(currentTime - lastModified);
|
needToRunAgain &&
|
||||||
scheduleSyncOnSave(scheduleTimeFromNow);
|
!runScheduled
|
||||||
} else if (
|
) {
|
||||||
needToRunAgain &&
|
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
||||||
!runScheduled &&
|
needToRunAgain = false;
|
||||||
this.syncStatus === "idle"
|
} else {
|
||||||
) {
|
if (caller === "FILE_CHANGES") {
|
||||||
scheduleSyncOnSave(this.settings!.syncOnSaveAfterMilliseconds!);
|
needToRunAgain = true;
|
||||||
needToRunAgain = false;
|
|
||||||
} else {
|
|
||||||
needToRunAgain = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, this.settings.syncOnSaveAfterMilliseconds);
|
}
|
||||||
this.syncOnSaveIntervalID = intervalID;
|
};
|
||||||
this.registerInterval(intervalID);
|
|
||||||
|
this.app.workspace.onLayoutReady(() => {
|
||||||
|
// listen to sync done
|
||||||
|
this.registerEvent(
|
||||||
|
this.syncEvent?.on("SYNC_DONE", () => {
|
||||||
|
checkCurrFileModified("SYNC");
|
||||||
|
})!
|
||||||
|
);
|
||||||
|
|
||||||
|
// listen to current file save changes
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.vault.on("modify", (x) => {
|
||||||
|
// console.debug(`event=modify! file=${x}`);
|
||||||
|
checkCurrFileModified("FILE_CHANGES");
|
||||||
|
})
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableMobileStatusBarIfSet() {
|
enableMobileStatusBarIfSet() {
|
||||||
if (Platform.isMobile && this.settings.enableMobileStatusBar) {
|
this.app.workspace.onLayoutReady(() => {
|
||||||
changeMobileStatusBar("enable");
|
if (Platform.isMobile && this.settings.enableMobileStatusBar) {
|
||||||
}
|
this.appContainerObserver = changeMobileStatusBar("enable");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveAgreeToUseNewSyncAlgorithm() {
|
async saveAgreeToUseNewSyncAlgorithm() {
|
||||||
@@ -1183,9 +1239,10 @@ export default class RemotelySavePlugin extends Plugin {
|
|||||||
i: number,
|
i: number,
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
pathName: string,
|
pathName: string,
|
||||||
decision: string
|
decision: string,
|
||||||
|
triggerSource: SyncTriggerSourceType
|
||||||
) {
|
) {
|
||||||
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName}`;
|
const msg = `syncing progress=${i}/${totalCount},decision=${decision},path=${pathName},source=${triggerSource}`;
|
||||||
this.currSyncMsg = msg;
|
this.currSyncMsg = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+121
-12
@@ -517,20 +517,129 @@ export const stringToFragment = (string: string) => {
|
|||||||
* https://forum.obsidian.md/t/css-to-show-status-bar-on-mobile-devices/77185
|
* https://forum.obsidian.md/t/css-to-show-status-bar-on-mobile-devices/77185
|
||||||
* @param op
|
* @param op
|
||||||
*/
|
*/
|
||||||
export const changeMobileStatusBar = (op: "enable" | "disable") => {
|
export const changeMobileStatusBar = (
|
||||||
const bar = document.querySelector(
|
op: "enable" | "disable",
|
||||||
|
oldAppContainerObserver?: MutationObserver
|
||||||
|
) => {
|
||||||
|
const appContainer = document.getElementsByClassName("app-container")[0] as
|
||||||
|
| HTMLElement
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const statusbar = document.querySelector(
|
||||||
".is-mobile .app-container .status-bar"
|
".is-mobile .app-container .status-bar"
|
||||||
) as HTMLElement;
|
) as HTMLElement | undefined;
|
||||||
|
|
||||||
|
if (appContainer === undefined || statusbar === undefined) {
|
||||||
|
// give up, exit
|
||||||
|
console.warn(`give up watching appContainer for statusbar`);
|
||||||
|
console.warn(`appContainer=${appContainer}, statusbar=${statusbar}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (op === "enable") {
|
if (op === "enable") {
|
||||||
bar.style.setProperty("display", "flex");
|
const callback = async (
|
||||||
const navBar = document.getElementsByClassName(
|
mutationList: MutationRecord[],
|
||||||
"mobile-navbar"
|
observer: MutationObserver
|
||||||
)[0] as HTMLElement;
|
) => {
|
||||||
// thanks to community's solution
|
for (const mutation of mutationList) {
|
||||||
const height = window.getComputedStyle(navBar).getPropertyValue("height");
|
// console.debug(mutation);
|
||||||
bar.style.setProperty("margin-bottom", height);
|
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
|
||||||
|
const k = mutation.addedNodes[0] as Element;
|
||||||
|
if (
|
||||||
|
k.className.contains("mobile-navbar") ||
|
||||||
|
k.className.contains("mobile-toolbar")
|
||||||
|
) {
|
||||||
|
// have to wait, otherwise the height is not correct??
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
const height = window
|
||||||
|
.getComputedStyle(k as Element)
|
||||||
|
.getPropertyValue("height");
|
||||||
|
|
||||||
|
statusbar.style.setProperty("display", "flex");
|
||||||
|
statusbar.style.setProperty("margin-bottom", height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
observer.observe(appContainer, {
|
||||||
|
attributes: false,
|
||||||
|
childList: true,
|
||||||
|
characterData: false,
|
||||||
|
subtree: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// init, manual call
|
||||||
|
const navBar = document.getElementsByClassName(
|
||||||
|
"mobile-navbar"
|
||||||
|
)[0] as HTMLElement;
|
||||||
|
// thanks to community's solution
|
||||||
|
const height = window.getComputedStyle(navBar).getPropertyValue("height");
|
||||||
|
statusbar.style.setProperty("display", "flex");
|
||||||
|
statusbar.style.setProperty("margin-bottom", height);
|
||||||
|
} catch (e) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
|
||||||
|
return observer;
|
||||||
} else {
|
} else {
|
||||||
bar.style.removeProperty("display");
|
if (oldAppContainerObserver !== undefined) {
|
||||||
bar.style.removeProperty("margin-bottom");
|
console.debug(`disconnect oldAppContainerObserver`);
|
||||||
|
oldAppContainerObserver.disconnect();
|
||||||
|
oldAppContainerObserver = undefined;
|
||||||
|
}
|
||||||
|
statusbar.style.removeProperty("display");
|
||||||
|
statusbar.style.removeProperty("margin-bottom");
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/remotely-save/remotely-save/issues/567
|
||||||
|
* https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/Case-Sensitivity-in-API-2/td-p/191279
|
||||||
|
* @param entities
|
||||||
|
*/
|
||||||
|
export const fixEntityListCasesInplace = (entities: { keyRaw: string }[]) => {
|
||||||
|
entities.sort((a, b) => a.keyRaw.length - b.keyRaw.length);
|
||||||
|
// console.log(JSON.stringify(entities,null,2));
|
||||||
|
|
||||||
|
const caseMapping: Record<string, string> = { "": "" };
|
||||||
|
for (const e of entities) {
|
||||||
|
// console.log(`looking for: ${JSON.stringify(e, null, 2)}`);
|
||||||
|
|
||||||
|
let parentFolder = getParentFolder(e.keyRaw);
|
||||||
|
if (parentFolder === "/") {
|
||||||
|
parentFolder = "";
|
||||||
|
}
|
||||||
|
const parentFolderLower = parentFolder.toLocaleLowerCase();
|
||||||
|
const segs = e.keyRaw.split("/");
|
||||||
|
if (e.keyRaw.endsWith("/")) {
|
||||||
|
// folder
|
||||||
|
if (caseMapping.hasOwnProperty(parentFolderLower)) {
|
||||||
|
const newKeyRaw = `${caseMapping[parentFolderLower]}${segs
|
||||||
|
.slice(-2)
|
||||||
|
.join("/")}`;
|
||||||
|
caseMapping[newKeyRaw.toLocaleLowerCase()] = newKeyRaw;
|
||||||
|
e.keyRaw = newKeyRaw;
|
||||||
|
// console.log(JSON.stringify(caseMapping,null,2));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw Error(`${parentFolder} doesn't have cases record??`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// file
|
||||||
|
if (caseMapping.hasOwnProperty(parentFolderLower)) {
|
||||||
|
const newKeyRaw = `${caseMapping[parentFolderLower]}${segs
|
||||||
|
.slice(-1)
|
||||||
|
.join("/")}`;
|
||||||
|
e.keyRaw = newKeyRaw;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw Error(`${parentFolder} doesn't have cases record??`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
};
|
||||||
|
|||||||
+4
-2
@@ -239,7 +239,8 @@ export class RemoteClient {
|
|||||||
deleteFromRemote = async (
|
deleteFromRemote = async (
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
cipher: Cipher,
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = "",
|
||||||
|
synthesizedFolder: boolean = false
|
||||||
) => {
|
) => {
|
||||||
if (this.serviceType === "s3") {
|
if (this.serviceType === "s3") {
|
||||||
return await s3.deleteFromRemote(
|
return await s3.deleteFromRemote(
|
||||||
@@ -247,7 +248,8 @@ export class RemoteClient {
|
|||||||
this.s3Config!,
|
this.s3Config!,
|
||||||
fileOrFolderPath,
|
fileOrFolderPath,
|
||||||
cipher,
|
cipher,
|
||||||
remoteEncryptedKey
|
remoteEncryptedKey,
|
||||||
|
synthesizedFolder
|
||||||
);
|
);
|
||||||
} else if (this.serviceType === "webdav") {
|
} else if (this.serviceType === "webdav") {
|
||||||
return await webdav.deleteFromRemote(
|
return await webdav.deleteFromRemote(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "./baseTypes";
|
} from "./baseTypes";
|
||||||
import {
|
import {
|
||||||
bufferToArrayBuffer,
|
bufferToArrayBuffer,
|
||||||
|
fixEntityListCasesInplace,
|
||||||
getFolderLevels,
|
getFolderLevels,
|
||||||
hasEmojiInText,
|
hasEmojiInText,
|
||||||
headersToRecord,
|
headersToRecord,
|
||||||
@@ -635,6 +636,8 @@ export const listAllFromRemote = async (client: WrappedDropboxClient) => {
|
|||||||
unifiedContents.push(...unifiedContents2);
|
unifiedContents.push(...unifiedContents2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fixEntityListCasesInplace(unifiedContents);
|
||||||
|
|
||||||
return unifiedContents;
|
return unifiedContents;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -471,6 +471,11 @@ export class WrappedOnedriveClient {
|
|||||||
const pathFrag = encodeURI(pathFragOrig);
|
const pathFrag = encodeURI(pathFragOrig);
|
||||||
theUrl = `${API_PREFIX}${pathFrag}`;
|
theUrl = `${API_PREFIX}${pathFrag}`;
|
||||||
}
|
}
|
||||||
|
// we want to support file name with hash #
|
||||||
|
// because every url we construct here do not contain the # symbol
|
||||||
|
// thus it should be safe to directly replace the character
|
||||||
|
theUrl = theUrl.replace(/#/g, "%23");
|
||||||
|
// console.debug(`building url: [${pathFragOrig}] => [${theUrl}]`)
|
||||||
return theUrl;
|
return theUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -250,6 +250,7 @@ const fromS3ObjectToEntity = (
|
|||||||
mtimeCli: mtimeCli,
|
mtimeCli: mtimeCli,
|
||||||
sizeRaw: x.Size!,
|
sizeRaw: x.Size!,
|
||||||
etag: x.ETag,
|
etag: x.ETag,
|
||||||
|
synthesizedFolder: false,
|
||||||
};
|
};
|
||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
@@ -599,7 +600,10 @@ export const listAllFromRemote = async (
|
|||||||
s3Client: S3Client,
|
s3Client: S3Client,
|
||||||
s3Config: S3Config
|
s3Config: S3Config
|
||||||
) => {
|
) => {
|
||||||
return await listFromRemoteRaw(s3Client, s3Config, s3Config.remotePrefix);
|
const res = (
|
||||||
|
await listFromRemoteRaw(s3Client, s3Config, s3Config.remotePrefix)
|
||||||
|
).filter((x) => x.keyRaw !== "" && x.keyRaw !== "/");
|
||||||
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -715,11 +719,15 @@ export const deleteFromRemote = async (
|
|||||||
s3Config: S3Config,
|
s3Config: S3Config,
|
||||||
fileOrFolderPath: string,
|
fileOrFolderPath: string,
|
||||||
cipher: Cipher,
|
cipher: Cipher,
|
||||||
remoteEncryptedKey: string = ""
|
remoteEncryptedKey: string = "",
|
||||||
|
synthesizedFolder: boolean = false
|
||||||
) => {
|
) => {
|
||||||
if (fileOrFolderPath === "/") {
|
if (fileOrFolderPath === "/") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (synthesizedFolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let remoteFileName = fileOrFolderPath;
|
let remoteFileName = fileOrFolderPath;
|
||||||
if (!cipher.isPasswordEmpty()) {
|
if (!cipher.isPasswordEmpty()) {
|
||||||
remoteFileName = remoteEncryptedKey;
|
remoteFileName = remoteEncryptedKey;
|
||||||
@@ -771,7 +779,7 @@ export const checkConnectivity = async (
|
|||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// TODO: no universal way now, just check this in connectivity
|
// TODO: no universal way now, just check this in connectivity
|
||||||
if (Platform.isIosApp && !s3Config.s3Endpoint.startsWith("https")) {
|
if (Platform.isIosApp && s3Config.s3Endpoint.startsWith("http://")) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Your s3 endpoint could only be https, not http, because of the iOS restriction.`
|
`Your s3 endpoint could only be https, not http, because of the iOS restriction.`
|
||||||
);
|
);
|
||||||
|
|||||||
+35
-4
@@ -2098,10 +2098,16 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (val) => {
|
.onChange(async (val) => {
|
||||||
if (val === "enable") {
|
if (val === "enable") {
|
||||||
this.plugin.settings.enableMobileStatusBar = true;
|
this.plugin.settings.enableMobileStatusBar = true;
|
||||||
changeMobileStatusBar("enable");
|
this.plugin.appContainerObserver =
|
||||||
|
changeMobileStatusBar("enable");
|
||||||
} else {
|
} else {
|
||||||
this.plugin.settings.enableMobileStatusBar = false;
|
this.plugin.settings.enableMobileStatusBar = false;
|
||||||
changeMobileStatusBar("disable");
|
changeMobileStatusBar(
|
||||||
|
"disable",
|
||||||
|
this.plugin.appContainerObserver
|
||||||
|
);
|
||||||
|
this.plugin.appContainerObserver?.disconnect();
|
||||||
|
this.plugin.appContainerObserver = undefined;
|
||||||
}
|
}
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
});
|
});
|
||||||
@@ -2198,12 +2204,37 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
|||||||
.setName(t("settings_syncplans"))
|
.setName(t("settings_syncplans"))
|
||||||
.setDesc(t("settings_syncplans_desc"))
|
.setDesc(t("settings_syncplans_desc"))
|
||||||
.addButton(async (button) => {
|
.addButton(async (button) => {
|
||||||
button.setButtonText(t("settings_syncplans_button_json"));
|
button.setButtonText(t("settings_syncplans_button_1"));
|
||||||
button.onClick(async () => {
|
button.onClick(async () => {
|
||||||
await exportVaultSyncPlansToFiles(
|
await exportVaultSyncPlansToFiles(
|
||||||
this.plugin.db,
|
this.plugin.db,
|
||||||
this.app.vault,
|
this.app.vault,
|
||||||
this.plugin.vaultRandomID
|
this.plugin.vaultRandomID,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_syncplans_button_5"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.vaultRandomID,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
new Notice(t("settings_syncplans_notice"));
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton(async (button) => {
|
||||||
|
button.setButtonText(t("settings_syncplans_button_all"));
|
||||||
|
button.onClick(async () => {
|
||||||
|
await exportVaultSyncPlansToFiles(
|
||||||
|
this.plugin.db,
|
||||||
|
this.app.vault,
|
||||||
|
this.plugin.vaultRandomID,
|
||||||
|
-1
|
||||||
);
|
);
|
||||||
new Notice(t("settings_syncplans_notice"));
|
new Notice(t("settings_syncplans_notice"));
|
||||||
});
|
});
|
||||||
|
|||||||
+131
-15
@@ -18,6 +18,7 @@ import {
|
|||||||
isVaildText,
|
isVaildText,
|
||||||
atWhichLevel,
|
atWhichLevel,
|
||||||
mkdirpInVault,
|
mkdirpInVault,
|
||||||
|
getFolderLevels,
|
||||||
} from "./misc";
|
} from "./misc";
|
||||||
import {
|
import {
|
||||||
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
|
DEFAULT_FILE_NAME_FOR_METADATAONREMOTE,
|
||||||
@@ -326,7 +327,10 @@ export const ensembleMixedEnties = async (
|
|||||||
): Promise<SyncPlanType> => {
|
): Promise<SyncPlanType> => {
|
||||||
const finalMappings: SyncPlanType = {};
|
const finalMappings: SyncPlanType = {};
|
||||||
|
|
||||||
|
const synthFolders: Record<string, Entity> = {};
|
||||||
|
|
||||||
// remote has to be first
|
// remote has to be first
|
||||||
|
// we also have to synthesize folders here
|
||||||
for (const remote of remoteEntityList) {
|
for (const remote of remoteEntityList) {
|
||||||
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
const remoteCopied = ensureMTimeOfRemoteEntityValid(
|
||||||
await decryptRemoteEntityInplace(
|
await decryptRemoteEntityInplace(
|
||||||
@@ -352,6 +356,42 @@ export const ensembleMixedEnties = async (
|
|||||||
key: key,
|
key: key,
|
||||||
remote: remoteCopied,
|
remote: remoteCopied,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const f of getFolderLevels(key, true)) {
|
||||||
|
if (finalMappings.hasOwnProperty(f)) {
|
||||||
|
delete synthFolders[f];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!synthFolders.hasOwnProperty(f) ||
|
||||||
|
remoteCopied.mtimeSvr! >= synthFolders[f].mtimeSvr!
|
||||||
|
) {
|
||||||
|
synthFolders[f] = {
|
||||||
|
key: f,
|
||||||
|
keyRaw: `<synth: ${f}>`,
|
||||||
|
keyEnc: `<enc synth: ${f}>`,
|
||||||
|
size: 0,
|
||||||
|
sizeRaw: 0,
|
||||||
|
sizeEnc: 0,
|
||||||
|
mtimeSvr: remoteCopied.mtimeSvr,
|
||||||
|
mtimeSvrFmt: remoteCopied.mtimeSvrFmt,
|
||||||
|
mtimeCli: remoteCopied.mtimeCli,
|
||||||
|
mtimeCliFmt: remoteCopied.mtimeCliFmt,
|
||||||
|
synthesizedFolder: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`synthFolders:`);
|
||||||
|
console.debug(synthFolders);
|
||||||
|
|
||||||
|
// special: add synth folders
|
||||||
|
for (const key of Object.keys(synthFolders)) {
|
||||||
|
finalMappings[key] = {
|
||||||
|
key: key,
|
||||||
|
remote: synthFolders[key],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
if (Object.keys(finalMappings).length === 0 || localEntityList.length === 0) {
|
||||||
@@ -863,13 +903,27 @@ export const getSyncPlanInplace = async (
|
|||||||
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
|
throw Error(`unexpectedly keptFolder no decisions: ${[...keptFolder]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finally we want to make our life easier
|
||||||
|
const currTime = Date.now();
|
||||||
|
const currTimeFmt = unixTimeToStr(currTime);
|
||||||
|
// because the path should not as / in the beginning,
|
||||||
|
// we should be safe to add these keys:
|
||||||
|
mixedEntityMappings["/$@meta"] = {
|
||||||
|
key: "/$@meta", // don't mess up with the types
|
||||||
|
sideNotes: {
|
||||||
|
generateTime: currTime,
|
||||||
|
generateTimeFmt: currTimeFmt,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return mixedEntityMappings;
|
return mixedEntityMappings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitThreeStepsOnEntityMappings = (
|
const splitFourStepsOnEntityMappings = (
|
||||||
mixedEntityMappings: Record<string, MixedEntity>
|
mixedEntityMappings: Record<string, MixedEntity>
|
||||||
) => {
|
) => {
|
||||||
type StepArrayType = MixedEntity[] | undefined | null;
|
type StepArrayType = MixedEntity[] | undefined | null;
|
||||||
|
const onlyMarkSyncedOps: StepArrayType[] = [];
|
||||||
const folderCreationOps: StepArrayType[] = [];
|
const folderCreationOps: StepArrayType[] = [];
|
||||||
const deletionOps: StepArrayType[] = [];
|
const deletionOps: StepArrayType[] = [];
|
||||||
const uploadDownloads: StepArrayType[] = [];
|
const uploadDownloads: StepArrayType[] = [];
|
||||||
@@ -885,6 +939,11 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
|
|
||||||
for (let i = 0; i < sortedKeys.length; ++i) {
|
for (let i = 0; i < sortedKeys.length; ++i) {
|
||||||
const key = sortedKeys[i];
|
const key = sortedKeys[i];
|
||||||
|
|
||||||
|
if (key === "/$@meta") {
|
||||||
|
continue; // special
|
||||||
|
}
|
||||||
|
|
||||||
const val = mixedEntityMappings[key];
|
const val = mixedEntityMappings[key];
|
||||||
|
|
||||||
if (!key.endsWith("/")) {
|
if (!key.endsWith("/")) {
|
||||||
@@ -892,14 +951,27 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
val.decision === "equal" ||
|
|
||||||
val.decision === "conflict_created_then_do_nothing" ||
|
|
||||||
val.decision === "folder_existed_both_then_do_nothing" ||
|
|
||||||
val.decision === "local_is_created_too_large_then_do_nothing" ||
|
val.decision === "local_is_created_too_large_then_do_nothing" ||
|
||||||
val.decision === "remote_is_created_too_large_then_do_nothing" ||
|
val.decision === "remote_is_created_too_large_then_do_nothing" ||
|
||||||
val.decision === "folder_to_skip"
|
val.decision === "folder_to_skip"
|
||||||
) {
|
) {
|
||||||
// pass
|
// pass
|
||||||
|
} else if (
|
||||||
|
val.decision === "equal" ||
|
||||||
|
val.decision === "conflict_created_then_do_nothing" ||
|
||||||
|
val.decision === "folder_existed_both_then_do_nothing"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
onlyMarkSyncedOps.length === 0 ||
|
||||||
|
onlyMarkSyncedOps[0] === undefined ||
|
||||||
|
onlyMarkSyncedOps[0] === null
|
||||||
|
) {
|
||||||
|
onlyMarkSyncedOps[0] = [val];
|
||||||
|
} else {
|
||||||
|
onlyMarkSyncedOps[0].push(val); // only one level is needed here
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't need to update realTotalCount here
|
||||||
} else if (
|
} else if (
|
||||||
val.decision === "folder_existed_local_then_also_create_remote" ||
|
val.decision === "folder_existed_local_then_also_create_remote" ||
|
||||||
val.decision === "folder_existed_remote_then_also_create_local" ||
|
val.decision === "folder_existed_remote_then_also_create_local" ||
|
||||||
@@ -979,6 +1051,7 @@ const splitThreeStepsOnEntityMappings = (
|
|||||||
deletionOps.reverse(); // inplace reverse
|
deletionOps.reverse(); // inplace reverse
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
onlyMarkSyncedOps: onlyMarkSyncedOps,
|
||||||
folderCreationOps: folderCreationOps,
|
folderCreationOps: folderCreationOps,
|
||||||
deletionOps: deletionOps,
|
deletionOps: deletionOps,
|
||||||
uploadDownloads: uploadDownloads,
|
uploadDownloads: uploadDownloads,
|
||||||
@@ -1009,14 +1082,39 @@ const dispatchOperationToActualV3 = async (
|
|||||||
if (r.decision === "only_history") {
|
if (r.decision === "only_history") {
|
||||||
clearPrevSyncRecordByVaultAndProfile(db, vaultRandomID, profileID, key);
|
clearPrevSyncRecordByVaultAndProfile(db, vaultRandomID, profileID, key);
|
||||||
} else if (
|
} else if (
|
||||||
r.decision === "equal" ||
|
|
||||||
r.decision === "conflict_created_then_do_nothing" ||
|
|
||||||
r.decision === "local_is_created_too_large_then_do_nothing" ||
|
r.decision === "local_is_created_too_large_then_do_nothing" ||
|
||||||
r.decision === "remote_is_created_too_large_then_do_nothing" ||
|
r.decision === "remote_is_created_too_large_then_do_nothing" ||
|
||||||
r.decision === "folder_to_skip" ||
|
r.decision === "folder_to_skip"
|
||||||
|
) {
|
||||||
|
// !! no actual sync being kept happens,
|
||||||
|
// so no sync record here
|
||||||
|
// pass
|
||||||
|
} else if (
|
||||||
|
r.decision === "equal" ||
|
||||||
|
r.decision === "conflict_created_then_do_nothing" ||
|
||||||
r.decision === "folder_existed_both_then_do_nothing"
|
r.decision === "folder_existed_both_then_do_nothing"
|
||||||
) {
|
) {
|
||||||
// pass
|
// !! we need to upsert the record,
|
||||||
|
// so that next time we can determine the change delta
|
||||||
|
// if we have prevSync, we store it because it should keep all necessary info
|
||||||
|
let entity = r.prevSync;
|
||||||
|
// if we don't have prevSync, we use remote entity AND local mtime
|
||||||
|
// as if it is "uploaded"
|
||||||
|
if (entity === undefined && r.remote !== undefined) {
|
||||||
|
entity = await decryptRemoteEntityInplace(r.remote, cipher);
|
||||||
|
entity = await fullfillMTimeOfRemoteEntityInplace(
|
||||||
|
entity,
|
||||||
|
r.local?.mtimeCli
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (entity !== undefined) {
|
||||||
|
await upsertPrevSyncRecordByVaultAndProfile(
|
||||||
|
db,
|
||||||
|
vaultRandomID,
|
||||||
|
profileID,
|
||||||
|
entity
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (
|
} else if (
|
||||||
r.decision === "local_is_modified_then_push" ||
|
r.decision === "local_is_modified_then_push" ||
|
||||||
r.decision === "local_is_created_then_push" ||
|
r.decision === "local_is_created_then_push" ||
|
||||||
@@ -1074,7 +1172,12 @@ const dispatchOperationToActualV3 = async (
|
|||||||
);
|
);
|
||||||
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
|
} else if (r.decision === "local_is_deleted_thus_also_delete_remote") {
|
||||||
// local is deleted, we need to delete remote now
|
// local is deleted, we need to delete remote now
|
||||||
await client.deleteFromRemote(r.key, cipher, r.remote!.keyEnc);
|
await client.deleteFromRemote(
|
||||||
|
r.key,
|
||||||
|
cipher,
|
||||||
|
r.remote!.keyEnc,
|
||||||
|
r.remote!.synthesizedFolder
|
||||||
|
);
|
||||||
await clearPrevSyncRecordByVaultAndProfile(
|
await clearPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
vaultRandomID,
|
vaultRandomID,
|
||||||
@@ -1128,7 +1231,12 @@ const dispatchOperationToActualV3 = async (
|
|||||||
r.decision === "folder_to_be_deleted_on_both" ||
|
r.decision === "folder_to_be_deleted_on_both" ||
|
||||||
r.decision === "folder_to_be_deleted_on_remote"
|
r.decision === "folder_to_be_deleted_on_remote"
|
||||||
) {
|
) {
|
||||||
await client.deleteFromRemote(r.key, cipher, r.remote!.keyEnc);
|
await client.deleteFromRemote(
|
||||||
|
r.key,
|
||||||
|
cipher,
|
||||||
|
r.remote!.keyEnc,
|
||||||
|
r.remote!.synthesizedFolder
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await clearPrevSyncRecordByVaultAndProfile(
|
await clearPrevSyncRecordByVaultAndProfile(
|
||||||
db,
|
db,
|
||||||
@@ -1157,13 +1265,15 @@ export const doActualSync = async (
|
|||||||
) => {
|
) => {
|
||||||
console.debug(`concurrency === ${concurrency}`);
|
console.debug(`concurrency === ${concurrency}`);
|
||||||
const {
|
const {
|
||||||
|
onlyMarkSyncedOps,
|
||||||
folderCreationOps,
|
folderCreationOps,
|
||||||
deletionOps,
|
deletionOps,
|
||||||
uploadDownloads,
|
uploadDownloads,
|
||||||
allFilesCount,
|
allFilesCount,
|
||||||
realModifyDeleteCount,
|
realModifyDeleteCount,
|
||||||
realTotalCount,
|
realTotalCount,
|
||||||
} = splitThreeStepsOnEntityMappings(mixedEntityMappings);
|
} = splitFourStepsOnEntityMappings(mixedEntityMappings);
|
||||||
|
// console.debug(`onlyMarkSyncedOps: ${JSON.stringify(onlyMarkSyncedOps)}`);
|
||||||
// console.debug(`folderCreationOps: ${JSON.stringify(folderCreationOps)}`);
|
// console.debug(`folderCreationOps: ${JSON.stringify(folderCreationOps)}`);
|
||||||
// console.debug(`deletionOps: ${JSON.stringify(deletionOps)}`);
|
// console.debug(`deletionOps: ${JSON.stringify(deletionOps)}`);
|
||||||
// console.debug(`uploadDownloads: ${JSON.stringify(uploadDownloads)}`);
|
// console.debug(`uploadDownloads: ${JSON.stringify(uploadDownloads)}`);
|
||||||
@@ -1198,11 +1308,17 @@ export const doActualSync = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nested = [folderCreationOps, deletionOps, uploadDownloads];
|
const nested = [
|
||||||
|
onlyMarkSyncedOps,
|
||||||
|
folderCreationOps,
|
||||||
|
deletionOps,
|
||||||
|
uploadDownloads,
|
||||||
|
];
|
||||||
const logTexts = [
|
const logTexts = [
|
||||||
`1. create all folders from shadowest to deepest`,
|
`1. record the items already being synced`,
|
||||||
`2. delete files and folders from deepest to shadowest`,
|
`2. create all folders from shadowest to deepest`,
|
||||||
`3. upload or download files in parallel, with the desired concurrency=${concurrency}`,
|
`3. delete files and folders from deepest to shadowest`,
|
||||||
|
`4. upload or download files in parallel, with the desired concurrency=${concurrency}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
let realCounter = 0;
|
let realCounter = 0;
|
||||||
|
|||||||
@@ -285,3 +285,56 @@ describe("Misc: special char for dir", () => {
|
|||||||
expect(misc.checkHasSpecialCharForDir("xxx?yyy")).to.be.true;
|
expect(misc.checkHasSpecialCharForDir("xxx?yyy")).to.be.true;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Misc: Dropbox: should fix the folder name cases", () => {
|
||||||
|
it("should do nothing on empty folders", () => {
|
||||||
|
const input: any[] = [];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should sort folders by length by side effect", () => {
|
||||||
|
const input = [
|
||||||
|
{ keyRaw: "aaaa/" },
|
||||||
|
{ keyRaw: "bbb/" },
|
||||||
|
{ keyRaw: "c/" },
|
||||||
|
{ keyRaw: "dd/" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
{ keyRaw: "c/" },
|
||||||
|
{ keyRaw: "dd/" },
|
||||||
|
{ keyRaw: "bbb/" },
|
||||||
|
{ keyRaw: "aaaa/" },
|
||||||
|
];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.deep.equal(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fix folder names", () => {
|
||||||
|
const input = [
|
||||||
|
{ keyRaw: "AAA/" },
|
||||||
|
{ keyRaw: "aaa/bbb/CCC.md" },
|
||||||
|
{ keyRaw: "aaa/BBB/" },
|
||||||
|
|
||||||
|
{ keyRaw: "ddd/" },
|
||||||
|
{ keyRaw: "DDD/EEE/fff.md" },
|
||||||
|
{ keyRaw: "DDD/eee/" },
|
||||||
|
|
||||||
|
{ keyRaw: "Ggg/" },
|
||||||
|
{ keyRaw: "ggG/hHH你好/Fff世界.md" },
|
||||||
|
{ keyRaw: "ggG/Hhh你好/" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const output = [
|
||||||
|
{ keyRaw: "AAA/" },
|
||||||
|
{ keyRaw: "ddd/" },
|
||||||
|
{ keyRaw: "Ggg/" },
|
||||||
|
{ keyRaw: "AAA/BBB/" },
|
||||||
|
{ keyRaw: "ddd/eee/" },
|
||||||
|
{ keyRaw: "Ggg/Hhh你好/" },
|
||||||
|
{ keyRaw: "AAA/BBB/CCC.md" },
|
||||||
|
{ keyRaw: "ddd/eee/fff.md" },
|
||||||
|
{ keyRaw: "Ggg/Hhh你好/Fff世界.md" },
|
||||||
|
];
|
||||||
|
expect(misc.fixEntityListCasesInplace(input)).to.deep.equal(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user