mirror of
https://github.com/remotely-save/remotely-save.git
synced 2024-06-07 21:10:45 +00:00
basically working onedrive
This commit is contained in:
parent
f3e6bc2fdd
commit
6552fd32d1
@ -12,6 +12,8 @@ if you want to view the source, please visit the github repository of this plugi
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||
const DEFAULT_ONEDRIVE_CLIENT_ID = process.env.ONEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_ONEDRIVE_AUTHORITY = process.env.ONEDRIVE_AUTHORITY || "";
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
@ -40,6 +42,8 @@ esbuild
|
||||
outfile: "main.js",
|
||||
define: {
|
||||
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
|
||||
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
|
||||
},
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
@ -13,13 +13,15 @@
|
||||
"browser": {
|
||||
"path": "path-browserify",
|
||||
"process": "process/browser",
|
||||
"stream": "stream-browserify"
|
||||
"stream": "stream-browserify",
|
||||
"crypto": "crypto-browserify"
|
||||
},
|
||||
"source": "main.ts",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.11.0",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
@ -48,6 +50,8 @@
|
||||
"@aws-sdk/client-s3": "^3.37.0",
|
||||
"@aws-sdk/lib-storage": "^3.40.1",
|
||||
"@aws-sdk/signature-v4-crt": "^3.37.0",
|
||||
"@azure/msal-node": "^1.4.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.1",
|
||||
"acorn": "^8.5.0",
|
||||
"assert": "^2.0.0",
|
||||
"aws-crt": "^1.10.1",
|
||||
|
@ -3,7 +3,7 @@
|
||||
* To avoid circular dependency.
|
||||
*/
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox";
|
||||
export type SUPPORTED_SERVICES_TYPE = "s3" | "webdav" | "dropbox" | "onedrive";
|
||||
|
||||
export interface S3Config {
|
||||
s3Endpoint: string;
|
||||
@ -32,10 +32,22 @@ export interface WebdavConfig {
|
||||
authType: WebdavAuthType;
|
||||
}
|
||||
|
||||
export interface OnedriveConfig {
|
||||
accessToken: string;
|
||||
clientID: string;
|
||||
authority: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
accessTokenExpiresAtTime: number;
|
||||
deltaLink: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface RemotelySavePluginSettings {
|
||||
s3: S3Config;
|
||||
webdav: WebdavConfig;
|
||||
dropbox: DropboxConfig;
|
||||
onedrive: OnedriveConfig;
|
||||
password: string;
|
||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
}
|
||||
@ -50,6 +62,7 @@ export interface RemoteItem {
|
||||
|
||||
export const COMMAND_URI = "remotely-save";
|
||||
export const COMMAND_CALLBACK = "remotely-save-cb";
|
||||
export const COMMAND_CALLBACK_ONEDRIVE = "remotely-save-cb-onedrive";
|
||||
|
||||
export interface UriParams {
|
||||
func?: string;
|
||||
|
287
src/main.ts
287
src/main.ts
@ -31,8 +31,8 @@ import { DEFAULT_S3_CONFIG } from "./remoteForS3";
|
||||
import { DEFAULT_WEBDAV_CONFIG } from "./remoteForWebdav";
|
||||
import {
|
||||
DEFAULT_DROPBOX_CONFIG,
|
||||
getAuthUrlAndVerifier,
|
||||
sendAuthReq,
|
||||
getAuthUrlAndVerifier as getAuthUrlAndVerifierDropbox,
|
||||
sendAuthReq as sendAuthReqDropbox,
|
||||
setConfigBySuccessfullAuthInplace,
|
||||
} from "./remoteForDropbox";
|
||||
|
||||
@ -50,23 +50,49 @@ import type {
|
||||
import type { ProcessQrCodeResultType } from "./importExport";
|
||||
import { exportQrCodeUri, importQrCodeUri } from "./importExport";
|
||||
|
||||
import {
|
||||
getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedrive,
|
||||
sendAuthReq as sendAuthReqOnedrive,
|
||||
DEFAULT_ONEDRIVE_CONFIG,
|
||||
WrappedOnedriveClient,
|
||||
AccessCodeResponseSuccessfulType,
|
||||
} from "./remoteForOnedrive";
|
||||
|
||||
const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
s3: DEFAULT_S3_CONFIG,
|
||||
webdav: DEFAULT_WEBDAV_CONFIG,
|
||||
dropbox: DEFAULT_DROPBOX_CONFIG,
|
||||
onedrive: DEFAULT_ONEDRIVE_CONFIG,
|
||||
password: "",
|
||||
serviceType: "s3",
|
||||
};
|
||||
|
||||
interface OAuth2Info {
|
||||
verifier?: string;
|
||||
helperModal?: Modal;
|
||||
authDiv?: HTMLElement;
|
||||
revokeDiv?: HTMLElement;
|
||||
revokeAuthSetting?: Setting;
|
||||
}
|
||||
|
||||
export default class RemotelySavePlugin extends Plugin {
|
||||
settings: RemotelySavePluginSettings;
|
||||
// cm: CodeMirror.Editor;
|
||||
db: InternalDBs;
|
||||
syncStatus: SyncStatusType;
|
||||
oauth2Info: OAuth2Info;
|
||||
|
||||
async onload() {
|
||||
console.log(`loading plugin ${this.manifest.id}`);
|
||||
|
||||
this.oauth2Info = {
|
||||
verifier: "",
|
||||
helperModal: undefined,
|
||||
authDiv: undefined,
|
||||
revokeDiv: undefined,
|
||||
revokeAuthSetting: undefined,
|
||||
}; // init
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
await this.prepareDB();
|
||||
@ -110,6 +136,77 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
);
|
||||
this.registerObsidianProtocolHandler(
|
||||
"remotely-save-cb-onedrive",
|
||||
async (inputParams) => {
|
||||
if (inputParams.code !== undefined) {
|
||||
let rsp = await sendAuthReqOnedrive(
|
||||
this.settings.onedrive.clientID,
|
||||
this.settings.onedrive.authority,
|
||||
inputParams.code,
|
||||
this.oauth2Info.verifier
|
||||
);
|
||||
|
||||
if ((rsp as any).error !== undefined) {
|
||||
throw Error(`${JSON.stringify(rsp)}`);
|
||||
}
|
||||
|
||||
if (this.oauth2Info.helperModal !== undefined) {
|
||||
this.oauth2Info.helperModal.contentEl.empty();
|
||||
this.oauth2Info.helperModal.contentEl.createEl("p", {
|
||||
text: "Please wait, the plugin is trying to connect to Onedrive...",
|
||||
});
|
||||
}
|
||||
|
||||
rsp = rsp as AccessCodeResponseSuccessfulType;
|
||||
this.settings.onedrive.accessToken = rsp.access_token;
|
||||
this.settings.onedrive.accessTokenExpiresAtTime =
|
||||
Date.now() + rsp.expires_in - 5 * 60 * 1000;
|
||||
this.settings.onedrive.accessTokenExpiresInSeconds = rsp.expires_in;
|
||||
this.settings.onedrive.refreshToken = rsp.refresh_token;
|
||||
this.saveSettings();
|
||||
|
||||
const self = this;
|
||||
const client = new RemoteClient(
|
||||
"onedrive",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.settings.onedrive,
|
||||
this.app.vault.getName(),
|
||||
() => self.saveSettings()
|
||||
);
|
||||
this.settings.onedrive.username = await client.getUser();
|
||||
this.saveSettings();
|
||||
|
||||
this.oauth2Info.verifier = ""; // reset it
|
||||
this.oauth2Info.helperModal?.close(); // close it
|
||||
this.oauth2Info.helperModal = undefined;
|
||||
|
||||
this.oauth2Info.authDiv?.toggleClass(
|
||||
"onedrive-auth-button-hide",
|
||||
this.settings.onedrive.username !== ""
|
||||
);
|
||||
this.oauth2Info.authDiv = undefined;
|
||||
|
||||
this.oauth2Info.revokeAuthSetting?.setDesc(
|
||||
`You've connected as user ${this.settings.dropbox.username}. If you want to disconnect, click this button.`
|
||||
);
|
||||
this.oauth2Info.revokeAuthSetting = undefined;
|
||||
this.oauth2Info.revokeDiv?.toggleClass(
|
||||
"onedrive-revoke-auth-button-hide",
|
||||
this.settings.onedrive.username === ""
|
||||
);
|
||||
this.oauth2Info.revokeDiv = undefined;
|
||||
} else {
|
||||
throw Error(
|
||||
`do not know how to deal with the callback: ${JSON.stringify(
|
||||
inputParams
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.addRibbonIcon("switch", "Remotely Save", async () => {
|
||||
if (this.syncStatus !== "idle") {
|
||||
@ -134,6 +231,7 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.s3,
|
||||
this.settings.webdav,
|
||||
this.settings.dropbox,
|
||||
this.settings.onedrive,
|
||||
this.app.vault.getName(),
|
||||
() => self.saveSettings()
|
||||
);
|
||||
@ -325,7 +423,7 @@ export class DropboxAuthModal extends Modal {
|
||||
async onOpen() {
|
||||
let { contentEl } = this;
|
||||
|
||||
const { authUrl, verifier } = await getAuthUrlAndVerifier(
|
||||
const { authUrl, verifier } = await getAuthUrlAndVerifierDropbox(
|
||||
this.plugin.settings.dropbox.clientID
|
||||
);
|
||||
|
||||
@ -358,7 +456,7 @@ export class DropboxAuthModal extends Modal {
|
||||
button.onClick(async () => {
|
||||
new Notice("Trying to connect to Dropbox");
|
||||
try {
|
||||
const authRes = await sendAuthReq(
|
||||
const authRes = await sendAuthReqDropbox(
|
||||
this.plugin.settings.dropbox.clientID,
|
||||
verifier,
|
||||
authCode
|
||||
@ -374,6 +472,7 @@ export class DropboxAuthModal extends Modal {
|
||||
undefined,
|
||||
undefined,
|
||||
this.plugin.settings.dropbox,
|
||||
undefined,
|
||||
this.app.vault.getName(),
|
||||
() => self.plugin.saveSettings()
|
||||
);
|
||||
@ -407,6 +506,52 @@ export class DropboxAuthModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
export class OnedriveAuthModal extends Modal {
|
||||
readonly plugin: RemotelySavePlugin;
|
||||
readonly authDiv: HTMLDivElement;
|
||||
readonly revokeAuthDiv: HTMLDivElement;
|
||||
readonly revokeAuthSetting: Setting;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
authDiv: HTMLDivElement,
|
||||
revokeAuthDiv: HTMLDivElement,
|
||||
revokeAuthSetting: Setting
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.authDiv = authDiv;
|
||||
this.revokeAuthDiv = revokeAuthDiv;
|
||||
this.revokeAuthSetting = revokeAuthSetting;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
let { contentEl } = this;
|
||||
|
||||
const { authUrl, verifier } = await getAuthUrlAndVerifierOnedrive(
|
||||
this.plugin.settings.onedrive.clientID,
|
||||
this.plugin.settings.onedrive.authority
|
||||
);
|
||||
this.plugin.oauth2Info.verifier = verifier;
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: "Visit the address in a browser, and follow the steps.",
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
text: "Finally you should be redirected to Obsidian.",
|
||||
});
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: authUrl,
|
||||
text: authUrl,
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
export class ExportSettingsQrCodeModal extends Modal {
|
||||
plugin: RemotelySavePlugin;
|
||||
constructor(app: App, plugin: RemotelySavePlugin) {
|
||||
@ -617,11 +762,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new Notice("Checking...");
|
||||
const client = new RemoteClient(
|
||||
"s3",
|
||||
this.plugin.settings.s3,
|
||||
undefined
|
||||
);
|
||||
const client = new RemoteClient("s3", this.plugin.settings.s3);
|
||||
const res = await client.checkConnectivity();
|
||||
if (res) {
|
||||
new Notice("Great! The bucket can be accessed.");
|
||||
@ -659,7 +800,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
cls: "dropbox-revoke-auth-button-hide",
|
||||
});
|
||||
|
||||
const revokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
|
||||
const dropboxRevokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
|
||||
.setName("Revoke Auth")
|
||||
.setDesc(
|
||||
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
|
||||
@ -674,6 +815,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
undefined,
|
||||
undefined,
|
||||
this.plugin.settings.dropbox,
|
||||
undefined,
|
||||
this.app.vault.getName(),
|
||||
() => self.plugin.saveSettings()
|
||||
);
|
||||
@ -709,7 +851,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
this.plugin,
|
||||
dropboxAuthDiv,
|
||||
dropboxRevokeAuthDiv,
|
||||
revokeAuthSetting
|
||||
dropboxRevokeAuthSetting
|
||||
).open();
|
||||
});
|
||||
});
|
||||
@ -736,6 +878,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
undefined,
|
||||
undefined,
|
||||
this.plugin.settings.dropbox,
|
||||
undefined,
|
||||
this.app.vault.getName(),
|
||||
() => self.plugin.saveSettings()
|
||||
);
|
||||
@ -749,6 +892,120 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
});
|
||||
});
|
||||
|
||||
const onedriveDiv = containerEl.createEl("div", { cls: "onedrive-hide" });
|
||||
onedriveDiv.toggleClass(
|
||||
"onedrive-hide",
|
||||
this.plugin.settings.serviceType !== "onedrive"
|
||||
);
|
||||
onedriveDiv.createEl("h2", { text: "Remote For Onedrive" });
|
||||
onedriveDiv.createEl("p", {
|
||||
text: "Disclaimer: This app is NOT an official Onedrive product.",
|
||||
cls: "onedrive-disclaimer",
|
||||
});
|
||||
onedriveDiv.createEl("p", {
|
||||
text: "Disclaimer: The information is stored in PLAIN TEXT locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Onedrive, please immediately disconnect this app on https://microsoft.com/consent .",
|
||||
cls: "onedrive-disclaimer",
|
||||
});
|
||||
onedriveDiv.createEl("p", {
|
||||
text: `We will create and sync inside the folder /Apps/${
|
||||
this.plugin.manifest.id
|
||||
}/${this.app.vault.getName()} on your Onedrive.`,
|
||||
});
|
||||
|
||||
const onedriveSelectAuthDiv = onedriveDiv.createDiv();
|
||||
const onedriveAuthDiv = onedriveSelectAuthDiv.createDiv({
|
||||
cls: "onedrive-auth-button-hide",
|
||||
});
|
||||
const onedriveRevokeAuthDiv = onedriveSelectAuthDiv.createDiv({
|
||||
cls: "onedrive-revoke-auth-button-hide",
|
||||
});
|
||||
|
||||
const onedriveRevokeAuthSetting = new Setting(onedriveRevokeAuthDiv)
|
||||
.setName("Revoke Auth")
|
||||
.setDesc(
|
||||
`You've connected as user ${this.plugin.settings.onedrive.username}. If you want to disconnect, click this button`
|
||||
)
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Revoke Auth");
|
||||
button.onClick(async () => {
|
||||
try {
|
||||
this.plugin.settings.onedrive = JSON.parse(
|
||||
JSON.stringify(DEFAULT_ONEDRIVE_CONFIG)
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
onedriveAuthDiv.toggleClass(
|
||||
"onedrive-auth-button-hide",
|
||||
this.plugin.settings.onedrive.username !== ""
|
||||
);
|
||||
onedriveRevokeAuthDiv.toggleClass(
|
||||
"onedrive-revoke-auth-button-hide",
|
||||
this.plugin.settings.onedrive.username === ""
|
||||
);
|
||||
new Notice("Revoked!");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice("Something goes wrong while revoking");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(onedriveAuthDiv)
|
||||
.setName("Auth")
|
||||
.setDesc("Auth")
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Auth");
|
||||
button.onClick(async () => {
|
||||
const modal = new OnedriveAuthModal(
|
||||
this.app,
|
||||
this.plugin,
|
||||
onedriveAuthDiv,
|
||||
onedriveRevokeAuthDiv,
|
||||
onedriveRevokeAuthSetting
|
||||
);
|
||||
this.plugin.oauth2Info.helperModal = modal;
|
||||
this.plugin.oauth2Info.authDiv = onedriveAuthDiv;
|
||||
this.plugin.oauth2Info.revokeDiv = onedriveRevokeAuthDiv;
|
||||
this.plugin.oauth2Info.revokeAuthSetting = onedriveRevokeAuthSetting;
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
|
||||
onedriveAuthDiv.toggleClass(
|
||||
"onedrive-auth-button-hide",
|
||||
this.plugin.settings.onedrive.username !== ""
|
||||
);
|
||||
onedriveRevokeAuthDiv.toggleClass(
|
||||
"onedrive-revoke-auth-button-hide",
|
||||
this.plugin.settings.onedrive.username === ""
|
||||
);
|
||||
|
||||
new Setting(onedriveDiv)
|
||||
.setName("check connectivity")
|
||||
.setDesc("check connectivity")
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText("Check");
|
||||
button.onClick(async () => {
|
||||
new Notice("Checking...");
|
||||
const self = this;
|
||||
const client = new RemoteClient(
|
||||
"onedrive",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.plugin.settings.onedrive,
|
||||
this.app.vault.getName(),
|
||||
() => self.plugin.saveSettings()
|
||||
);
|
||||
|
||||
const res = await client.checkConnectivity();
|
||||
if (res) {
|
||||
new Notice("Great! We can connect to Onedrive!");
|
||||
} else {
|
||||
new Notice("We cannot connect to Onedrive.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const webdavDiv = containerEl.createEl("div", { cls: "webdav-hide" });
|
||||
webdavDiv.toggleClass(
|
||||
"webdav-hide",
|
||||
@ -836,6 +1093,7 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
undefined,
|
||||
this.plugin.settings.webdav,
|
||||
undefined,
|
||||
undefined,
|
||||
this.app.vault.getName()
|
||||
);
|
||||
const res = await client.checkConnectivity();
|
||||
@ -853,11 +1111,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
.setName("Choose service")
|
||||
.setDesc("Choose a service.")
|
||||
.addDropdown(async (dropdown) => {
|
||||
const currService = this.plugin.settings.serviceType;
|
||||
|
||||
dropdown.addOption("s3", "S3 (-compatible)");
|
||||
dropdown.addOption("dropbox", "Dropbox");
|
||||
dropdown.addOption("webdav", "Webdav");
|
||||
dropdown.addOption("onedrive", "OneDrive (alpha)");
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.serviceType)
|
||||
.onChange(async (val: SUPPORTED_SERVICES_TYPE) => {
|
||||
@ -870,6 +1127,10 @@ class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
"dropbox-hide",
|
||||
this.plugin.settings.serviceType !== "dropbox"
|
||||
);
|
||||
onedriveDiv.toggleClass(
|
||||
"onedrive-hide",
|
||||
this.plugin.settings.serviceType !== "onedrive"
|
||||
);
|
||||
webdavDiv.toggleClass(
|
||||
"webdav-hide",
|
||||
this.plugin.settings.serviceType !== "webdav"
|
||||
|
25
src/misc.ts
25
src/misc.ts
@ -163,3 +163,28 @@ export const extractSvgSub = (x: string, subEl: string = "rect") => {
|
||||
svg.setAttribute("viewbox", "0 0 10 10");
|
||||
return svg.innerHTML;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/18230217
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomIntInclusive = (min: number, max: number) => {
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(randomBuffer);
|
||||
let randomNumber = randomBuffer[0] / (0xffffffff + 1);
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(randomNumber * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
/**
|
||||
* Random buffer
|
||||
* @param byteLength
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomArrayBuffer = (byteLength: number) => {
|
||||
const k = window.crypto.getRandomValues(new Uint8Array(byteLength));
|
||||
return bufferToArrayBuffer(k);
|
||||
};
|
||||
|
@ -5,10 +5,12 @@ import type {
|
||||
S3Config,
|
||||
DropboxConfig,
|
||||
WebdavConfig,
|
||||
OnedriveConfig,
|
||||
} from "./baseTypes";
|
||||
import * as s3 from "./remoteForS3";
|
||||
import * as webdav from "./remoteForWebdav";
|
||||
import * as dropbox from "./remoteForDropbox";
|
||||
import * as onedrive from "./remoteForOnedrive";
|
||||
|
||||
export class RemoteClient {
|
||||
readonly serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
@ -18,12 +20,15 @@ export class RemoteClient {
|
||||
readonly webdavConfig?: WebdavConfig;
|
||||
readonly dropboxClient?: dropbox.WrappedDropboxClient;
|
||||
readonly dropboxConfig?: DropboxConfig;
|
||||
readonly onedriveClient?: onedrive.WrappedOnedriveClient;
|
||||
readonly onedriveConfig?: OnedriveConfig;
|
||||
|
||||
constructor(
|
||||
serviceType: SUPPORTED_SERVICES_TYPE,
|
||||
s3Config?: S3Config,
|
||||
webdavConfig?: WebdavConfig,
|
||||
dropboxConfig?: DropboxConfig,
|
||||
onedriveConfig?: OnedriveConfig,
|
||||
vaultName?: string,
|
||||
saveUpdatedConfigFunc?: () => Promise<any>
|
||||
) {
|
||||
@ -51,6 +56,18 @@ export class RemoteClient {
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
} else if (serviceType === "onedrive") {
|
||||
if (vaultName === undefined || saveUpdatedConfigFunc === undefined) {
|
||||
throw Error(
|
||||
"remember to provide vault name and callback while init onedrive client"
|
||||
);
|
||||
}
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.onedriveClient = onedrive.getOnedriveClient(
|
||||
this.onedriveConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -67,6 +84,11 @@ export class RemoteClient {
|
||||
return await webdav.getRemoteMeta(this.webdavClient, fileOrFolderPath);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.getRemoteMeta(this.dropboxClient, fileOrFolderPath);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.getRemoteMeta(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -109,6 +131,16 @@ export class RemoteClient {
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.uploadToRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
isRecursively,
|
||||
password,
|
||||
remoteEncryptedKey,
|
||||
foldersCreatedBefore
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -121,6 +153,8 @@ export class RemoteClient {
|
||||
return await webdav.listFromRemote(this.webdavClient, prefix);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.listFromRemote(this.dropboxClient, prefix);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.listFromRemote(this.onedriveClient, prefix);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -161,6 +195,15 @@ export class RemoteClient {
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.downloadFromRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
vault,
|
||||
mtime,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -193,6 +236,13 @@ export class RemoteClient {
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.deleteFromRemote(
|
||||
this.onedriveClient,
|
||||
fileOrFolderPath,
|
||||
password,
|
||||
remoteEncryptedKey
|
||||
);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -205,6 +255,8 @@ export class RemoteClient {
|
||||
return await webdav.checkConnectivity(this.webdavClient);
|
||||
} else if (this.serviceType === "dropbox") {
|
||||
return await dropbox.checkConnectivity(this.dropboxClient);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.checkConnectivity(this.onedriveClient);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
@ -213,6 +265,8 @@ export class RemoteClient {
|
||||
getUser = async () => {
|
||||
if (this.serviceType === "dropbox") {
|
||||
return await dropbox.getUserDisplayName(this.dropboxClient);
|
||||
} else if (this.serviceType === "onedrive") {
|
||||
return await onedrive.getUserDisplayName(this.onedriveClient);
|
||||
} else {
|
||||
throw Error(`not supported service type ${this.serviceType}`);
|
||||
}
|
||||
|
691
src/remoteForOnedrive.ts
Normal file
691
src/remoteForOnedrive.ts
Normal file
@ -0,0 +1,691 @@
|
||||
import * as path from "path";
|
||||
import { request, Vault } from "obsidian";
|
||||
import { PublicClientApplication, CryptoProvider } from "@azure/msal-node";
|
||||
import { COMMAND_CALLBACK_ONEDRIVE } from "./baseTypes";
|
||||
import type { OnedriveConfig, RemoteItem } from "./baseTypes";
|
||||
|
||||
import {
|
||||
Client,
|
||||
FileUpload,
|
||||
UploadEventHandlers,
|
||||
AuthenticationProvider,
|
||||
AuthenticationProviderOptions,
|
||||
Range,
|
||||
LargeFileUploadSession,
|
||||
LargeFileUploadTask,
|
||||
LargeFileUploadTaskOptions,
|
||||
UploadResult,
|
||||
} from "@microsoft/microsoft-graph-client";
|
||||
import type { Drive, DriveItem, User } from "@microsoft/microsoft-graph-types";
|
||||
import {
|
||||
getFolderLevels,
|
||||
getPathFolder,
|
||||
getRandomArrayBuffer,
|
||||
getRandomIntInclusive,
|
||||
mkdirpInVault,
|
||||
} from "./misc";
|
||||
import { decryptArrayBuffer, encryptArrayBuffer } from "./encrypt";
|
||||
|
||||
const SCOPES = ["User.Read", "Files.ReadWrite.AppFolder", "offline_access"];
|
||||
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVE}`;
|
||||
|
||||
export const DEFAULT_ONEDRIVE_CONFIG: OnedriveConfig = {
|
||||
accessToken: "",
|
||||
clientID: process.env.DEFAULT_ONEDRIVE_CLIENT_ID,
|
||||
authority: process.env.DEFAULT_ONEDRIVE_AUTHORITY,
|
||||
refreshToken: "",
|
||||
accessTokenExpiresInSeconds: 0,
|
||||
accessTokenExpiresAtTime: 0,
|
||||
deltaLink: "",
|
||||
username: "",
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Onedrive authorization using PKCE
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
export async function getAuthUrlAndVerifier(
|
||||
clientID: string,
|
||||
authority: string
|
||||
) {
|
||||
const cryptoProvider = new CryptoProvider();
|
||||
const { verifier, challenge } = await cryptoProvider.generatePkceCodes();
|
||||
|
||||
const pkceCodes = {
|
||||
challengeMethod: "S256", // Use SHA256 Algorithm
|
||||
verifier: verifier,
|
||||
challenge: challenge,
|
||||
};
|
||||
|
||||
const authCodeUrlParams = {
|
||||
redirectUri: REDIRECT_URI,
|
||||
scopes: SCOPES,
|
||||
codeChallenge: pkceCodes.challenge, // PKCE Code Challenge
|
||||
codeChallengeMethod: pkceCodes.challengeMethod, // PKCE Code Challenge Method
|
||||
};
|
||||
|
||||
const pca = new PublicClientApplication({
|
||||
auth: {
|
||||
clientId: clientID,
|
||||
authority: authority,
|
||||
},
|
||||
});
|
||||
const authCodeUrl = await pca.getAuthCodeUrl(authCodeUrlParams);
|
||||
|
||||
return {
|
||||
authUrl: authCodeUrl,
|
||||
verifier: verifier,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check doc from
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
* https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow
|
||||
*/
|
||||
export interface AccessCodeResponseSuccessfulType {
|
||||
token_type: "Bearer" | "bearer";
|
||||
expires_in: number;
|
||||
ext_expires_in?: number;
|
||||
scope: string;
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
id_token?: string;
|
||||
}
|
||||
export interface AccessCodeResponseFailedType {
|
||||
error: string;
|
||||
error_description: string;
|
||||
error_codes: number[];
|
||||
timestamp: string;
|
||||
trace_id: string;
|
||||
correlation_id: string;
|
||||
}
|
||||
|
||||
export const sendAuthReq = async (
|
||||
clientID: string,
|
||||
authority: string,
|
||||
authCode: string,
|
||||
verifier: string
|
||||
) => {
|
||||
// // original code snippets for references
|
||||
// const authResponse = await pca.acquireTokenByCode({
|
||||
// redirectUri: REDIRECT_URI,
|
||||
// scopes: SCOPES,
|
||||
// code: authCode,
|
||||
// codeVerifier: verifier, // PKCE Code Verifier
|
||||
// });
|
||||
// console.log('authResponse')
|
||||
// console.log(authResponse)
|
||||
// return authResponse;
|
||||
|
||||
// Because of the CORS problem,
|
||||
// we need to construct raw request using Obsidian request,
|
||||
// instead of using msal
|
||||
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/getting-started/graph-oauth?view=odsp-graph-online#code-flow
|
||||
const rsp1 = await request({
|
||||
url: `${authority}/oauth2/v2.0/token`,
|
||||
method: "POST",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: new URLSearchParams({
|
||||
tenant: "consumers",
|
||||
client_id: clientID,
|
||||
scope: SCOPES.join(" "),
|
||||
code: authCode,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
grant_type: "authorization_code",
|
||||
code_verifier: verifier,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// console.log(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
};
|
||||
|
||||
export const sendRefreshTokenReq = async (
|
||||
clientID: string,
|
||||
authority: string,
|
||||
refreshToken: string
|
||||
) => {
|
||||
// also use Obsidian request to bypass CORS issue.
|
||||
const rsp1 = await request({
|
||||
url: `${authority}/oauth2/v2.0/token`,
|
||||
method: "POST",
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
body: new URLSearchParams({
|
||||
tenant: "consumers",
|
||||
client_id: clientID,
|
||||
scope: SCOPES.join(" "),
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
const rsp2 = JSON.parse(rsp1);
|
||||
// console.log(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Other usual common methods
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const getOnedrivePath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/concepts/special-folders-appfolder?view=odsp-graph-online
|
||||
const prefix = `/drive/special/approot:/${vaultName}`;
|
||||
if (fileOrFolderPath.startsWith(prefix)) {
|
||||
// already transformed, return as is
|
||||
return fileOrFolderPath;
|
||||
}
|
||||
|
||||
let key = fileOrFolderPath;
|
||||
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
|
||||
// special
|
||||
return prefix;
|
||||
}
|
||||
if (key.endsWith("/")) {
|
||||
key = key.slice(0, key.length - 1);
|
||||
}
|
||||
|
||||
key = `${prefix}/${key}`;
|
||||
return key;
|
||||
};
|
||||
|
||||
const getNormPath = (fileOrFolderPath: string, vaultName: string) => {
|
||||
const prefix = `/drive/special/approot:/${vaultName}`;
|
||||
|
||||
if (
|
||||
!(fileOrFolderPath === prefix || fileOrFolderPath.startsWith(`${prefix}/`))
|
||||
) {
|
||||
throw Error(
|
||||
`"${fileOrFolderPath}" doesn't starts with "${prefix}/" or equals to "${prefix}"`
|
||||
);
|
||||
}
|
||||
|
||||
if (fileOrFolderPath === prefix) {
|
||||
return "/";
|
||||
}
|
||||
return fileOrFolderPath.slice(`${prefix}/`.length);
|
||||
};
|
||||
|
||||
const fromDriveItemToRemoteItem = (
|
||||
x: DriveItem,
|
||||
vaultName: string
|
||||
): RemoteItem => {
|
||||
let key = "";
|
||||
|
||||
const COMMON_PREFIX = `/drive/root:/Apps/remotely-save/${vaultName}`;
|
||||
const COMMON_PREFIX_OTHERS = `/drive/items/`;
|
||||
if (`${x.parentReference.path}/${x.name}`.startsWith(COMMON_PREFIX)) {
|
||||
key = `${x.parentReference.path}/${x.name}`.substring(
|
||||
COMMON_PREFIX.length + 1
|
||||
);
|
||||
} else if (x.parentReference.path.startsWith(COMMON_PREFIX_OTHERS)) {
|
||||
// it's something like
|
||||
// /drive/items/<some_id>!<another_id>:/${vaultName}/<subfolder>
|
||||
// with uri encoded!
|
||||
const parPath = decodeURIComponent(x.parentReference.path);
|
||||
key = parPath.substring(parPath.indexOf(":") + 1);
|
||||
if (key.startsWith(`/${vaultName}/`)) {
|
||||
key = key.substring(`/${vaultName}/`.length);
|
||||
key = `${key}/${x.name}`;
|
||||
} else if (key === `/${vaultName}`) {
|
||||
key = x.name;
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${JSON.stringify(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${JSON.stringify(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const isFolder = "folder" in x;
|
||||
if (isFolder) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
return {
|
||||
key: key,
|
||||
lastModified: Date.parse(x.fileSystemInfo.lastModifiedDateTime),
|
||||
size: isFolder ? 0 : x.size,
|
||||
remoteType: "onedrive",
|
||||
etag: x.eTag || x.cTag || "",
|
||||
};
|
||||
};
|
||||
|
||||
// to adapt to the required interface
|
||||
class MyAuthProvider implements AuthenticationProvider {
|
||||
onedriveConfig: OnedriveConfig;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveConfig: OnedriveConfig,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
}
|
||||
getAccessToken = async () => {
|
||||
if (
|
||||
this.onedriveConfig.accessToken === "" ||
|
||||
this.onedriveConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
const currentTs = Date.now();
|
||||
if (this.onedriveConfig.accessTokenExpiresAtTime > currentTs) {
|
||||
return this.onedriveConfig.accessToken;
|
||||
} else {
|
||||
// use refreshToken to refresh
|
||||
const r = await sendRefreshTokenReq(
|
||||
this.onedriveConfig.clientID,
|
||||
this.onedriveConfig.authority,
|
||||
this.onedriveConfig.refreshToken
|
||||
);
|
||||
if ((r as any).error !== undefined) {
|
||||
const r2 = r as AccessCodeResponseFailedType;
|
||||
throw Error(
|
||||
`Error while refreshing accessToken: ${r2.error}, ${r2.error_codes}: ${r2.error_description}`
|
||||
);
|
||||
}
|
||||
const r2 = r as AccessCodeResponseSuccessfulType;
|
||||
this.onedriveConfig.accessToken = r2.access_token;
|
||||
this.onedriveConfig.refreshToken = r2.refresh_token;
|
||||
this.onedriveConfig.accessTokenExpiresInSeconds = r2.expires_in;
|
||||
this.onedriveConfig.accessTokenExpiresAtTime =
|
||||
currentTs + r2.expires_in * 1000 - 60 * 2 * 1000;
|
||||
await this.saveUpdatedConfigFunc();
|
||||
console.log("Onedrive accessToken updated");
|
||||
return this.onedriveConfig.accessToken;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class WrappedOnedriveClient {
|
||||
onedriveConfig: OnedriveConfig;
|
||||
vaultName: string;
|
||||
client: Client;
|
||||
vaultFolderExists: boolean;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveConfig = onedriveConfig;
|
||||
this.vaultName = vaultName;
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
this.client = Client.initWithMiddleware({
|
||||
authProvider: new MyAuthProvider(onedriveConfig, saveUpdatedConfigFunc),
|
||||
});
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
// check token
|
||||
if (
|
||||
this.onedriveConfig.accessToken === "" ||
|
||||
this.onedriveConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// console.log(`checking remote has folder /${this.vaultName}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// console.log(`already checked, /${this.vaultName} exist before`)
|
||||
} else {
|
||||
const k = await this.client.api("/drive/special/approot/children").get();
|
||||
// console.log(k);
|
||||
this.vaultFolderExists =
|
||||
(k.value as DriveItem[]).filter((x) => x.name === this.vaultName)
|
||||
.length > 0;
|
||||
if (!this.vaultFolderExists) {
|
||||
console.log(`remote does not have folder /${this.vaultName}`);
|
||||
await this.client.api("/drive/special/approot/children").post({
|
||||
name: `${this.vaultName}`,
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
console.log(`remote folder /${this.vaultName} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// console.log(`remote folder /${this.vaultName} exists`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getOnedriveClient = (
|
||||
onedriveConfig: OnedriveConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) => {
|
||||
return new WrappedOnedriveClient(
|
||||
onedriveConfig,
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use delta api to list all files and folders
|
||||
* https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_delta?view=odsp-graph-online
|
||||
* @param client
|
||||
* @param prefix
|
||||
*/
|
||||
export const listFromRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
prefix?: string
|
||||
) => {
|
||||
if (prefix !== undefined) {
|
||||
throw Error("prefix not supported (yet)");
|
||||
}
|
||||
await client.init();
|
||||
|
||||
const NEXT_LINK_KEY = "@odata.nextLink";
|
||||
const DELTA_LINK_KEY = "@odata.deltaLink";
|
||||
let res = await client.client
|
||||
.api(`/drive/special/approot:/${client.vaultName}:/delta`)
|
||||
.get();
|
||||
const driveItems = res.value as DriveItem[];
|
||||
|
||||
while (NEXT_LINK_KEY in res) {
|
||||
res = await client.client.api(res[NEXT_LINK_KEY]).get();
|
||||
driveItems.push(...JSON.parse(JSON.stringify(res.value as DriveItem[])));
|
||||
}
|
||||
|
||||
// lastly we should have delta link?
|
||||
if (DELTA_LINK_KEY in res) {
|
||||
client.onedriveConfig.deltaLink = res[DELTA_LINK_KEY];
|
||||
await client.saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
// unify everything to RemoteItem
|
||||
const unifiedContents = driveItems
|
||||
.map((x) => fromDriveItemToRemoteItem(x, client.vaultName))
|
||||
.filter((x) => x.key !== "/");
|
||||
return {
|
||||
Contents: unifiedContents,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRemoteMeta = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string
|
||||
) => {
|
||||
await client.init();
|
||||
const remotePath = getOnedrivePath(fileOrFolderPath, client.vaultName);
|
||||
// console.log(`remotePath=${remotePath}`);
|
||||
const rsp = await client.client
|
||||
.api(remotePath)
|
||||
.select("cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size")
|
||||
.get();
|
||||
// console.log(rsp);
|
||||
const driveItem = rsp as DriveItem;
|
||||
const res = fromDriveItemToRemoteItem(driveItem, client.vaultName);
|
||||
// console.log(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
export const uploadToRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
isRecursively: boolean = false,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = "",
|
||||
foldersCreatedBefore: Set<string> | undefined = undefined
|
||||
) => {
|
||||
await client.init();
|
||||
|
||||
let uploadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
uploadFile = remoteEncryptedKey;
|
||||
}
|
||||
uploadFile = getOnedrivePath(uploadFile, client.vaultName);
|
||||
// console.log(`uploadFile=${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
|
||||
if (foldersCreatedBefore?.has(uploadFile)) {
|
||||
// created, pass
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/56479865/creating-nested-folders-in-one-go-onedrive-api
|
||||
// use PATCH to create folder recursively!!!
|
||||
await client.client.api(uploadFile).patch({
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
}
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
} else {
|
||||
// if encrypted,
|
||||
// upload a fake, random-size file
|
||||
// with the encrypted file name
|
||||
const byteLengthRandom = getRandomIntInclusive(
|
||||
1,
|
||||
65536 /* max allowed */
|
||||
);
|
||||
const arrBufRandom = await encryptArrayBuffer(
|
||||
getRandomArrayBuffer(byteLengthRandom),
|
||||
password
|
||||
);
|
||||
|
||||
const uploadSession: LargeFileUploadSession =
|
||||
await LargeFileUploadTask.createUploadSession(
|
||||
client.client,
|
||||
`https://graph.microsoft.com/v1.0/me${encodeURIComponent(
|
||||
uploadFile
|
||||
)}:/createUploadSession`,
|
||||
{
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
},
|
||||
}
|
||||
);
|
||||
const task = new LargeFileUploadTask(
|
||||
client.client,
|
||||
new FileUpload(
|
||||
arrBufRandom,
|
||||
path.posix.basename(uploadFile),
|
||||
arrBufRandom.byteLength
|
||||
),
|
||||
uploadSession,
|
||||
{
|
||||
rangeSize: 1024 * 1024,
|
||||
uploadEventHandlers: {
|
||||
progress: (range?: Range) => {
|
||||
// Handle progress event
|
||||
// console.log(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
},
|
||||
} as UploadEventHandlers,
|
||||
} as LargeFileUploadTaskOptions
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// console.log(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
// file
|
||||
// we ignore isRecursively parameter here
|
||||
const localContent = await vault.adapter.readBinary(fileOrFolderPath);
|
||||
let remoteContent = localContent;
|
||||
if (password !== "") {
|
||||
remoteContent = await encryptArrayBuffer(localContent, password);
|
||||
}
|
||||
|
||||
// no need to create parent folders firstly, cool!
|
||||
|
||||
// we need to customize the special root folder,
|
||||
// so use LargeFileUploadTask instead of OneDriveLargeFileUploadTask
|
||||
const progress = (range?: Range) => {
|
||||
// Handle progress event
|
||||
// console.log(
|
||||
// `uploading ${range.minValue}-${range.maxValue} of ${fileOrFolderPath}`
|
||||
// );
|
||||
};
|
||||
const uploadEventHandlers: UploadEventHandlers = {
|
||||
progress: progress,
|
||||
};
|
||||
const options: LargeFileUploadTaskOptions = {
|
||||
rangeSize: 1024 * 1024,
|
||||
uploadEventHandlers: uploadEventHandlers,
|
||||
};
|
||||
const payload = {
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
},
|
||||
};
|
||||
// uploadFile already starts with /drive/special/approot:/${vaultName}
|
||||
const uploadSession: LargeFileUploadSession =
|
||||
await LargeFileUploadTask.createUploadSession(
|
||||
client.client,
|
||||
`https://graph.microsoft.com/v1.0/me${encodeURIComponent(
|
||||
uploadFile
|
||||
)}:/createUploadSession`,
|
||||
payload
|
||||
);
|
||||
const fileObject = new FileUpload(
|
||||
remoteContent,
|
||||
path.posix.basename(uploadFile),
|
||||
remoteContent.byteLength
|
||||
);
|
||||
const task = new LargeFileUploadTask(
|
||||
client.client,
|
||||
fileObject,
|
||||
uploadSession,
|
||||
options
|
||||
);
|
||||
const uploadResult: UploadResult = await task.upload();
|
||||
// console.log(uploadResult)
|
||||
const res = await getRemoteMeta(client, uploadFile);
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadFromRemoteRaw = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string
|
||||
): Promise<ArrayBuffer> => {
|
||||
await client.init();
|
||||
const key = getOnedrivePath(fileOrFolderPath, client.vaultName);
|
||||
const rsp = await client.client
|
||||
.api(key)
|
||||
.select("@microsoft.graph.downloadUrl")
|
||||
.get();
|
||||
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
||||
const content = await (await fetch(downloadUrl)).arrayBuffer();
|
||||
return content;
|
||||
};
|
||||
|
||||
export const downloadFromRemote = async (
|
||||
client: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
vault: Vault,
|
||||
mtime: number,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
await client.init();
|
||||
|
||||
const isFolder = fileOrFolderPath.endsWith("/");
|
||||
|
||||
await mkdirpInVault(fileOrFolderPath, vault);
|
||||
|
||||
if (isFolder) {
|
||||
// mkdirp locally is enough
|
||||
// do nothing here
|
||||
} else {
|
||||
let downloadFile = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
downloadFile = remoteEncryptedKey;
|
||||
}
|
||||
downloadFile = getOnedrivePath(downloadFile, client.vaultName);
|
||||
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: WrappedOnedriveClient,
|
||||
fileOrFolderPath: string,
|
||||
password: string = "",
|
||||
remoteEncryptedKey: string = ""
|
||||
) => {
|
||||
if (fileOrFolderPath === "/") {
|
||||
return;
|
||||
}
|
||||
let remoteFileName = fileOrFolderPath;
|
||||
if (password !== "") {
|
||||
remoteFileName = remoteEncryptedKey;
|
||||
}
|
||||
remoteFileName = getOnedrivePath(remoteFileName, client.vaultName);
|
||||
|
||||
await client.init();
|
||||
await client.client.api(remoteFileName).delete();
|
||||
};
|
||||
|
||||
export const checkConnectivity = async (client: WrappedOnedriveClient) => {
|
||||
try {
|
||||
const k = await getUserDisplayName(client);
|
||||
return k !== "<unknown display name>";
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getUserDisplayName = async (client: WrappedOnedriveClient) => {
|
||||
await client.init();
|
||||
const res: User = await client.client.api("/me").select("displayName").get();
|
||||
return res.displayName || "<unknown display name>";
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#send-a-sign-out-request
|
||||
* https://docs.microsoft.com/en-us/graph/api/user-revokesigninsessions
|
||||
* https://docs.microsoft.com/en-us/graph/api/user-invalidateallrefreshtokens
|
||||
* @param client
|
||||
*/
|
||||
// export const revokeAuth = async (client: WrappedOnedriveClient) => {
|
||||
// await client.init();
|
||||
// await client.client.api('/me/revokeSignInSessions').post(undefined);
|
||||
// };
|
||||
|
||||
export const getRevokeAddr = async () => {
|
||||
return "https://account.live.com/consent/Manage";
|
||||
};
|
15
styles.css
15
styles.css
@ -30,6 +30,21 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
.onedrive-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onedrive-revoke-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.webdav-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ const webpack = require("webpack");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
|
||||
const DEFAULT_DROPBOX_APP_KEY = process.env.DROPBOX_APP_KEY || "";
|
||||
const DEFAULT_ONEDRIVE_CLIENT_ID = process.env.ONEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_ONEDRIVE_AUTHORITY = process.env.ONEDRIVE_AUTHORITY || "";
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/main.ts",
|
||||
@ -16,6 +18,8 @@ module.exports = {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.DEFAULT_DROPBOX_APP_KEY": `"${DEFAULT_DROPBOX_APP_KEY}"`,
|
||||
"process.env.DEFAULT_ONEDRIVE_CLIENT_ID": `"${DEFAULT_ONEDRIVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_ONEDRIVE_AUTHORITY": `"${DEFAULT_ONEDRIVE_AUTHORITY}"`,
|
||||
}),
|
||||
// Work around for Buffer is undefined:
|
||||
// https://github.com/webpack/changelog-v5/issues/10
|
||||
@ -47,8 +51,8 @@ module.exports = {
|
||||
// buffer: require.resolve("buffer/"),
|
||||
// console: require.resolve("console-browserify"),
|
||||
// constants: require.resolve("constants-browserify"),
|
||||
// crypto: require.resolve("crypto-browserify"),
|
||||
crypto: false,
|
||||
crypto: require.resolve("crypto-browserify"),
|
||||
// crypto: false,
|
||||
// domain: require.resolve("domain-browser"),
|
||||
// events: require.resolve("events"),
|
||||
// http: require.resolve("stream-http"),
|
||||
|
Loading…
Reference in New Issue
Block a user