onedrive full
This commit is contained in:
@@ -278,6 +278,11 @@ export const checkProRunnableAndFixInplace = async (
|
||||
service: "googledrive",
|
||||
name: "Google Drive",
|
||||
},
|
||||
{
|
||||
feature: "feature-onedrive_full",
|
||||
service: "onedrivefull",
|
||||
name: "Onedrive (Full)",
|
||||
},
|
||||
{ feature: "feature-box", service: "box", name: "Box" },
|
||||
{ feature: "feature-pcloud", service: "pcloud", name: "pCloud" },
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ export const PRO_WEBSITE = global.DEFAULT_REMOTELYSAVE_WEBSITE;
|
||||
|
||||
export type PRO_FEATURE_TYPE =
|
||||
| "feature-smart_conflict"
|
||||
| "feature-onedrive_full"
|
||||
| "feature-google_drive"
|
||||
| "feature-box"
|
||||
| "feature-pcloud"
|
||||
@@ -167,3 +168,24 @@ export interface AzureBlobStorageConfig {
|
||||
partsConcurrency: number;
|
||||
kind: "azureblobstorage";
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Onedrive (Full)
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
export const COMMAND_CALLBACK_ONEDRIVEFULL = "remotely-save-cb-onedrivefull";
|
||||
|
||||
export interface OnedriveFullConfig {
|
||||
accessToken: string;
|
||||
clientID: string;
|
||||
authority: string;
|
||||
refreshToken: string;
|
||||
accessTokenExpiresInSeconds: number;
|
||||
accessTokenExpiresAtTime: number;
|
||||
deltaLink: string;
|
||||
username: string;
|
||||
credentialsShouldBeDeletedAtTime?: number;
|
||||
remoteBaseDir?: string;
|
||||
emptyFile: "skip" | "error";
|
||||
kind: "onedrivefull";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,948 @@
|
||||
import { CryptoProvider, PublicClientApplication } from "@azure/msal-node";
|
||||
import type { AuthenticationProvider } from "@microsoft/microsoft-graph-client";
|
||||
import type {
|
||||
DriveItem,
|
||||
FileSystemInfo,
|
||||
UploadSession,
|
||||
User,
|
||||
} from "@microsoft/microsoft-graph-types";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { request, requestUrl } from "obsidian";
|
||||
import {
|
||||
DEFAULT_CONTENT_TYPE,
|
||||
type Entity,
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
ONEDRIVE_AUTHORITY,
|
||||
ONEDRIVE_CLIENT_ID,
|
||||
} from "../../src/baseTypes";
|
||||
import { VALID_REQURL } from "../../src/baseTypesObs";
|
||||
import { FakeFs } from "../../src/fsAll";
|
||||
import { bufferToArrayBuffer } from "../../src/misc";
|
||||
import {
|
||||
COMMAND_CALLBACK_ONEDRIVEFULL,
|
||||
type OnedriveFullConfig,
|
||||
} from "./baseTypesPro";
|
||||
|
||||
const SCOPES = ["User.Read", "Files.ReadWrite", "offline_access"]; // not using Files.ReadWrite.All
|
||||
const REDIRECT_URI = `obsidian://${COMMAND_CALLBACK_ONEDRIVEFULL}`; // diff from Onedrive (App Folder)
|
||||
|
||||
export const DEFAULT_ONEDRIVEFULL_CONFIG: OnedriveFullConfig = {
|
||||
accessToken: "",
|
||||
clientID: ONEDRIVE_CLIENT_ID ?? "",
|
||||
authority: ONEDRIVE_AUTHORITY ?? "",
|
||||
refreshToken: "",
|
||||
accessTokenExpiresInSeconds: 0,
|
||||
accessTokenExpiresAtTime: 0,
|
||||
deltaLink: "",
|
||||
username: "",
|
||||
credentialsShouldBeDeletedAtTime: 0,
|
||||
emptyFile: "skip",
|
||||
kind: "onedrivefull",
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 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,
|
||||
errorCallBack: any
|
||||
) => {
|
||||
// // original code snippets for references
|
||||
// const authResponse = await pca.acquireTokenByCode({
|
||||
// redirectUri: REDIRECT_URI,
|
||||
// scopes: SCOPES,
|
||||
// code: authCode,
|
||||
// codeVerifier: verifier, // PKCE Code Verifier
|
||||
// });
|
||||
// console.info('authResponse')
|
||||
// console.info(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
|
||||
try {
|
||||
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.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
await errorCallBack(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const sendRefreshTokenReq = async (
|
||||
clientID: string,
|
||||
authority: string,
|
||||
refreshToken: string
|
||||
) => {
|
||||
// also use Obsidian request to bypass CORS issue.
|
||||
try {
|
||||
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.info(rsp2);
|
||||
|
||||
if (rsp2.error !== undefined) {
|
||||
return rsp2 as AccessCodeResponseFailedType;
|
||||
} else {
|
||||
return rsp2 as AccessCodeResponseSuccessfulType;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const setConfigBySuccessfullAuthInplace = async (
|
||||
config: OnedriveFullConfig,
|
||||
authRes: AccessCodeResponseSuccessfulType,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
console.info("start updating local info of OneDrive token");
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresAtTime =
|
||||
Date.now() + authRes.expires_in - 5 * 60 * 1000;
|
||||
config.accessTokenExpiresInSeconds = authRes.expires_in;
|
||||
config.refreshToken = authRes.refresh_token!;
|
||||
|
||||
// manually set it expired after 80 days;
|
||||
config.credentialsShouldBeDeletedAtTime =
|
||||
Date.now() + OAUTH2_FORCE_EXPIRE_MILLISECONDS;
|
||||
|
||||
if (saveUpdatedConfigFunc !== undefined) {
|
||||
await saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
console.info("finish updating local info of Onedrive token");
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Other usual common methods
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* This is different from Onedrive (App Folder)
|
||||
*/
|
||||
const getOnedrivePath = (fileOrFolderPath: string, remoteBaseDir: string) => {
|
||||
// https://learn.microsoft.com/en-us/onedrive/developer/rest-api/concepts/addressing-driveitems?view=odsp-graph-online
|
||||
const prefix = `/drive/root:/${remoteBaseDir}`;
|
||||
|
||||
let key = fileOrFolderPath;
|
||||
if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
|
||||
// special
|
||||
return prefix;
|
||||
}
|
||||
if (key.endsWith("/")) {
|
||||
key = key.slice(0, key.length - 1);
|
||||
}
|
||||
|
||||
if (key.startsWith("/")) {
|
||||
console.warn(`why the path ${key} starts with '/'? but we just go on.`);
|
||||
key = `${prefix}${key}`;
|
||||
} else {
|
||||
key = `${prefix}/${key}`;
|
||||
}
|
||||
return key;
|
||||
};
|
||||
|
||||
const constructFromDriveItemToEntityError = (x: DriveItem) => {
|
||||
const fullPathOriginal = `${x.parentReference?.path}/${x.name}`;
|
||||
return `parentPath="${
|
||||
x.parentReference?.path ?? "(no parentReference or path)"
|
||||
}", selfName="${x.name}"`;
|
||||
};
|
||||
|
||||
const fromDriveItemToEntity = (x: DriveItem, remoteBaseDir: string): Entity => {
|
||||
let key = "";
|
||||
|
||||
// possible prefix:
|
||||
// pure english: /drive/root:${remoteBaseDir}
|
||||
const FIRST_COMMON_PREFIX_RAW = `/drive/root:/${remoteBaseDir}`;
|
||||
const SECOND_COMMON_PREFIX_RAW = `/drive/root:/${encodeURIComponent(
|
||||
remoteBaseDir
|
||||
)}`;
|
||||
|
||||
if (
|
||||
x.parentReference === undefined ||
|
||||
x.parentReference === null ||
|
||||
x.parentReference.path === undefined ||
|
||||
x.parentReference.path === null
|
||||
) {
|
||||
throw Error("x.parentReference.path is undefinded or null");
|
||||
}
|
||||
const fullPathOriginal = `${x.parentReference?.path}/${x.name}`;
|
||||
const matchFirstPrefixRes = fullPathOriginal.startsWith(
|
||||
FIRST_COMMON_PREFIX_RAW
|
||||
);
|
||||
const matchSecondPrefixRes = fullPathOriginal.startsWith(
|
||||
SECOND_COMMON_PREFIX_RAW
|
||||
);
|
||||
|
||||
if (matchFirstPrefixRes) {
|
||||
key = fullPathOriginal.substring(FIRST_COMMON_PREFIX_RAW.length + 1);
|
||||
} else if (matchSecondPrefixRes) {
|
||||
key = fullPathOriginal.substring(SECOND_COMMON_PREFIX_RAW.length + 1);
|
||||
} else {
|
||||
throw Error(
|
||||
`we meet file/folder and do not know how to deal with it:\n${constructFromDriveItemToEntityError(
|
||||
x
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const isFolder = "folder" in x;
|
||||
if (isFolder) {
|
||||
key = `${key}/`;
|
||||
}
|
||||
|
||||
const mtimeSvr = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!);
|
||||
const mtimeCli = Date.parse(x?.fileSystemInfo!.lastModifiedDateTime!);
|
||||
return {
|
||||
key: key,
|
||||
keyRaw: key,
|
||||
mtimeSvr: mtimeSvr,
|
||||
mtimeCli: mtimeCli,
|
||||
size: isFolder ? 0 : x.size!,
|
||||
sizeRaw: isFolder ? 0 : x.size!,
|
||||
synthesizedFile: false,
|
||||
// hash: ?? // TODO
|
||||
};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// The client.
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// to adapt to the required interface
|
||||
class MyAuthProvider implements AuthenticationProvider {
|
||||
onedriveFullConfig: OnedriveFullConfig;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
constructor(
|
||||
onedriveFullConfig: OnedriveFullConfig,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
this.onedriveFullConfig = onedriveFullConfig;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
}
|
||||
|
||||
async getAccessToken() {
|
||||
if (
|
||||
this.onedriveFullConfig.accessToken === "" ||
|
||||
this.onedriveFullConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
const currentTs = Date.now();
|
||||
if (this.onedriveFullConfig.accessTokenExpiresAtTime > currentTs) {
|
||||
return this.onedriveFullConfig.accessToken;
|
||||
} else {
|
||||
// use refreshToken to refresh
|
||||
const r = await sendRefreshTokenReq(
|
||||
this.onedriveFullConfig.clientID,
|
||||
this.onedriveFullConfig.authority,
|
||||
this.onedriveFullConfig.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.onedriveFullConfig.accessToken = r2.access_token;
|
||||
this.onedriveFullConfig.refreshToken = r2.refresh_token!;
|
||||
this.onedriveFullConfig.accessTokenExpiresInSeconds = r2.expires_in;
|
||||
this.onedriveFullConfig.accessTokenExpiresAtTime =
|
||||
currentTs + r2.expires_in * 1000 - 60 * 2 * 1000;
|
||||
await this.saveUpdatedConfigFunc();
|
||||
console.info("Onedrive accessToken updated");
|
||||
return this.onedriveFullConfig.accessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* to export the settings in qrcode,
|
||||
* we want to "trim" or "shrink" the settings
|
||||
* @param onedriveFullConfig
|
||||
*/
|
||||
export const getShrinkedSettings = (onedriveFullConfig: OnedriveFullConfig) => {
|
||||
const config = cloneDeep(onedriveFullConfig);
|
||||
config.accessToken = "x";
|
||||
config.accessTokenExpiresInSeconds = 1;
|
||||
config.accessTokenExpiresAtTime = 1;
|
||||
return config;
|
||||
};
|
||||
|
||||
export class FakeFsOnedriveFull extends FakeFs {
|
||||
kind: "onedrivefull";
|
||||
onedriveFullConfig: OnedriveFullConfig;
|
||||
remoteBaseDir: string;
|
||||
vaultFolderExists: boolean;
|
||||
authGetter: MyAuthProvider;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
foldersCreatedBefore: Set<string>;
|
||||
|
||||
constructor(
|
||||
onedriveFullConfig: OnedriveFullConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
super();
|
||||
this.kind = "onedrivefull";
|
||||
this.onedriveFullConfig = onedriveFullConfig;
|
||||
this.remoteBaseDir =
|
||||
this.onedriveFullConfig.remoteBaseDir || vaultName || "";
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
this.authGetter = new MyAuthProvider(
|
||||
onedriveFullConfig,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
this.foldersCreatedBefore = new Set();
|
||||
}
|
||||
|
||||
async _init() {
|
||||
// check token
|
||||
if (
|
||||
this.onedriveFullConfig.accessToken === "" ||
|
||||
this.onedriveFullConfig.refreshToken === ""
|
||||
) {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
// check vault folder
|
||||
// console.info(`checking remote has folder /${this.remoteBaseDir}`);
|
||||
if (this.vaultFolderExists) {
|
||||
// console.info(`already checked, /${this.remoteBaseDir} exist before`)
|
||||
} else {
|
||||
const k = await this._getJson("/drive/root/children");
|
||||
// console.debug(k);
|
||||
this.vaultFolderExists =
|
||||
(k.value as DriveItem[]).filter((x) => x.name === this.remoteBaseDir)
|
||||
.length > 0;
|
||||
if (!this.vaultFolderExists) {
|
||||
console.info(`remote does not have folder /${this.remoteBaseDir}`);
|
||||
await this._postJson("/drive/root/children", {
|
||||
name: `${this.remoteBaseDir}`,
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
});
|
||||
console.info(`remote folder /${this.remoteBaseDir} created`);
|
||||
this.vaultFolderExists = true;
|
||||
} else {
|
||||
// console.info(`remote folder /${this.remoteBaseDir} exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildUrl(pathFragOrig: string) {
|
||||
const API_PREFIX = "https://graph.microsoft.com/v1.0";
|
||||
let theUrl = "";
|
||||
if (
|
||||
pathFragOrig.startsWith("http://") ||
|
||||
pathFragOrig.startsWith("https://")
|
||||
) {
|
||||
theUrl = pathFragOrig;
|
||||
} else {
|
||||
const pathFrag = encodeURI(pathFragOrig);
|
||||
theUrl = `${API_PREFIX}${pathFrag}`;
|
||||
}
|
||||
// we want to support file name with hash #
|
||||
// because every url we construct here do not contain the # symbol
|
||||
// thus it should be safe to directly replace the character
|
||||
theUrl = theUrl.replace(/#/g, "%23");
|
||||
// console.debug(`building url: [${pathFragOrig}] => [${theUrl}]`)
|
||||
return theUrl;
|
||||
}
|
||||
|
||||
async _getJson(pathFragOrig: string) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(`getJson, theUrl=${theUrl}`);
|
||||
return JSON.parse(
|
||||
await request({
|
||||
url: theUrl,
|
||||
method: "GET",
|
||||
contentType: "application/json",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _postJson(pathFragOrig: string, payload: any) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(`postJson, theUrl=${theUrl}`);
|
||||
return JSON.parse(
|
||||
await request({
|
||||
url: theUrl,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _patchJson(pathFragOrig: string, payload: any) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(`patchJson, theUrl=${theUrl}`);
|
||||
return JSON.parse(
|
||||
await request({
|
||||
url: theUrl,
|
||||
method: "PATCH",
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async _deleteJson(pathFragOrig: string) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(`deleteJson, theUrl=${theUrl}`);
|
||||
if (VALID_REQURL) {
|
||||
await requestUrl({
|
||||
url: theUrl,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await fetch(theUrl, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _putArrayBuffer(pathFragOrig: string, payload: ArrayBuffer) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(`putArrayBuffer, theUrl=${theUrl}`);
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
// biome-ignore lint/correctness/noConstantCondition: hard code
|
||||
if (false /*VALID_REQURL*/) {
|
||||
const res = await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
contentType: DEFAULT_CONTENT_TYPE,
|
||||
headers: {
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
return res.json as DriveItem | UploadSession;
|
||||
} else {
|
||||
const res = await fetch(theUrl, {
|
||||
method: "PUT",
|
||||
body: payload,
|
||||
headers: {
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
Authorization: `Bearer ${await this.authGetter.getAccessToken()}`,
|
||||
},
|
||||
});
|
||||
return (await res.json()) as DriveItem | UploadSession;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A specialized function to upload large files by parts
|
||||
* @param pathFragOrig
|
||||
* @param payload
|
||||
* @param rangeMin
|
||||
* @param rangeEnd the end, exclusive
|
||||
* @param size
|
||||
*/
|
||||
async _putUint8ArrayByRange(
|
||||
pathFragOrig: string,
|
||||
payload: Uint8Array,
|
||||
rangeStart: number,
|
||||
rangeEnd: number,
|
||||
size: number
|
||||
) {
|
||||
const theUrl = this._buildUrl(pathFragOrig);
|
||||
console.debug(
|
||||
`putUint8ArrayByRange, theUrl=${theUrl}, range=${rangeStart}-${
|
||||
rangeEnd - 1
|
||||
}, len=${rangeEnd - rangeStart}, size=${size}`
|
||||
);
|
||||
// NO AUTH HEADER here!
|
||||
// TODO:
|
||||
// 20220401: On Android, requestUrl has issue that text becomes base64.
|
||||
// Use fetch everywhere instead!
|
||||
// biome-ignore lint/correctness/noConstantCondition: hard code
|
||||
if (false /*VALID_REQURL*/) {
|
||||
const res = await requestUrl({
|
||||
url: theUrl,
|
||||
method: "PUT",
|
||||
body: bufferToArrayBuffer(payload.subarray(rangeStart, rangeEnd)),
|
||||
contentType: DEFAULT_CONTENT_TYPE,
|
||||
headers: {
|
||||
// no "Content-Length" allowed here
|
||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||
/* "Cache-Control": "no-cache", not allowed here!!! */
|
||||
},
|
||||
});
|
||||
return res.json as DriveItem | UploadSession;
|
||||
} else {
|
||||
const res = await fetch(theUrl, {
|
||||
method: "PUT",
|
||||
body: payload.subarray(rangeStart, rangeEnd),
|
||||
headers: {
|
||||
"Content-Length": `${rangeEnd - rangeStart}`,
|
||||
"Content-Range": `bytes ${rangeStart}-${rangeEnd - 1}/${size}`,
|
||||
"Content-Type": DEFAULT_CONTENT_TYPE,
|
||||
/* "Cache-Control": "no-cache", not allowed here!!! */
|
||||
},
|
||||
});
|
||||
return (await res.json()) as DriveItem | UploadSession;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
async walk(): Promise<Entity[]> {
|
||||
await this._init();
|
||||
|
||||
const NEXT_LINK_KEY = "@odata.nextLink";
|
||||
const DELTA_LINK_KEY = "@odata.deltaLink";
|
||||
|
||||
let res = await this._getJson(`/drive/root:/${this.remoteBaseDir}:/delta`);
|
||||
const driveItems = res.value as DriveItem[];
|
||||
// console.debug(driveItems);
|
||||
|
||||
while (NEXT_LINK_KEY in res) {
|
||||
res = await this._getJson(res[NEXT_LINK_KEY]);
|
||||
driveItems.push(...cloneDeep(res.value as DriveItem[]));
|
||||
}
|
||||
|
||||
// lastly we should have delta link?
|
||||
if (DELTA_LINK_KEY in res) {
|
||||
this.onedriveFullConfig.deltaLink = res[DELTA_LINK_KEY];
|
||||
await this.saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
// unify everything to Entity
|
||||
const unifiedContents = driveItems
|
||||
.map((x) => fromDriveItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.key !== "/");
|
||||
|
||||
return unifiedContents;
|
||||
}
|
||||
|
||||
async walkPartial(): Promise<Entity[]> {
|
||||
await this._init();
|
||||
|
||||
const DELTA_LINK_KEY = "@odata.deltaLink";
|
||||
|
||||
const res = await this._getJson(
|
||||
`/drive/root:/${this.remoteBaseDir}:/delta`
|
||||
);
|
||||
const driveItems = res.value as DriveItem[];
|
||||
// console.debug(driveItems);
|
||||
|
||||
// lastly we should have delta link?
|
||||
if (DELTA_LINK_KEY in res) {
|
||||
this.onedriveFullConfig.deltaLink = res[DELTA_LINK_KEY];
|
||||
await this.saveUpdatedConfigFunc();
|
||||
}
|
||||
|
||||
// unify everything to Entity
|
||||
const unifiedContents = driveItems
|
||||
.map((x) => fromDriveItemToEntity(x, this.remoteBaseDir))
|
||||
.filter((x) => x.key !== "/");
|
||||
|
||||
return unifiedContents;
|
||||
}
|
||||
|
||||
async stat(key: string): Promise<Entity> {
|
||||
await this._init();
|
||||
return await this._statFromRoot(getOnedrivePath(key, this.remoteBaseDir));
|
||||
}
|
||||
|
||||
async _statFromRoot(key: string): Promise<Entity> {
|
||||
// console.info(`remotePath=${remotePath}`);
|
||||
const rsp = await this._getJson(
|
||||
`${key}?$select=cTag,eTag,fileSystemInfo,folder,file,name,parentReference,size`
|
||||
);
|
||||
// console.info(rsp);
|
||||
const driveItem = rsp as DriveItem;
|
||||
const res = fromDriveItemToEntity(driveItem, this.remoteBaseDir);
|
||||
// console.info(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
async mkdir(key: string, mtime?: number, ctime?: number): Promise<Entity> {
|
||||
if (!key.endsWith("/")) {
|
||||
throw Error(`you should not call mkdir on ${key}`);
|
||||
}
|
||||
await this._init();
|
||||
const uploadFolder = getOnedrivePath(key, this.remoteBaseDir);
|
||||
console.debug(`mkdir uploadFolder=${uploadFolder}`);
|
||||
return await this._mkdirFromRoot(uploadFolder, mtime, ctime);
|
||||
}
|
||||
|
||||
async _mkdirFromRoot(
|
||||
key: string,
|
||||
mtime?: number,
|
||||
ctime?: number
|
||||
): Promise<Entity> {
|
||||
// console.debug(`foldersCreatedBefore=${Array.from(this.foldersCreatedBefore)}`);
|
||||
if (this.foldersCreatedBefore.has(key)) {
|
||||
// created, pass
|
||||
// console.debug(`folder ${key} created.`)
|
||||
} else {
|
||||
// https://stackoverflow.com/questions/56479865/creating-nested-folders-in-one-go-onedrive-api
|
||||
// use PATCH to create folder recursively!!!
|
||||
const playload: any = {
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
};
|
||||
const fileSystemInfo: Record<string, string> = {};
|
||||
if (mtime !== undefined && mtime !== 0) {
|
||||
const mtimeStr = new Date(mtime).toISOString();
|
||||
fileSystemInfo["lastModifiedDateTime"] = mtimeStr;
|
||||
}
|
||||
if (ctime !== undefined && ctime !== 0) {
|
||||
const ctimeStr = new Date(ctime).toISOString();
|
||||
fileSystemInfo["createdDateTime"] = ctimeStr;
|
||||
}
|
||||
if (Object.keys(fileSystemInfo).length > 0) {
|
||||
playload["fileSystemInfo"] = fileSystemInfo;
|
||||
}
|
||||
await this._patchJson(key, playload);
|
||||
}
|
||||
const res = await this._statFromRoot(key);
|
||||
return res;
|
||||
}
|
||||
|
||||
async writeFile(
|
||||
key: string,
|
||||
content: ArrayBuffer,
|
||||
mtime: number,
|
||||
ctime: number
|
||||
): Promise<Entity> {
|
||||
if (key.endsWith("/")) {
|
||||
throw Error(`you should not call writeFile on ${key}`);
|
||||
}
|
||||
await this._init();
|
||||
const uploadFile = getOnedrivePath(key, this.remoteBaseDir);
|
||||
console.debug(`uploadFile=${uploadFile}`);
|
||||
return await this._writeFileFromRoot(
|
||||
uploadFile,
|
||||
content,
|
||||
mtime,
|
||||
ctime,
|
||||
key,
|
||||
this.onedriveFullConfig.emptyFile
|
||||
);
|
||||
}
|
||||
|
||||
async _writeFileFromRoot(
|
||||
key: string,
|
||||
content: ArrayBuffer,
|
||||
mtime: number,
|
||||
ctime: number,
|
||||
origKey: string,
|
||||
emptyFile: "skip" | "error"
|
||||
): Promise<Entity> {
|
||||
if (content.byteLength === 0) {
|
||||
if (emptyFile === "error") {
|
||||
throw Error(
|
||||
`${origKey}: Empty file is not allowed in OneDrive, and please write something in it.`
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
key: origKey,
|
||||
keyRaw: origKey,
|
||||
mtimeSvr: mtime,
|
||||
mtimeCli: mtime,
|
||||
size: 0,
|
||||
sizeRaw: 0,
|
||||
synthesizedFile: true,
|
||||
// hash: ?? // TODO
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ctimeStr = new Date(ctime).toISOString();
|
||||
const mtimeStr = new Date(mtime).toISOString();
|
||||
|
||||
// no need to create parent folders firstly, cool!
|
||||
|
||||
// hard code range size
|
||||
const MIN_UNIT = 327680; // bytes in msft doc, about 0.32768 MB
|
||||
const RANGE_SIZE = MIN_UNIT * 20; // about 6.5536 MB
|
||||
const DIRECT_UPLOAD_MAX_SIZE = 1000 * 1000 * 4; // 4 Megabyte
|
||||
|
||||
if (content.byteLength < DIRECT_UPLOAD_MAX_SIZE) {
|
||||
// directly using put!
|
||||
await this._putArrayBuffer(
|
||||
`${key}:/content?${new URLSearchParams({
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
})}`,
|
||||
content
|
||||
);
|
||||
if (mtime !== 0 && ctime !== 0) {
|
||||
await this._patchJson(key, {
|
||||
fileSystemInfo: {
|
||||
lastModifiedDateTime: mtimeStr,
|
||||
createdDateTime: ctimeStr,
|
||||
} as FileSystemInfo,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// upload large files!
|
||||
// ref: https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_createuploadsession?view=odsp-graph-online
|
||||
|
||||
// 1. create uploadSession
|
||||
// uploadFile already starts with /drive/root:/${remoteBaseDir}
|
||||
let playload: any = {
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
},
|
||||
};
|
||||
if (mtime !== 0 && ctime !== 0) {
|
||||
playload = {
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
|
||||
// this is only possible using uploadSession
|
||||
fileSystemInfo: {
|
||||
lastModifiedDateTime: mtimeStr,
|
||||
createdDateTime: ctimeStr,
|
||||
} as FileSystemInfo,
|
||||
},
|
||||
};
|
||||
}
|
||||
const s: UploadSession = await this._postJson(
|
||||
`${key}:/createUploadSession`,
|
||||
playload
|
||||
);
|
||||
const uploadUrl = s.uploadUrl!;
|
||||
console.debug("uploadSession = ");
|
||||
console.debug(s);
|
||||
|
||||
// 2. upload by ranges
|
||||
// convert to uint8
|
||||
const uint8 = new Uint8Array(content);
|
||||
|
||||
// upload the ranges one by one
|
||||
let rangeStart = 0;
|
||||
while (rangeStart < uint8.byteLength) {
|
||||
await this._putUint8ArrayByRange(
|
||||
uploadUrl,
|
||||
uint8,
|
||||
rangeStart,
|
||||
Math.min(rangeStart + RANGE_SIZE, uint8.byteLength),
|
||||
uint8.byteLength
|
||||
);
|
||||
rangeStart += RANGE_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await this._statFromRoot(key);
|
||||
return res;
|
||||
}
|
||||
|
||||
async readFile(key: string): Promise<ArrayBuffer> {
|
||||
await this._init();
|
||||
if (key.endsWith("/")) {
|
||||
throw new Error(`you should not call readFile on folder ${key}`);
|
||||
}
|
||||
const downloadFile = getOnedrivePath(key, this.remoteBaseDir);
|
||||
return await this._readFileFromRoot(downloadFile);
|
||||
}
|
||||
|
||||
async _readFileFromRoot(key: string): Promise<ArrayBuffer> {
|
||||
const rsp = await this._getJson(
|
||||
`${key}?$select=@microsoft.graph.downloadUrl`
|
||||
);
|
||||
const downloadUrl: string = rsp["@microsoft.graph.downloadUrl"];
|
||||
// biome-ignore lint/correctness/noConstantCondition: <explanation>
|
||||
if (false /*VALID_REQURL*/) {
|
||||
const content = (
|
||||
await requestUrl({
|
||||
url: downloadUrl,
|
||||
headers: { "Cache-Control": "no-cache" },
|
||||
})
|
||||
).arrayBuffer;
|
||||
return content;
|
||||
} else {
|
||||
// cannot set no-cache here, will have cors error
|
||||
const content = await (
|
||||
await fetch(downloadUrl, { cache: "no-store" })
|
||||
).arrayBuffer();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
async rename(key1: string, key2: string): Promise<void> {
|
||||
if (key1 === "" || key1 === "/" || key2 === "" || key2 === "/") {
|
||||
return;
|
||||
}
|
||||
const remoteFileName1 = getOnedrivePath(key1, this.remoteBaseDir);
|
||||
const remoteFileName2 = getOnedrivePath(key2, this.remoteBaseDir);
|
||||
await this._init();
|
||||
await this._patchJson(remoteFileName1, {
|
||||
name: remoteFileName2,
|
||||
});
|
||||
}
|
||||
|
||||
async rm(key: string): Promise<void> {
|
||||
if (key === "" || key === "/") {
|
||||
return;
|
||||
}
|
||||
const remoteFileName = getOnedrivePath(key, this.remoteBaseDir);
|
||||
|
||||
await this._init();
|
||||
await this._deleteJson(remoteFileName);
|
||||
}
|
||||
|
||||
async checkConnect(callbackFunc?: any): Promise<boolean> {
|
||||
try {
|
||||
const k = await this.getUserDisplayName();
|
||||
return k !== "<unknown display name>";
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
callbackFunc?.(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getUserDisplayName() {
|
||||
await this._init();
|
||||
const res: User = await this._getJson("/me?$select=displayName");
|
||||
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
|
||||
*/
|
||||
async revokeAuth() {
|
||||
// await this._init();
|
||||
// await this._postJson("/me/revokeSignInSessions", {});
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async getRevokeAddr() {
|
||||
return "https://account.live.com/consent/Manage";
|
||||
}
|
||||
|
||||
allowEmptyFile(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@
|
||||
"protocol_pro_connect_fail": "Something went wrong from response from Remotely Save official website. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
"protocol_pro_connect_succ_revoke": "You've connected as user {{email}}. If you want to disconnect, click this button.",
|
||||
|
||||
"protocol_onedrivefull_connecting": "Connecting to OneDrive...\nPlease DO NOT close this modal.",
|
||||
"protocol_onedrivefull_connect_succ_revoke": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"protocol_onedrivefull_connect_fail": "Something went wrong from response from OneDrive. Maybe you rejected the auth?",
|
||||
"protocol_onedrivefull_connect_unknown": "Do not know how to deal with the callback: {{params}}",
|
||||
|
||||
"protocol_box_connecting": "Connectting",
|
||||
"protocol_box_connect_manualinput_succ": "You've connected",
|
||||
"protocol_box_connect_fail": "Something went wrong from response from Box official website. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
@@ -27,6 +32,18 @@
|
||||
"protocol_koofr_connect_fail": "Something went wrong from response from Koofr website. Maybe the network connection is not good. Maybe you rejected the auth?",
|
||||
"protocol_koofr_connect_succ_revoke": "You've connected. If you want to disconnect, click this button.",
|
||||
|
||||
"modal_onedrivefullauth_shortdesc": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).\nVisit the address in a browser, and follow the steps.\nFinally you should be redirected to Obsidian.",
|
||||
"modal_onedrivefullauth_shortdesc_linux": "It seems that you are using Obsidian on Linux, and you might not be able to jump back here properly. Please consider <a href=\"https://github.com/remotely-save/remotely-save/issues/415\">using</a> the flatpack version of Obsidian, or creating an <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> file</a>.",
|
||||
"modal_onedrivefullauth_copybutton": "Click to copy the auth url",
|
||||
"modal_onedrivefullauth_copynotice": "The auth url is copied to the clipboard!",
|
||||
"modal_onedrivefullrevokeauth_step1": "Step 1: Go to the following address, click the \"Edit\" button for the plugin, then click \"Remove these permissions\" button on the page.",
|
||||
"modal_onedrivefullrevokeauth_step2": "Step 2: Click the button below, to clean the locally-saved login credentials.",
|
||||
"modal_onedrivefullrevokeauth_clean": "Clean Locally-Saved Login Credentials",
|
||||
"modal_onedrivefullrevokeauth_clean_desc": "You need to click the button.",
|
||||
"modal_onedrivefullrevokeauth_clean_button": "Clean",
|
||||
"modal_onedrivefullrevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_onedrivefullrevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
|
||||
"modal_googledriveauth_tutorial": "<p>Please firstly go to the address, then go on the auth flow. In the end, you will see a code, please paste that code here and submit.</p>",
|
||||
"modal_googledriveauth_copybutton": "Click to copy the auth url",
|
||||
"modal_googledriveauth_copynotice": "The auth url is copied to the clipboard!",
|
||||
@@ -112,6 +129,29 @@
|
||||
"modal_proauth_maualinput_notice": "Trying to connect, wait...",
|
||||
"modal_proauth_maualinput_conn_fail": "Failed to connect",
|
||||
|
||||
"settings_onedrivefull": "Remote For Onedrive (for personal) (Full)",
|
||||
"settings_chooseservice_onedrivefull": "OneDrive for personal (Full) (PRO)",
|
||||
"settings_onedrivefull_disclaimer1": "Disclaimer: This app is NOT an official Microsoft / OneDrive product.",
|
||||
"settings_onedrivefull_disclaimer2": "Disclaimer: The information is stored 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 .",
|
||||
"settings_onedrivefull_folder": "We will create and sync inside the folder /{{remoteBaseDir}} on your OneDrive.",
|
||||
"settings_onedrivefull_nobiz": "Currently only OneDrive for personal is supported. OneDrive for Business is NOT supported (yet).",
|
||||
"settings_onedrivefull_pro_desc": "<p><strong>!!It's a PRO feature of Remotely Save! You need a Remotely Save online account for this feature!!</strong>(<a href=\"#settings-pro\">scroll down</a> for more info about PRO account.)</p>",
|
||||
"settings_onedrivefull_notshowuphint": "OneDrive for personal (Full) Settings Not Available",
|
||||
"settings_onedrivefull_notshowuphint_desc": "OneDrive for personal (Full) settings are not available, because you haven't subscribed to the PRO feature in your Remotely Save account.",
|
||||
"settings_onedrivefull_notshowuphint_view_pro": "View PRO Settings",
|
||||
"settings_onedrivefull_revoke": "Revoke Auth",
|
||||
"settings_onedrivefull_revoke_desc": "You've connected as user {{username}}. If you want to disconnect, click this button.",
|
||||
"settings_onedrivefull_revoke_button": "Revoke Auth",
|
||||
"settings_onedrivefull_auth": "Auth",
|
||||
"settings_onedrivefull_auth_desc": "Auth.",
|
||||
"settings_onedrivefull_auth_button": "Auth",
|
||||
"settings_onedrivefull_connect_succ": "Great! We can connect to OneDrive!",
|
||||
"settings_onedrivefull_connect_fail": "We cannot connect to OneDrive.",
|
||||
"settings_onedrivefull_emptyfile": "Empty File Handling",
|
||||
"settings_onedrivefull_emptyfile_desc": "OneDrive doesn't allow uploading empty file (even in its official website). Do you want to show up errors or silently skip the empty files?",
|
||||
"settings_onedrivefull_emptyfile_skip": "Skip",
|
||||
"settings_onedrivefull_emptyfile_error": "Error and abort",
|
||||
|
||||
"settings_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_googledrive_disclaimer1": "Disclaimer: This app is NOT an official Google product. The app just uses Google Drive's public api.",
|
||||
@@ -230,6 +270,7 @@
|
||||
"settings_azureblobstorage_connect_succ": "Great! We can connect to Azure Blob Storage!",
|
||||
"settings_azureblobstorage_connect_fail": "We cannot connect to Azure Blob Storage.",
|
||||
|
||||
"settings_export_onedrivefull_button": "Export Onedrive (Full) Part",
|
||||
"settings_export_googledrive_button": "Export Google Drive Part",
|
||||
"settings_export_box_button": "Export Box Part",
|
||||
"settings_export_pcloud_button": "Export pCloud Part",
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
"protocol_pro_connect_fail": "Remotely Save 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
|
||||
"protocol_pro_connect_succ_revoke": "您已连接上账号 {{email}}。如果要取消连接,请点击此按钮。",
|
||||
|
||||
"protocol_onedrivefull_connecting": "正在连接 OneDrive……\n请不要关闭此弹窗。",
|
||||
"protocol_onedrivefull_connect_succ_revoke": "您已作为用户 {{username}} 连接上了。如果您想取消连接,请点击此按钮。",
|
||||
"protocol_onedrivefull_connect_fail": "OneDrive 的回调请求有点异常。您是否拒绝了鉴权?",
|
||||
"protocol_onedrivefull_connect_unknown": "不知道如何处理此 callback:{{params}}",
|
||||
|
||||
"protocol_box_connecting": "正在连接",
|
||||
"protocol_box_connect_manualinput_succ": "连接成功",
|
||||
"protocol_box_connect_fail": "Box 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
|
||||
@@ -27,6 +32,18 @@
|
||||
"protocol_koofr_connect_fail": "Yandex Disk 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
|
||||
"protocol_koofr_connect_succ_revoke": "您已连接上账号。如果要取消连接,请点击此按钮。",
|
||||
|
||||
"modal_onedrivefullauth_shortdesc": "现在只支持个人版 OneDrive,(暂)不支持企业版。\n在浏览器中访问以下地址,然后按照网页提示操作。\n到了最后,您应该会被自动重定向回来 Obsidian。",
|
||||
"modal_onedrivefullauth_shortdesc_linux": "您正在用 Linux,有可能无法跳转回来。请考虑<a href=\"https://github.com/remotely-save/remotely-save/issues/415\">使用</a> flatpack 版本的 Obsidian,或创建 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> 文件</a>。",
|
||||
"modal_onedrivefullauth_copybutton": "点击此按钮从而复制鉴权 url",
|
||||
"modal_onedrivefullauth_copynotice": "鉴权 url 已复制到剪贴板!",
|
||||
"modal_onedrivefullrevokeauth_step1": "第 1 步:用浏览器打开以下地址,点击本插件对应的“Edit”按钮,点击“Remove these permissions”按钮。",
|
||||
"modal_onedrivefullrevokeauth_step2": "第 2 步:点击以下按钮,清理本地保存的登录信息。",
|
||||
"modal_onedrivefullrevokeauth_clean": "清理本地保存的登录信息",
|
||||
"modal_onedrivefullrevokeauth_clean_desc": "您需要点击“清理”按钮。",
|
||||
"modal_onedrivefullrevokeauth_clean_button": "清理",
|
||||
"modal_onedrivefullrevokeauth_clean_notice": "已清理!",
|
||||
"modal_onedrivefullrevokeauth_clean_fail": "鉴权途中出错了。",
|
||||
|
||||
"modal_googledriveauth_tutorial": "<p>请访问此网址,然后会进入授权流程。最后,您会看到一个码,请复制粘贴到这里然后提交。</p>",
|
||||
"modal_googledriveauth_copybutton": "点击以复制网址",
|
||||
"modal_googledriveauth_copynotice": "网址已复制!",
|
||||
@@ -123,6 +140,25 @@
|
||||
"modal_proauth_maualinput_notice": "正在连接,请稍候......",
|
||||
"modal_proauth_maualinput_conn_fail": "连接失败",
|
||||
|
||||
"settings_onedrivefull": "Onedrive(个人版)(Full)设置",
|
||||
"settings_chooseservice_onedrivefull": "OneDrive(个人版)(Full)",
|
||||
"settings_onedrivefull_disclaimer1": "声明:此插件不是微软或 OneDrive 的官方产品。",
|
||||
"settings_onedrivefull_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现了 OneDrive 有不符合预期的访问,请立刻从 https://microsoft.com/consent 删除记录于此插件的连接鉴权。",
|
||||
"settings_onedrivefull_folder": "我们会在您的 OneDrive 上创建此文件夹并在里面同步:/{{remoteBaseDir}}。",
|
||||
"settings_onedrivefull_nobiz": "现在只支持个人版 OneDrive。(暂时)并不支持企业版 OneDrive。",
|
||||
"settings_onedrivefull_revoke": "撤回鉴权",
|
||||
"settings_onedrivefull_revoke_desc": "您现在使用了用户名 {{username}} 来连接。如果想取消连接,请点击此按钮。",
|
||||
"settings_onedrivefull_revoke_button": "撤回鉴权",
|
||||
"settings_onedrivefull_auth": "鉴权",
|
||||
"settings_onedrivefull_auth_desc": "鉴权。",
|
||||
"settings_onedrivefull_auth_button": "鉴权",
|
||||
"settings_onedrivefull_connect_succ": "很好!我们可连接上 OneDrive!",
|
||||
"settings_onedrivefull_connect_fail": "我们未能连接上 OneDrive。",
|
||||
"settings_onedrivefull_emptyfile": "空文件处理",
|
||||
"settings_onedrivefull_emptyfile_desc": "OneDrive 不允许上传空文件(即使官网也是不允许的)。那么你想跳过空文件还是返回错误?",
|
||||
"settings_onedrivefull_emptyfile_skip": "跳过",
|
||||
"settings_onedrivefull_emptyfile_error": "返回错误和中断",
|
||||
|
||||
"settings_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_googledrive_disclaimer1": "声明:本插件不是 Google 的官方产品。只是用到了它的公开 API。",
|
||||
@@ -241,6 +277,7 @@
|
||||
"settings_azureblobstorage_connect_succ": "很好!我们可连接上 Azure Blob Storage!",
|
||||
"settings_azureblobstorage_connect_fail": "我们未能连接上 Azure Blob Storage。",
|
||||
|
||||
"settings_export_onedrivefull_button": "导出 OneDrive (Full) 部分",
|
||||
"settings_export_googledrive_button": "导出 Google Drive 部分",
|
||||
"settings_export_box_button": "导出 Box 部分",
|
||||
"settings_export_pcloud_button": "导出 pCloud 部分",
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
"protocol_pro_connect_fail": "Remotely Save 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
|
||||
"protocol_pro_connect_succ_revoke": "您已連線上賬號 {{email}}。如果要取消連線,請點選此按鈕。",
|
||||
|
||||
"protocol_onedrivefull_connecting": "正在連線 OneDrive……\n請不要關閉此彈窗。",
|
||||
"protocol_onedrivefull_connect_succ_revoke": "您已作為使用者 {{username}} 連線上了。如果您想取消連線,請點選此按鈕。",
|
||||
"protocol_onedrivefull_connect_fail": "OneDrive 的回撥請求有點異常。您是否拒絕了鑑權?",
|
||||
"protocol_onedrivefull_connect_unknown": "不知道如何處理此 callback:{{params}}",
|
||||
|
||||
"protocol_box_connecting": "正在連線",
|
||||
"protocol_box_connect_manualinput_succ": "連線成功",
|
||||
"protocol_box_connect_fail": "Box 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
|
||||
@@ -27,6 +32,18 @@
|
||||
"protocol_koofr_connect_fail": "Yandex Disk 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
|
||||
"protocol_koofr_connect_succ_revoke": "您已連線上賬號。如果要取消連線,請點選此按鈕。",
|
||||
|
||||
"modal_onedrivefullauth_shortdesc": "現在只支援個人版 OneDrive,(暫)不支援企業版。\n在瀏覽器中訪問以下地址,然後按照網頁提示操作。\n到了最後,您應該會被自動重定向回來 Obsidian。",
|
||||
"modal_onedrivefullauth_shortdesc_linux": "您正在用 Linux,有可能無法跳轉回來。請考慮<a href=\"https://github.com/remotely-save/remotely-save/issues/415\">使用</a> flatpack 版本的 Obsidian,或建立 <a href=\"https://github.com/remotely-save/remotely-save/blob/master/docs/linux.md\"><code>obsidian.desktop</code> 檔案</a>。",
|
||||
"modal_onedrivefullauth_copybutton": "點選此按鈕從而複製鑑權 url",
|
||||
"modal_onedrivefullauth_copynotice": "鑑權 url 已複製到剪貼簿!",
|
||||
"modal_onedrivefullrevokeauth_step1": "第 1 步:用瀏覽器開啟以下地址,點選本外掛對應的“Edit”按鈕,點選“Remove these permissions”按鈕。",
|
||||
"modal_onedrivefullrevokeauth_step2": "第 2 步:點選以下按鈕,清理本地儲存的登入資訊。",
|
||||
"modal_onedrivefullrevokeauth_clean": "清理本地儲存的登入資訊",
|
||||
"modal_onedrivefullrevokeauth_clean_desc": "您需要點選“清理”按鈕。",
|
||||
"modal_onedrivefullrevokeauth_clean_button": "清理",
|
||||
"modal_onedrivefullrevokeauth_clean_notice": "已清理!",
|
||||
"modal_onedrivefullrevokeauth_clean_fail": "鑑權途中出錯了。",
|
||||
|
||||
"modal_googledriveauth_tutorial": "<p>請訪問此網址,然後會進入授權流程。最後,您會看到一個碼,請複製貼上到這裡然後提交。</p>",
|
||||
"modal_googledriveauth_copybutton": "點選以複製網址",
|
||||
"modal_googledriveauth_copynotice": "網址已複製!",
|
||||
@@ -123,6 +140,25 @@
|
||||
"modal_proauth_maualinput_notice": "正在連線,請稍候......",
|
||||
"modal_proauth_maualinput_conn_fail": "連線失敗",
|
||||
|
||||
"settings_onedrivefull": "Onedrive(個人版)(Full)設定",
|
||||
"settings_chooseservice_onedrivefull": "OneDrive(個人版)(Full)",
|
||||
"settings_onedrivefull_disclaimer1": "宣告:此外掛不是微軟或 OneDrive 的官方產品。",
|
||||
"settings_onedrivefull_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現了 OneDrive 有不符合預期的訪問,請立刻從 https://microsoft.com/consent 刪除記錄於此外掛的連線鑑權。",
|
||||
"settings_onedrivefull_folder": "我們會在您的 OneDrive 上建立此資料夾並在裡面同步:/{{remoteBaseDir}}。",
|
||||
"settings_onedrivefull_nobiz": "現在只支援個人版 OneDrive。(暫時)並不支援企業版 OneDrive。",
|
||||
"settings_onedrivefull_revoke": "撤回鑑權",
|
||||
"settings_onedrivefull_revoke_desc": "您現在使用了使用者名稱 {{username}} 來連線。如果想取消連線,請點選此按鈕。",
|
||||
"settings_onedrivefull_revoke_button": "撤回鑑權",
|
||||
"settings_onedrivefull_auth": "鑑權",
|
||||
"settings_onedrivefull_auth_desc": "鑑權。",
|
||||
"settings_onedrivefull_auth_button": "鑑權",
|
||||
"settings_onedrivefull_connect_succ": "很好!我們可連線上 OneDrive!",
|
||||
"settings_onedrivefull_connect_fail": "我們未能連線上 OneDrive。",
|
||||
"settings_onedrivefull_emptyfile": "空檔案處理",
|
||||
"settings_onedrivefull_emptyfile_desc": "OneDrive 不允許上傳空檔案(即使官網也是不允許的)。那麼你想跳過空檔案還是返回錯誤?",
|
||||
"settings_onedrivefull_emptyfile_skip": "跳過",
|
||||
"settings_onedrivefull_emptyfile_error": "返回錯誤和中斷",
|
||||
|
||||
"settings_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_chooseservice_googledrive": "Google Drive (PRO) (beta)",
|
||||
"settings_googledrive_disclaimer1": "宣告:本外掛不是 Google 的官方產品。只是用到了它的公開 API。",
|
||||
@@ -241,6 +277,7 @@
|
||||
"settings_azureblobstorage_connect_succ": "很好!我們可連線上 Azure Blob Storage!",
|
||||
"settings_azureblobstorage_connect_fail": "我們未能連線上 Azure Blob Storage。",
|
||||
|
||||
"settings_export_onedrivefull_button": "匯出 OneDrive (Full) 部分",
|
||||
"settings_export_googledrive_button": "匯出 Google Drive 部分",
|
||||
"settings_export_box_button": "匯出 Box 部分",
|
||||
"settings_export_pcloud_button": "匯出 pCloud 部分",
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { type App, Modal, Notice, Platform, Setting } from "obsidian";
|
||||
import { getClient } from "../../src/fsGetter";
|
||||
import type { TransItemType } from "../../src/i18n";
|
||||
import type RemotelySavePlugin from "../../src/main";
|
||||
import { stringToFragment } from "../../src/misc";
|
||||
import { ChangeRemoteBaseDirModal } from "../../src/settings";
|
||||
import {
|
||||
DEFAULT_ONEDRIVEFULL_CONFIG,
|
||||
getAuthUrlAndVerifier as getAuthUrlAndVerifierOnedriveFull,
|
||||
sendRefreshTokenReq as sendRefreshTokenReqOnedriveFull,
|
||||
} from "./fsOnedriveFull";
|
||||
|
||||
export class OnedriveFullAuthModal 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() {
|
||||
const { contentEl } = this;
|
||||
|
||||
const { authUrl, verifier } = await getAuthUrlAndVerifierOnedriveFull(
|
||||
this.plugin.settings.onedrivefull.clientID,
|
||||
this.plugin.settings.onedrivefull.authority
|
||||
);
|
||||
this.plugin.oauth2Info.verifier = verifier;
|
||||
|
||||
const t = (x: TransItemType, vars?: any) => {
|
||||
return this.plugin.i18n.t(x, vars);
|
||||
};
|
||||
|
||||
t("modal_onedrivefullauth_shortdesc")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
contentEl.createEl("p", {
|
||||
text: val,
|
||||
});
|
||||
});
|
||||
if (Platform.isLinux) {
|
||||
t("modal_onedrivefullauth_shortdesc_linux")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
contentEl.createEl("p", {
|
||||
text: stringToFragment(val),
|
||||
});
|
||||
});
|
||||
}
|
||||
const div2 = contentEl.createDiv();
|
||||
div2.createEl(
|
||||
"button",
|
||||
{
|
||||
text: t("modal_onedrivefullauth_copybutton"),
|
||||
},
|
||||
(el) => {
|
||||
el.onclick = async () => {
|
||||
await navigator.clipboard.writeText(authUrl);
|
||||
new Notice(t("modal_onedrivefullauth_copynotice"));
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: authUrl,
|
||||
text: authUrl,
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
export class OnedriveFullRevokeAuthModal extends Modal {
|
||||
readonly plugin: RemotelySavePlugin;
|
||||
readonly authDiv: HTMLDivElement;
|
||||
readonly revokeAuthDiv: HTMLDivElement;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
authDiv: HTMLDivElement,
|
||||
revokeAuthDiv: HTMLDivElement
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.authDiv = authDiv;
|
||||
this.revokeAuthDiv = revokeAuthDiv;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
const t = (x: TransItemType, vars?: any) => {
|
||||
return this.plugin.i18n.t(x, vars);
|
||||
};
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: t("modal_onedrivefullrevokeauth_step1"),
|
||||
});
|
||||
const consentUrl = "https://microsoft.com/consent";
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: consentUrl,
|
||||
text: consentUrl,
|
||||
});
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: t("modal_onedrivefullrevokeauth_step2"),
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName(t("modal_onedrivefullrevokeauth_clean"))
|
||||
.setDesc(t("modal_onedrivefullrevokeauth_clean_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("modal_onedrivefullrevokeauth_clean_button"));
|
||||
button.onClick(async () => {
|
||||
try {
|
||||
this.plugin.settings.onedrivefull = JSON.parse(
|
||||
JSON.stringify(DEFAULT_ONEDRIVEFULL_CONFIG)
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
this.authDiv.toggleClass(
|
||||
"onedrivefull-auth-button-hide",
|
||||
this.plugin.settings.onedrivefull.username !== ""
|
||||
);
|
||||
this.revokeAuthDiv.toggleClass(
|
||||
"onedrivefull-revoke-auth-button-hide",
|
||||
this.plugin.settings.onedrivefull.username === ""
|
||||
);
|
||||
new Notice(t("modal_onedrivefullrevokeauth_clean_notice"));
|
||||
this.close();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice(t("modal_onedrivefullrevokeauth_clean_fail"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
export const generateOnedriveFullSettingsPart = (
|
||||
containerEl: HTMLElement,
|
||||
t: (x: TransItemType, vars?: any) => string,
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
const onedriveFullDiv = containerEl.createEl("div", {
|
||||
cls: "onedrivefull-hide",
|
||||
});
|
||||
onedriveFullDiv.toggleClass(
|
||||
"onedrivefull-hide",
|
||||
plugin.settings.serviceType !== "onedrivefull"
|
||||
);
|
||||
onedriveFullDiv.createEl("h2", { text: t("settings_onedrivefull") });
|
||||
|
||||
const onedriveFullLongDescDiv = onedriveFullDiv.createEl("div", {
|
||||
cls: "settings-long-desc",
|
||||
});
|
||||
for (const c of [
|
||||
t("settings_onedrivefull_disclaimer1"),
|
||||
t("settings_onedrivefull_disclaimer2"),
|
||||
]) {
|
||||
onedriveFullLongDescDiv.createEl("p", {
|
||||
text: c,
|
||||
cls: "onedrivefull-disclaimer",
|
||||
});
|
||||
}
|
||||
|
||||
onedriveFullLongDescDiv.createEl("p", {
|
||||
text: t("settings_onedrivefull_folder", {
|
||||
remoteBaseDir:
|
||||
plugin.settings.onedrivefull.remoteBaseDir || app.vault.getName(),
|
||||
}),
|
||||
});
|
||||
|
||||
onedriveFullLongDescDiv.createEl("p", {
|
||||
text: t("settings_onedrivefull_nobiz"),
|
||||
});
|
||||
|
||||
onedriveFullLongDescDiv.createDiv({
|
||||
text: stringToFragment(t("settings_onedrivefull_pro_desc")),
|
||||
cls: "onedrivefull-disclaimer",
|
||||
});
|
||||
|
||||
const onedriveFullNotShowUpHintSetting = new Setting(onedriveFullDiv)
|
||||
.setName(t("settings_onedrivefull_notshowuphint"))
|
||||
.setDesc(t("settings_onedrivefull_notshowuphint_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_onedrivefull_notshowuphint_view_pro"));
|
||||
button.onClick(async () => {
|
||||
window.location.href = "#settings-pro";
|
||||
});
|
||||
});
|
||||
|
||||
const onedriveFullAllowedToUsedDiv = onedriveFullDiv.createDiv();
|
||||
// if pro enabled, show up; otherwise hide.
|
||||
const allowOnedriveFull =
|
||||
plugin.settings.pro?.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-onedrive_full"
|
||||
).length === 1;
|
||||
console.debug(`allow to show up onedriveFull settings? ${allowOnedriveFull}`);
|
||||
if (allowOnedriveFull) {
|
||||
onedriveFullAllowedToUsedDiv.removeClass("onedrivefull-allow-to-use-hide");
|
||||
onedriveFullNotShowUpHintSetting.settingEl.addClass(
|
||||
"onedrivefull-allow-to-use-hide"
|
||||
);
|
||||
} else {
|
||||
onedriveFullAllowedToUsedDiv.addClass("onedrivefull-allow-to-use-hide");
|
||||
onedriveFullNotShowUpHintSetting.settingEl.removeClass(
|
||||
"onedrivefull-allow-to-use-hide"
|
||||
);
|
||||
}
|
||||
|
||||
const onedriveFullSelectAuthDiv = onedriveFullAllowedToUsedDiv.createDiv();
|
||||
const onedriveFullAuthDiv = onedriveFullSelectAuthDiv.createDiv({
|
||||
cls: "onedrivefull-auth-button-hide settings-auth-related",
|
||||
});
|
||||
const onedriveFullRevokeAuthDiv = onedriveFullSelectAuthDiv.createDiv({
|
||||
cls: "onedrivefull-revoke-auth-button-hide settings-auth-related",
|
||||
});
|
||||
|
||||
const onedriveFullRevokeAuthSetting = new Setting(onedriveFullRevokeAuthDiv)
|
||||
.setName(t("settings_onedrivefull_revoke"))
|
||||
.setDesc(t("settings_onedrivefull_revoke_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_onedrivefull_revoke_button"));
|
||||
button.onClick(async () => {
|
||||
new OnedriveFullRevokeAuthModal(
|
||||
app,
|
||||
plugin,
|
||||
onedriveFullAuthDiv,
|
||||
onedriveFullRevokeAuthDiv
|
||||
).open();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(onedriveFullAuthDiv)
|
||||
.setName(t("settings_onedrivefull_auth"))
|
||||
.setDesc(t("settings_onedrivefull_auth_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_onedrivefull_auth_button"));
|
||||
button.onClick(async () => {
|
||||
const modal = new OnedriveFullAuthModal(
|
||||
app,
|
||||
plugin,
|
||||
onedriveFullAuthDiv,
|
||||
onedriveFullRevokeAuthDiv,
|
||||
onedriveFullRevokeAuthSetting
|
||||
);
|
||||
plugin.oauth2Info.helperModal = modal;
|
||||
plugin.oauth2Info.authDiv = onedriveFullAuthDiv;
|
||||
plugin.oauth2Info.revokeDiv = onedriveFullRevokeAuthDiv;
|
||||
plugin.oauth2Info.revokeAuthSetting = onedriveFullRevokeAuthSetting;
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
|
||||
onedriveFullAuthDiv.toggleClass(
|
||||
"onedrivefull-auth-button-hide",
|
||||
plugin.settings.onedrivefull.refreshToken !== ""
|
||||
);
|
||||
onedriveFullRevokeAuthDiv.toggleClass(
|
||||
"onedrivefull-revoke-auth-button-hide",
|
||||
plugin.settings.onedrivefull.refreshToken === ""
|
||||
);
|
||||
|
||||
let newonedriveFullRemoteBaseDir =
|
||||
plugin.settings.onedrivefull.remoteBaseDir || "";
|
||||
new Setting(onedriveFullAllowedToUsedDiv)
|
||||
.setName(t("settings_remotebasedir"))
|
||||
.setDesc(t("settings_remotebasedir_desc"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(app.vault.getName())
|
||||
.setValue(newonedriveFullRemoteBaseDir)
|
||||
.onChange((value) => {
|
||||
newonedriveFullRemoteBaseDir = value.trim();
|
||||
})
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("confirm"));
|
||||
button.onClick(() => {
|
||||
new ChangeRemoteBaseDirModal(
|
||||
app,
|
||||
plugin,
|
||||
newonedriveFullRemoteBaseDir,
|
||||
"onedrivefull"
|
||||
).open();
|
||||
});
|
||||
});
|
||||
new Setting(onedriveFullAllowedToUsedDiv)
|
||||
.setName(t("settings_checkonnectivity"))
|
||||
.setDesc(t("settings_checkonnectivity_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_checkonnectivity_button"));
|
||||
button.onClick(async () => {
|
||||
new Notice(t("settings_checkonnectivity_checking"));
|
||||
const client = getClient(plugin.settings, app.vault.getName(), () =>
|
||||
plugin.saveSettings()
|
||||
);
|
||||
const errors = { msg: "" };
|
||||
const res = await client.checkConnect((err: any) => {
|
||||
errors.msg = `${err}`;
|
||||
});
|
||||
if (res) {
|
||||
new Notice(t("settings_onedrivefull_connect_succ"));
|
||||
} else {
|
||||
new Notice(t("settings_onedrivefull_connect_fail"));
|
||||
new Notice(errors.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
onedriveFullDiv: onedriveFullDiv,
|
||||
onedriveFullAllowedToUsedDiv: onedriveFullAllowedToUsedDiv,
|
||||
onedriveFullNotShowUpHintSetting: onedriveFullNotShowUpHintSetting,
|
||||
};
|
||||
};
|
||||
@@ -248,6 +248,8 @@ export const generateProSettingsPart = (
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined,
|
||||
onedriveFullAllowedToUsedDiv: HTMLDivElement,
|
||||
onedriveFullNotShowUpHintSetting: Setting,
|
||||
googleDriveAllowedToUsedDiv: HTMLDivElement,
|
||||
googleDriveNotShowUpHintSetting: Setting,
|
||||
boxAllowedToUsedDiv: HTMLDivElement,
|
||||
@@ -306,6 +308,27 @@ export const generateProSettingsPart = (
|
||||
)
|
||||
);
|
||||
|
||||
const allowOnedriveFull =
|
||||
plugin.settings.pro?.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-box"
|
||||
).length === 1;
|
||||
console.debug(
|
||||
`allow to show up OnedriveFull settings? ${allowOnedriveFull}`
|
||||
);
|
||||
if (allowOnedriveFull) {
|
||||
onedriveFullAllowedToUsedDiv.removeClass(
|
||||
"onedrivefull-allow-to-use-hide"
|
||||
);
|
||||
onedriveFullNotShowUpHintSetting.settingEl.addClass(
|
||||
"onedrivefull-allow-to-use-hide"
|
||||
);
|
||||
} else {
|
||||
onedriveFullAllowedToUsedDiv.addClass("onedrivefull-allow-to-use-hide");
|
||||
onedriveFullNotShowUpHintSetting.settingEl.removeClass(
|
||||
"onedrivefull-allow-to-use-hide"
|
||||
);
|
||||
}
|
||||
|
||||
const allowGoogleDrive =
|
||||
plugin.settings.pro?.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-google_drive"
|
||||
|
||||
Reference in New Issue
Block a user