basically working onedrive

This commit is contained in:
fyears 2021-12-29 00:35:46 +08:00
parent f3e6bc2fdd
commit 6552fd32d1
9 changed files with 1088 additions and 17 deletions

View File

@ -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));

View File

@ -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",

View File

@ -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;

View File

@ -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"

View File

@ -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);
};

View File

@ -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
View 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";
};

View File

@ -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;
}

View File

@ -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"),