Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3aecb074 | ||
|
|
9128f9ed5b | ||
|
|
b581ef3f31 | ||
|
|
d8c42adc55 | ||
|
|
45b18ca63d | ||
|
|
52e9d3298a | ||
|
|
e82dfc1e88 | ||
|
|
0be19b677b | ||
|
|
d1839706af | ||
|
|
ce0cc232c8 | ||
|
|
12d09a30ef | ||
|
|
3fdc443ecf | ||
|
|
0700af855d |
@@ -13,5 +13,8 @@ main.js
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# debug
|
||||
logs.txt
|
||||
|
||||
# hidden files
|
||||
.*
|
||||
|
||||
@@ -12,7 +12,7 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
|
||||
|
||||
## Features
|
||||
|
||||
- **Amazon S3 or S3-compatible services are supported.** Webdav supports on the plan.
|
||||
- **Amazon S3 or S3-compatible services are supported.** Webdav supports on the half way.
|
||||
- **Obsidiain Mobile supported.** Vaults can be synced across mobile and desktop devices with the cloud service as the "broker".
|
||||
- **[End-to-end encryption](./docs/encryption.md) supported.** Files would be encrypted using openssl format before being sent to the cloud **if** user specify a password.
|
||||
- **[Minimal Intrusive](./docs/minimal_intrusive_design.md).**
|
||||
@@ -35,10 +35,18 @@ As of November 2021, the plugin is considered in BETA stage. **DO NOT USE IT for
|
||||
|
||||
## Usage
|
||||
|
||||
### s3
|
||||
|
||||
- Prepare your S3 (-compatible) service information: [endpoint, region](https://docs.aws.amazon.com/general/latest/gr/s3.html), [access key id, secret access key](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html), bucket name. The bucket should be empty and solely for syncing a vault.
|
||||
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios.
|
||||
- Configure (enable) [CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) for requests from `app://obsidian.md` and `capacitor://localhost` and `http://localhost`. It's unfortunately required, because the plugin sends requests from a browser-like envirement. And those addresses are tested and found on desktop and ios and android.
|
||||
- Download and enable this plugin.
|
||||
- Enter your infomation to the settings of this plugin.
|
||||
- If you want to enable end-to-end encryption, also set a password in settings. If you do not specify a password, the files and folders are synced in plain, original content to the cloud.
|
||||
- Click the new "switch" icon on the ribbon (the left sidebar), **every time** you want to sync your vault between local and remote. (No "auto sync" yet.)
|
||||
- **Be patient while syncing.** Especially in the first-time sync.
|
||||
|
||||
### webdav
|
||||
|
||||
- **webdav support is buggy (as of now, 20211122) and considered experimental, so it's hidden by default.** Highly recommend to use the more stable s3.
|
||||
- If you decide to give it a try, open settings, and click "Choose service" area five times, then a Notice should show up. Close and open settings again then you will be able to select webdav.
|
||||
- Currently webdav server should enable CORS for requests, because of technical limitations of mobile.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-remotely-save",
|
||||
"name": "Remotely Save",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"minAppVersion": "0.12.15",
|
||||
"description": "Yet another unofficial plugin allowing users to sync notes between local device and the cloud service.",
|
||||
"author": "fyears",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-remotely-save",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.6",
|
||||
"description": "This is yet another sync plugin for Obsidian app.",
|
||||
"scripts": {
|
||||
"dev": "webpack --mode development --watch",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Only type defs here.
|
||||
*/
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav";
|
||||
|
||||
export interface RemoteItem {
|
||||
key: string;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
remoteType: SUPPORTED_SERVICES_TYPE;
|
||||
etag?: string;
|
||||
}
|
||||
+7
-5
@@ -1,7 +1,7 @@
|
||||
import localforage from "localforage";
|
||||
import { TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
|
||||
import type { SUPPORTED_SERVICES_TYPE } from "./misc";
|
||||
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
import type { SyncPlanType } from "./sync";
|
||||
|
||||
export type LocalForage = typeof localforage;
|
||||
@@ -197,7 +197,8 @@ export const insertRenameRecord = async (
|
||||
await db.deleteHistoryTbl.setItem(k.key, k);
|
||||
};
|
||||
|
||||
export const upsertSyncMetaMappingDataS3 = async (
|
||||
export const upsertSyncMetaMappingData = async (
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
db: InternalDBs,
|
||||
localKey: string,
|
||||
localMTime: number,
|
||||
@@ -215,13 +216,14 @@ export const upsertSyncMetaMappingDataS3 = async (
|
||||
remoteMtime: remoteMTime,
|
||||
remoteSize: remoteSize,
|
||||
remoteExtraKey: remoteExtraKey,
|
||||
remoteType: "s3",
|
||||
remoteType: serviceType,
|
||||
keyType: localKey.endsWith("/") ? "folder" : "file",
|
||||
};
|
||||
await db.syncMappingTbl.setItem(remoteKey, aggregratedInfo);
|
||||
};
|
||||
|
||||
export const getSyncMetaMappingByRemoteKeyS3 = async (
|
||||
export const getSyncMetaMappingByRemoteKey = async (
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
db: InternalDBs,
|
||||
remoteKey: string,
|
||||
remoteMTime: number,
|
||||
@@ -240,7 +242,7 @@ export const getSyncMetaMappingByRemoteKeyS3 = async (
|
||||
potentialItem.remoteKey === remoteKey &&
|
||||
potentialItem.remoteMtime === remoteMTime &&
|
||||
potentialItem.remoteExtraKey === remoteExtraKey &&
|
||||
potentialItem.remoteType === "s3"
|
||||
potentialItem.remoteType === serviceType
|
||||
) {
|
||||
// the result was found
|
||||
return potentialItem;
|
||||
|
||||
+213
-33
@@ -25,23 +25,26 @@ import type { InternalDBs } from "./localdb";
|
||||
|
||||
import type { SyncStatusType, PasswordCheckType } from "./sync";
|
||||
import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
|
||||
import {
|
||||
DEFAULT_S3_CONFIG,
|
||||
getS3Client,
|
||||
listFromRemote,
|
||||
S3Config,
|
||||
checkS3Connectivity,
|
||||
} from "./s3";
|
||||
import { S3Config, DEFAULT_S3_CONFIG } from "./s3";
|
||||
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
|
||||
import { RemoteClient } from "./remote";
|
||||
import { exportSyncPlansToFiles } from "./debugMode";
|
||||
import { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
|
||||
interface RemotelySavePluginSettings {
|
||||
s3?: S3Config;
|
||||
password?: string;
|
||||
s3: S3Config;
|
||||
webdav: WebdavConfig;
|
||||
password: string;
|
||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
enableExperimentService: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: DEFAULT_S3_CONFIG,
|
||||
webdav: DEFAULT_WEBDAV_CONFIG,
|
||||
password: "",
|
||||
serviceType: "s3",
|
||||
enableExperimentService: false,
|
||||
};
|
||||
|
||||
export default class RemotelySavePlugin extends Plugin {
|
||||
@@ -81,13 +84,19 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
try {
|
||||
//console.log(`huh ${this.settings.password}`)
|
||||
new Notice("1/6 Remotely Save Sync Preparing");
|
||||
new Notice(
|
||||
`1/6 Remotely Save Sync Preparing (${this.settings.serviceType})`
|
||||
);
|
||||
this.syncStatus = "preparing";
|
||||
|
||||
new Notice("2/6 Starting to fetch remote meta data.");
|
||||
this.syncStatus = "getting_remote_meta";
|
||||
const s3Client = getS3Client(this.settings.s3);
|
||||
const remoteRsp = await listFromRemote(s3Client, this.settings.s3);
|
||||
const client = new RemoteClient(
|
||||
this.settings.serviceType,
|
||||
this.settings.s3,
|
||||
this.settings.webdav
|
||||
);
|
||||
const remoteRsp = await client.listFromRemote();
|
||||
|
||||
new Notice("3/6 Starting to fetch local meta data.");
|
||||
this.syncStatus = "getting_local_meta";
|
||||
@@ -115,6 +124,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
local,
|
||||
localHistory,
|
||||
this.db,
|
||||
client.serviceType,
|
||||
this.settings.password
|
||||
);
|
||||
console.log(syncPlan.mixedStates); // for debugging
|
||||
@@ -127,8 +137,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
|
||||
this.syncStatus = "syncing";
|
||||
await doActualSync(
|
||||
s3Client,
|
||||
this.settings.s3,
|
||||
client,
|
||||
this.db,
|
||||
this.app.vault,
|
||||
syncPlan,
|
||||
@@ -259,7 +268,69 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
|
||||
containerEl.createEl("h1", { text: "Remotely Save" });
|
||||
|
||||
const s3Div = containerEl.createEl("div");
|
||||
const generalDiv = containerEl.createEl("div");
|
||||
generalDiv.createEl("h2", { text: "General" });
|
||||
|
||||
const passwordDiv = generalDiv.createEl("div");
|
||||
let newPassword = `${this.plugin.settings.password}`;
|
||||
new Setting(passwordDiv)
|
||||
.setName("encryption password")
|
||||
.setDesc(
|
||||
'Password for E2E encryption. Empty for no password. You need to click "Confirm".'
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(`${this.plugin.settings.password}`)
|
||||
.onChange(async (value) => {
|
||||
newPassword = value.trim();
|
||||
})
|
||||
)
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Confirm");
|
||||
button.onClick(async () => {
|
||||
new PasswordModal(this.app, this.plugin, newPassword).open();
|
||||
});
|
||||
});
|
||||
|
||||
// we need to create the div in advance of s3Div and webdavDiv
|
||||
const serviceChooserDiv = generalDiv.createEl("div");
|
||||
|
||||
let clickChooserTimes = 0;
|
||||
serviceChooserDiv.onClickEvent((x) => {
|
||||
if (Platform.isIosApp) {
|
||||
// downgrade the experiment
|
||||
// because iOS doesn't support x.detail
|
||||
clickChooserTimes += 1;
|
||||
setTimeout(function () {
|
||||
clickChooserTimes = 0;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if ((Platform.isIosApp && clickChooserTimes === 5) || x.detail === 5) {
|
||||
if (this.plugin.settings.serviceType === "webdav") {
|
||||
new Notice(
|
||||
"You've enabled hidden unstable experimental webdav support before. Nothing changes."
|
||||
);
|
||||
} else if (!this.plugin.settings.enableExperimentService) {
|
||||
this.plugin.settings.enableExperimentService = true;
|
||||
this.plugin.saveSettings();
|
||||
new Notice(
|
||||
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
|
||||
);
|
||||
} else if (this.plugin.settings.enableExperimentService) {
|
||||
this.plugin.settings.enableExperimentService = false;
|
||||
this.plugin.saveSettings();
|
||||
new Notice(
|
||||
"You've disabled hidden unstable experimental webdav support. Reopen settings again."
|
||||
);
|
||||
}
|
||||
}
|
||||
x.preventDefault();
|
||||
});
|
||||
|
||||
const s3Div = containerEl.createEl("div", { cls: "s3-hide" });
|
||||
s3Div.toggleClass("s3-hide", this.plugin.settings.serviceType !== "s3");
|
||||
s3Div.createEl("h2", { text: "S3 (-compatible) Service" });
|
||||
|
||||
s3Div.createEl("p", {
|
||||
@@ -272,7 +343,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
|
||||
s3Div.createEl("p", {
|
||||
text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost",
|
||||
text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost",
|
||||
});
|
||||
|
||||
s3Div.createEl("p", {
|
||||
@@ -368,11 +439,12 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new Notice("Checking...");
|
||||
const s3Client = getS3Client(this.plugin.settings.s3);
|
||||
const res = await checkS3Connectivity(
|
||||
s3Client,
|
||||
this.plugin.settings.s3
|
||||
const client = new RemoteClient(
|
||||
"s3",
|
||||
this.plugin.settings.s3,
|
||||
undefined
|
||||
);
|
||||
const res = await client.checkConnectivity();
|
||||
if (res) {
|
||||
new Notice("Great! The bucket can be accessed.");
|
||||
} else {
|
||||
@@ -381,28 +453,136 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
const generalDiv = containerEl.createEl("div");
|
||||
generalDiv.createEl("h2", { text: "General" });
|
||||
const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" });
|
||||
webdavDiv.toggleClass(
|
||||
"webdav-hide",
|
||||
this.plugin.settings.serviceType !== "webdav"
|
||||
);
|
||||
|
||||
const passwordDiv = generalDiv.createEl("div");
|
||||
let newPassword = `${this.plugin.settings.password}`;
|
||||
new Setting(passwordDiv)
|
||||
.setName("encryption password")
|
||||
.setDesc(
|
||||
'Password for E2E encryption. Empty for no password. You need to click "Confirm".'
|
||||
)
|
||||
webdavDiv.createEl("h2", { text: "Webdav Service" });
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
text: "Disclaimer: Webdav functions are more experimental, and s3 functions are more stable now.",
|
||||
cls: "webdav-disclaimer",
|
||||
});
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
text: "Disclaimer: The infomation is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins may or may not be able to read the info. If you see any unintentional access to your webdav server, please immediately change the username and/or password to stop further accessment.",
|
||||
cls: "webdav-disclaimer",
|
||||
});
|
||||
|
||||
webdavDiv.createEl("p", {
|
||||
text: "You need to configure CORS to allow requests from origin app://obsidian.md and capacitor://localhost and http://localhost",
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("server address")
|
||||
.setDesc("server address")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(`${this.plugin.settings.password}`)
|
||||
.setValue(this.plugin.settings.webdav.address)
|
||||
.onChange(async (value) => {
|
||||
newPassword = value.trim();
|
||||
this.plugin.settings.webdav.address = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("server username")
|
||||
.setDesc("server username")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.webdav.username)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.webdav.username = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("server password")
|
||||
.setDesc("server password")
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.webdav.password)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.webdav.password = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("server auth type")
|
||||
.setDesc(
|
||||
"Server auth type. If you do not set password, this option would be ignored."
|
||||
)
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown.addOption("basic", "basic");
|
||||
dropdown.addOption("digest", "digest");
|
||||
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.webdav.authType)
|
||||
.onChange(async (val: WebdavAuthType) => {
|
||||
this.plugin.settings.webdav.authType = val;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(webdavDiv)
|
||||
.setName("check connectivity")
|
||||
.setDesc("check connectivity")
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Confirm");
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new PasswordModal(this.app, this.plugin, newPassword).open();
|
||||
new Notice("Checking...");
|
||||
const client = new RemoteClient(
|
||||
"webdav",
|
||||
undefined,
|
||||
this.plugin.settings.webdav
|
||||
);
|
||||
const res = await client.checkConnectivity();
|
||||
if (res) {
|
||||
new Notice("Great! The webdav server can be accessed.");
|
||||
} else {
|
||||
new Notice("The webdav server cannot be reached.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// we need to create chooser
|
||||
// after s3Div and webdavDiv being created
|
||||
new Setting(serviceChooserDiv)
|
||||
.setName("Choose service")
|
||||
.setDesc("Choose a service, by default s3")
|
||||
.addDropdown(async (dropdown) => {
|
||||
const currService = this.plugin.settings.serviceType;
|
||||
const enableExperimentService =
|
||||
this.plugin.settings.enableExperimentService;
|
||||
|
||||
dropdown.addOption("s3", "s3 (-compatible)");
|
||||
if (currService === "webdav" || enableExperimentService) {
|
||||
dropdown.addOption("webdav", "webdav (experimental)");
|
||||
if (!enableExperimentService) {
|
||||
this.plugin.settings.enableExperimentService = true;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.serviceType)
|
||||
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
|
||||
this.plugin.settings.serviceType = val;
|
||||
s3Div.toggleClass(
|
||||
"s3-hide",
|
||||
this.plugin.settings.serviceType !== "s3"
|
||||
);
|
||||
webdavDiv.toggleClass(
|
||||
"webdav-hide",
|
||||
this.plugin.settings.serviceType !== "webdav"
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+14
-2
@@ -4,8 +4,6 @@ import * as path from "path";
|
||||
import { base32 } from "rfc4648";
|
||||
import XRegExp from "xregexp";
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "ftp";
|
||||
|
||||
/**
|
||||
* If any part of the file starts with '.' or '_' then it's a hidden file.
|
||||
* @param item
|
||||
@@ -130,3 +128,17 @@ export const isVaildText = (a: string) => {
|
||||
a
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* If input is already a folder, returns it as is;
|
||||
* And if input is a file, returns its direname.
|
||||
* @param a
|
||||
* @returns
|
||||
*/
|
||||
export const getPathFolder = (a: string) => {
|
||||
if (a.endsWith("/")) {
|
||||
return a;
|
||||
}
|
||||
const b = path.posix.dirname(a);
|
||||
return b.endsWith("/") ? b : `${b}/`;
|
||||
};
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { Vault } from "obsidian";
|
||||
|
||||
import type { SUPPORTED_SERVICES_TYPE } from "./baseTypes";
|
||||
import * as s3 from "./s3";
|
||||
import * as webdav from "./webdav";
|
||||
|
||||
export class RemoteClient {
|
||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
readonly s3Client?: s3.S3Client;
|
||||
readonly s3Config?: s3.S3Config;
|
||||
readonly webdavClient?: webdav.WebDAVClient;
|
||||
readonly webdavConfig?: webdav.WebdavConfig;
|
||||
|
||||
constructor(
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
s3Config?: s3.S3Config,
|
||||
webdavConfig?: webdav.WebdavConfig
|
||||
) {
|
||||
this.serviceType = serviceType;
|
||||
if (serviceType === "s3") {
|
||||
this.s3Config = s3Config;
|
||||
this.s3Client = s3.getS3Client(s3Config);
|
||||
} else if (serviceType === "webdav") {
|
||||
this.webdavConfig = webdavConfig;
|
||||
this.webdavClient = webdav.getWebdavClient(webdavConfig);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
}
|
||||
|
||||
getRemoteMeta = async (fileOrFolderPath: string) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.getRemoteMeta(
|
||||
this.s3Client,
|
||||
this.s3Config,
|
||||
fileOrFolderPath
|
||||
);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
uploadToRemote = async (
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
isRecursively: boolean = false,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.uploadToRemote(
|
||||
this.s3Client,
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
isRecursively,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.uploadToRemote(
|
||||
this.webdavClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
isRecursively,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
listFromRemote = async (prefix?: string) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.listFromRemote(this.s3Client, this.s3Config, prefix);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.listFromRemote(this.webdavClient, prefix);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
downloadFromRemote = async (
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
mtime: number,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.downloadFromRemote(
|
||||
this.s3Client,
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
mtime,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.downloadFromRemote(
|
||||
this.webdavClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
mtime,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
deleteFromRemote = async (
|
||||
fileOrFolderPath: string,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.deleteFromRemote(
|
||||
this.s3Client,
|
||||
this.s3Config,
|
||||
fileOrFolderPath,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.deleteFromRemote(
|
||||
this.webdavClient,
|
||||
fileOrFolderPath,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
|
||||
checkConnectivity = async () => {
|
||||
if (this.serviceType === "s3") {
|
||||
return await s3.checkConnectivity(this.s3Client, this.s3Config);
|
||||
} else if (this.serviceType === "webdav") {
|
||||
return await webdav.checkConnectivity(this.webdavClient);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
HeadBucketCommand,
|
||||
ListObjectsV2CommandInput,
|
||||
ListObjectsV2CommandOutput,
|
||||
HeadObjectCommandOutput,
|
||||
} from "@aws-sdk/client-s3";
|
||||
export { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import type { _Object } from "@aws-sdk/client-s3";
|
||||
|
||||
@@ -24,6 +26,8 @@ import {
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
import * as mime from "mime-types";
|
||||
|
||||
import { RemoteItem } from "./baseTypes";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
|
||||
export interface S3Config {
|
||||
@@ -44,6 +48,29 @@ export const DEFAULT_S3_CONFIG = {
|
||||
|
||||
export type S3ObjectType = _Object;
|
||||
|
||||
const fromS3ObjectToRemoteItem = (x: S3ObjectType) => {
|
||||
return {
|
||||
key: x.Key,
|
||||
lastModified: x.LastModified.valueOf(),
|
||||
size: x.Size,
|
||||
remoteType: "s3",
|
||||
etag: x.ETag,
|
||||
} as RemoteItem;
|
||||
};
|
||||
|
||||
const fromS3HeadObjectToRemoteItem = (
|
||||
key: string,
|
||||
x: HeadObjectCommandOutput
|
||||
) => {
|
||||
return {
|
||||
key: key,
|
||||
lastModified: x.LastModified.valueOf(),
|
||||
size: x.ContentLength,
|
||||
remoteType: "s3",
|
||||
etag: x.ETag,
|
||||
} as RemoteItem;
|
||||
};
|
||||
|
||||
export const getS3Client = (s3Config: S3Config) => {
|
||||
let endpoint = s3Config.s3Endpoint;
|
||||
if (!(endpoint.startsWith("http://") || endpoint.startsWith("https://"))) {
|
||||
@@ -65,12 +92,14 @@ export const getRemoteMeta = async (
|
||||
s3Config: S3Config,
|
||||
fileOrFolderPath: string
|
||||
) => {
|
||||
return await s3Client.send(
|
||||
const res = await s3Client.send(
|
||||
new HeadObjectCommand({
|
||||
Bucket: s3Config.s3BucketName,
|
||||
Key: fileOrFolderPath,
|
||||
})
|
||||
);
|
||||
|
||||
return fromS3HeadObjectToRemoteItem(fileOrFolderPath, res);
|
||||
};
|
||||
|
||||
export const uploadToRemote = async (
|
||||
@@ -181,10 +210,7 @@ export const listFromRemote = async (
|
||||
|
||||
// ensemble fake rsp
|
||||
return {
|
||||
"$.metadata": {
|
||||
httpStatusCode: 200,
|
||||
},
|
||||
Contents: contents,
|
||||
Contents: contents.map((x) => fromS3ObjectToRemoteItem(x)),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -214,7 +240,7 @@ const getObjectBodyToArrayBuffer = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFromRemoteRaw = async (
|
||||
const downloadFromRemoteRaw = async (
|
||||
s3Client: S3Client,
|
||||
s3Config: S3Config,
|
||||
fileOrFolderPath: string
|
||||
@@ -302,7 +328,7 @@ export const deleteFromRemote = async (
|
||||
await s3Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: s3Config.s3BucketName,
|
||||
Key: element.Key,
|
||||
Key: element.key,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -320,7 +346,7 @@ export const deleteFromRemote = async (
|
||||
* @param s3Config
|
||||
* @returns
|
||||
*/
|
||||
export const checkS3Connectivity = async (
|
||||
export const checkConnectivity = async (
|
||||
s3Client: S3Client,
|
||||
s3Config: S3Config
|
||||
) => {
|
||||
|
||||
+68
-69
@@ -1,27 +1,15 @@
|
||||
import { TAbstractFile, TFolder, TFile, Vault } from "obsidian";
|
||||
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import {
|
||||
clearDeleteRenameHistoryOfKey,
|
||||
upsertSyncMetaMappingDataS3,
|
||||
getSyncMetaMappingByRemoteKeyS3,
|
||||
upsertSyncMetaMappingData,
|
||||
getSyncMetaMappingByRemoteKey,
|
||||
} from "./localdb";
|
||||
import type { FileFolderHistoryRecord, InternalDBs } from "./localdb";
|
||||
|
||||
import {
|
||||
S3Config,
|
||||
S3ObjectType,
|
||||
uploadToRemote,
|
||||
deleteFromRemote,
|
||||
downloadFromRemote,
|
||||
} from "./s3";
|
||||
import {
|
||||
mkdirpInVault,
|
||||
SUPPORTED_SERVICES_TYPE,
|
||||
isHiddenPath,
|
||||
isVaildText,
|
||||
} from "./misc";
|
||||
import { RemoteClient } from "./remote";
|
||||
import type { SUPPORTED_SERVICES_TYPE, RemoteItem } from "./baseTypes";
|
||||
import { mkdirpInVault, isHiddenPath, isVaildText } from "./misc";
|
||||
import {
|
||||
decryptBase32ToString,
|
||||
encryptStringToBase32,
|
||||
@@ -85,7 +73,7 @@ export interface PasswordCheckType {
|
||||
}
|
||||
|
||||
export const isPasswordOk = async (
|
||||
remote: S3ObjectType[],
|
||||
remote: RemoteItem[],
|
||||
password: string = ""
|
||||
) => {
|
||||
if (remote === undefined || remote.length === 0) {
|
||||
@@ -95,7 +83,7 @@ export const isPasswordOk = async (
|
||||
reason: "empty_remote",
|
||||
} as PasswordCheckType;
|
||||
}
|
||||
const santyCheckKey = remote[0].Key;
|
||||
const santyCheckKey = remote[0].key;
|
||||
if (santyCheckKey.startsWith(MAGIC_ENCRYPTED_PREFIX_BASE32)) {
|
||||
// this is encrypted!
|
||||
// try to decrypt it using the provided password.
|
||||
@@ -143,26 +131,28 @@ export const isPasswordOk = async (
|
||||
};
|
||||
|
||||
const ensembleMixedStates = async (
|
||||
remote: S3ObjectType[],
|
||||
remote: RemoteItem[],
|
||||
local: TAbstractFile[],
|
||||
deleteHistory: FileFolderHistoryRecord[],
|
||||
db: InternalDBs,
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
password: string = ""
|
||||
) => {
|
||||
const results = {} as Record<string, FileOrFolderMixedState>;
|
||||
|
||||
if (remote !== undefined) {
|
||||
for (const entry of remote) {
|
||||
const remoteEncryptedKey = entry.Key;
|
||||
const remoteEncryptedKey = entry.key;
|
||||
let key = remoteEncryptedKey;
|
||||
if (password !== "") {
|
||||
key = await decryptBase32ToString(remoteEncryptedKey, password);
|
||||
}
|
||||
const backwardMapping = await getSyncMetaMappingByRemoteKeyS3(
|
||||
const backwardMapping = await getSyncMetaMappingByRemoteKey(
|
||||
remoteType,
|
||||
db,
|
||||
key,
|
||||
entry.LastModified.valueOf(),
|
||||
entry.ETag
|
||||
entry.lastModified,
|
||||
entry.etag
|
||||
);
|
||||
|
||||
let r = {} as FileOrFolderMixedState;
|
||||
@@ -171,16 +161,16 @@ const ensembleMixedStates = async (
|
||||
r = {
|
||||
key: key,
|
||||
exist_remote: true,
|
||||
mtime_remote: backwardMapping.localMtime,
|
||||
size_remote: backwardMapping.localSize,
|
||||
mtime_remote: backwardMapping.localMtime || entry.lastModified,
|
||||
size_remote: backwardMapping.localSize || entry.size,
|
||||
remote_encrypted_key: remoteEncryptedKey,
|
||||
};
|
||||
} else {
|
||||
r = {
|
||||
key: key,
|
||||
exist_remote: true,
|
||||
mtime_remote: entry.LastModified.valueOf(),
|
||||
size_remote: entry.Size,
|
||||
mtime_remote: entry.lastModified,
|
||||
size_remote: entry.size,
|
||||
remote_encrypted_key: remoteEncryptedKey,
|
||||
};
|
||||
}
|
||||
@@ -398,14 +388,19 @@ const getOperation = (
|
||||
r.decision_branch = 11;
|
||||
}
|
||||
|
||||
if (r.decision === "unknown") {
|
||||
throw Error(`unknown decision for ${r}`);
|
||||
}
|
||||
|
||||
return r;
|
||||
};
|
||||
|
||||
export const getSyncPlan = async (
|
||||
remote: S3ObjectType[],
|
||||
remote: RemoteItem[],
|
||||
local: TAbstractFile[],
|
||||
deleteHistory: FileFolderHistoryRecord[],
|
||||
db: InternalDBs,
|
||||
remoteType: SUPPORTED_SERVICES_TYPE,
|
||||
password: string = ""
|
||||
) => {
|
||||
const mixedStates = await ensembleMixedStates(
|
||||
@@ -413,6 +408,7 @@ export const getSyncPlan = async (
|
||||
local,
|
||||
deleteHistory,
|
||||
db,
|
||||
remoteType,
|
||||
password
|
||||
);
|
||||
for (const [key, val] of Object.entries(mixedStates)) {
|
||||
@@ -420,27 +416,20 @@ export const getSyncPlan = async (
|
||||
}
|
||||
const plan = {
|
||||
ts: Date.now(),
|
||||
remoteType: "s3",
|
||||
remoteType: remoteType,
|
||||
mixedStates: mixedStates,
|
||||
} as SyncPlanType;
|
||||
return plan;
|
||||
};
|
||||
|
||||
export const doActualSync = async (
|
||||
s3Client: S3Client,
|
||||
s3Config: S3Config,
|
||||
const dispatchOperationToActual = async (
|
||||
key: string,
|
||||
state: FileOrFolderMixedState,
|
||||
client: RemoteClient,
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
syncPlan: SyncPlanType,
|
||||
password: string = ""
|
||||
) => {
|
||||
const keyStates = syncPlan.mixedStates;
|
||||
await Promise.all(
|
||||
Object.entries(keyStates)
|
||||
.sort((k, v) => -(k as string).length)
|
||||
.map(async ([k, v]) => {
|
||||
const key = k as string;
|
||||
const state = v as FileOrFolderMixedState;
|
||||
let remoteEncryptedKey = key;
|
||||
if (password !== "") {
|
||||
remoteEncryptedKey = state.remote_encrypted_key;
|
||||
@@ -458,9 +447,7 @@ export const doActualSync = async (
|
||||
} else if (state.decision === "skip") {
|
||||
// do nothing
|
||||
} else if (state.decision === "download_clearhist") {
|
||||
await downloadFromRemote(
|
||||
s3Client,
|
||||
s3Config,
|
||||
await client.downloadFromRemote(
|
||||
state.key,
|
||||
vault,
|
||||
state.mtime_remote,
|
||||
@@ -469,31 +456,28 @@ export const doActualSync = async (
|
||||
);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
} else if (state.decision === "upload_clearhist") {
|
||||
const remoteObjMeta = await uploadToRemote(
|
||||
s3Client,
|
||||
s3Config,
|
||||
const remoteObjMeta = await client.uploadToRemote(
|
||||
state.key,
|
||||
vault,
|
||||
false,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
await upsertSyncMetaMappingDataS3(
|
||||
await upsertSyncMetaMappingData(
|
||||
client.serviceType,
|
||||
db,
|
||||
state.key,
|
||||
state.mtime_local,
|
||||
state.size_local,
|
||||
state.key,
|
||||
remoteObjMeta.LastModified.valueOf(),
|
||||
remoteObjMeta.ContentLength,
|
||||
remoteObjMeta.ETag
|
||||
remoteObjMeta.lastModified,
|
||||
remoteObjMeta.size,
|
||||
remoteObjMeta.etag
|
||||
);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
} else if (state.decision === "download") {
|
||||
await mkdirpInVault(state.key, vault);
|
||||
await downloadFromRemote(
|
||||
s3Client,
|
||||
s3Config,
|
||||
await client.downloadFromRemote(
|
||||
state.key,
|
||||
vault,
|
||||
state.mtime_remote,
|
||||
@@ -501,39 +485,54 @@ export const doActualSync = async (
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (state.decision === "delremote_clearhist") {
|
||||
await deleteFromRemote(
|
||||
s3Client,
|
||||
s3Config,
|
||||
state.key,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
await client.deleteFromRemote(state.key, password, remoteEncryptedKey);
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
} else if (state.decision === "upload") {
|
||||
const remoteObjMeta = await uploadToRemote(
|
||||
s3Client,
|
||||
s3Config,
|
||||
const remoteObjMeta = await client.uploadToRemote(
|
||||
state.key,
|
||||
vault,
|
||||
false,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
await upsertSyncMetaMappingDataS3(
|
||||
await upsertSyncMetaMappingData(
|
||||
client.serviceType,
|
||||
db,
|
||||
state.key,
|
||||
state.mtime_local,
|
||||
state.size_local,
|
||||
state.key,
|
||||
remoteObjMeta.LastModified.valueOf(),
|
||||
remoteObjMeta.ContentLength,
|
||||
remoteObjMeta.ETag
|
||||
remoteObjMeta.lastModified,
|
||||
remoteObjMeta.size,
|
||||
remoteObjMeta.etag
|
||||
);
|
||||
} else if (state.decision === "clearhist") {
|
||||
await clearDeleteRenameHistoryOfKey(db, state.key);
|
||||
} else {
|
||||
throw Error("this should never happen!");
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
export const doActualSync = async (
|
||||
client: RemoteClient,
|
||||
db: InternalDBs,
|
||||
vault: Vault,
|
||||
syncPlan: SyncPlanType,
|
||||
password: string = ""
|
||||
) => {
|
||||
const keyStates = syncPlan.mixedStates;
|
||||
await Promise.all(
|
||||
Object.entries(keyStates)
|
||||
.sort((k, v) => -(k as string).length)
|
||||
.map(async ([k, v]) =>
|
||||
dispatchOperationToActual(
|
||||
k as string,
|
||||
v as FileOrFolderMixedState,
|
||||
client,
|
||||
db,
|
||||
vault,
|
||||
password
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
+246
@@ -0,0 +1,246 @@
|
||||
import { Buffer } from "buffer";
|
||||
import { FileStats, Vault } from "obsidian";
|
||||
import { AuthType, BufferLike, createClient } from "webdav/web";
|
||||
import type { WebDAVClient, ResponseDataDetailed, FileStat } from "webdav/web";
|
||||
export type { WebDAVClient } from "webdav/web";
|
||||
|
||||
import type { RemoteItem } from "./baseTypes";
|
||||
|
||||
import {
|
||||
arrayBufferToBuffer,
|
||||
bufferToArrayBuffer,
|
||||
mkdirpInVault,
|
||||
getPathFolder,
|
||||
} from "./misc";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
|
||||
export type WebdavAuthType = "digest" | "basic";
|
||||
|
||||
export interface WebdavConfig {
|
||||
address: string;
|
||||
username: string;
|
||||
password: string;
|
||||
authType: WebdavAuthType;
|
||||
}
|
||||
|
||||
export const DEFAULT_WEBDAV_CONFIG = {
|
||||
address: "",
|
||||
username: "",
|
||||
password: "",
|
||||
authType: "basic",
|
||||
} as WebdavConfig;
|
||||
|
||||
const getWebdavPath = (fileOrFolderPath: string) => {
|
||||
if (!fileOrFolderPath.startsWith("/")) {
|
||||
return `/${fileOrFolderPath}`;
|
||||
}
|
||||
return fileOrFolderPath;
|
||||
};
|
||||
|
||||
const getNormPath = (fileOrFolderPath: string) => {
|
||||
if (fileOrFolderPath.startsWith("/")) {
|
||||
return fileOrFolderPath.slice(1);
|
||||
}
|
||||
return fileOrFolderPath;
|
||||
};
|
||||
|
||||
const fromWebdavItemToRemoteItem = (x: FileStat) => {
|
||||
let key = getNormPath(x.filename);
|
||||
if (x.type === "directory" && !key.endsWith("/")) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
return {
|
||||
key: key,
|
||||
lastModified: Date.parse(x.lastmod).valueOf(),
|
||||
size: x.size,
|
||||
remoteType: "webdav",
|
||||
etag: x.etag || undefined,
|
||||
} as RemoteItem;
|
||||
};
|
||||
|
||||
export const getWebdavClient = (webdavConfig: WebdavConfig) => {
|
||||
if (webdavConfig.username !== "" && webdavConfig.password !== "") {
|
||||
return createClient(webdavConfig.address, {
|
||||
username: webdavConfig.username,
|
||||
password: webdavConfig.password,
|
||||
authType:
|
||||
webdavConfig.authType === "digest"
|
||||
? AuthType.Digest
|
||||
: AuthType.Password,
|
||||
});
|
||||
} else {
|
||||
console.log("no password");
|
||||
return createClient(webdavConfig.address);
|
||||
}
|
||||
};
|
||||
|
||||
export const getRemoteMeta = async (
|
||||
client: WebDAVClient,
|
||||
fileOrFolderPath: string
|
||||
) => {
|
||||
const res = (await client.stat(getWebdavPath(fileOrFolderPath), {
|
||||
details: false,
|
||||
})) as FileStat;
|
||||
return fromWebdavItemToRemoteItem(res);
|
||||
};
|
||||
|
||||
export const uploadToRemote = async (
|
||||
client: WebDAVClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
isRecursively: boolean = false,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
let uploadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
uploadFile = remoteEncryptedKey;
|
||||
}
|
||||
uploadFile = getWebdavPath(uploadFile);
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
if (isFolder && isRecursively) {
|
||||
throw Error("upload function doesn't implement recursive function yet!");
|
||||
} else if (isFolder && !isRecursively) {
|
||||
// folder
|
||||
if (password === "") {
|
||||
// if not encrypted, mkdir a remote folder
|
||||
await client.createDirectory(uploadFile, {
|
||||
recursive: true,
|
||||
});
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
} else {
|
||||
// if encrypted, upload a fake file with the encrypted file name
|
||||
await client.putFileContents(uploadFile, "", {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
return await getRemoteMeta(client, uploadFile);
|
||||
}
|
||||
} else {
|
||||
// file
|
||||
// we ignore isRecursively parameter here
|
||||
const localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
||||
let remoteContent = localContent;
|
||||
if (password !== "") {
|
||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
||||
}
|
||||
// we need to create folders before uploading
|
||||
const dir = getPathFolder(uploadFile);
|
||||
if (dir !== "/" && dir !== "") {
|
||||
await client.createDirectory(dir, { recursive: true });
|
||||
}
|
||||
await client.putFileContents(uploadFile, remoteContent, {
|
||||
overwrite: true,
|
||||
onUploadProgress: (progress) => {
|
||||
console.log(`Uploaded ${progress.loaded} bytes of ${progress.total}`);
|
||||
},
|
||||
});
|
||||
|
||||
return await getRemoteMeta(client, uploadFile);
|
||||
}
|
||||
};
|
||||
|
||||
export const listFromRemote = async (client: WebDAVClient, prefix?: string) => {
|
||||
if (prefix !== undefined) {
|
||||
throw Error("prefix not supported");
|
||||
}
|
||||
const contents = (await client.getDirectoryContents("/", {
|
||||
deep: true,
|
||||
details: false /* no need for verbose details here */,
|
||||
glob: "/**" /* avoid dot files by using glob */,
|
||||
})) as FileStat[];
|
||||
return {
|
||||
Contents: contents.map((x) => fromWebdavItemToRemoteItem(x)),
|
||||
};
|
||||
};
|
||||
|
||||
const downloadFromRemoteRaw = async (
|
||||
client: WebDAVClient,
|
||||
fileOrFolderPath: string
|
||||
) => {
|
||||
const buff = (await client.getFileContents(
|
||||
getWebdavPath(fileOrFolderPath)
|
||||
)) as BufferLike;
|
||||
if (buff instanceof ArrayBuffer) {
|
||||
return buff;
|
||||
} else if (buff instanceof Buffer) {
|
||||
return bufferToArrayBuffer(buff);
|
||||
}
|
||||
throw Error(`unexpected file content result with type ${typeof buff}`);
|
||||
};
|
||||
|
||||
export const downloadFromRemote = async (
|
||||
client: WebDAVClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
mtime: number,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
await mkdirpInVault(fileOrFolderPath, vault);
|
||||
|
||||
// the file is always local file
|
||||
// we need to encrypt it
|
||||
|
||||
if (isFolder) {
|
||||
// mkdirp locally is enough
|
||||
// do nothing here
|
||||
} else {
|
||||
let downloadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
downloadFile = remoteEncryptedKey;
|
||||
}
|
||||
downloadFile = getWebdavPath(downloadFile);
|
||||
const remoteContent = await downloadFromRemoteRaw(client, downloadFile);
|
||||
let localContent = remoteContent;
|
||||
if (password !== "") {
|
||||
localContent = await decryptArrayBuffer(remoteContent, password);
|
||||
}
|
||||
await vault.adapter.writeBinary(fileOrFolderPath, localContent, {
|
||||
mtime: mtime,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteFromRemote = async (
|
||||
client: WebDAVClient,
|
||||
fileOrFolderPath: string,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (fileOrFolderPath === "/") {
|
||||
return;
|
||||
}
|
||||
let remoteFileName = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
remoteFileName = remoteEncryptedKey;
|
||||
}
|
||||
remoteFileName = getWebdavPath(remoteFileName);
|
||||
try {
|
||||
await client.deleteFile(remoteFileName);
|
||||
console.log(`delete ${remoteFileName} succeeded`);
|
||||
} catch (err) {
|
||||
console.error("some error while deleting");
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WebDAVClient) => {
|
||||
try {
|
||||
const results = await getRemoteMeta(client, "/");
|
||||
if (results === undefined) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
+11
@@ -7,3 +7,14 @@
|
||||
.s3-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
.s3-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.webdav-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.webdav-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -89,3 +89,25 @@ describe("Misc: vaild file name tests", () => {
|
||||
expect(x).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Misc: get dirname", () => {
|
||||
it("should return itself for folder", async () => {
|
||||
const x = misc.getPathFolder("ssss/");
|
||||
// console.log(x)
|
||||
expect(x).to.equal("ssss/");
|
||||
});
|
||||
|
||||
it("should return folder for file", async () => {
|
||||
const x = misc.getPathFolder("sss/yyy");
|
||||
// console.log(x)
|
||||
expect(x).to.equal("sss/");
|
||||
});
|
||||
|
||||
it("should treat / specially", async () => {
|
||||
const x = misc.getPathFolder("/");
|
||||
expect(x).to.equal("/");
|
||||
|
||||
const y = misc.getPathFolder("/abc");
|
||||
expect(y).to.equal("/");
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"0.1.2": "0.12.15"
|
||||
"0.1.6": "0.12.15"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user