box
This commit is contained in:
parent
1e4d729eb7
commit
d5c2f726f9
@ -5,3 +5,5 @@ REMOTELYSAVE_WEBSITE=http://127.0.0.1:46683
|
||||
REMOTELYSAVE_CLIENT_ID=cli-xxx
|
||||
GOOGLEDRIVE_CLIENT_ID=xxx.apps.googleusercontent.com
|
||||
GOOGLEDRIVE_CLIENT_SECRET=GOCSPX-sss
|
||||
BOX_CLIENT_ID=
|
||||
BOX_CLIENT_SECRET=
|
||||
|
||||
2
.github/workflows/auto-build.yml
vendored
2
.github/workflows/auto-build.yml
vendored
@ -23,6 +23,8 @@ jobs:
|
||||
REMOTELYSAVE_CLIENT_ID: ${{secrets.REMOTELYSAVE_CLIENT_ID}}
|
||||
GOOGLEDRIVE_CLIENT_ID: ${{secrets.GOOGLEDRIVE_CLIENT_ID}}
|
||||
GOOGLEDRIVE_CLIENT_SECRET: ${{secrets.GOOGLEDRIVE_CLIENT_SECRET}}
|
||||
BOX_CLIENT_ID: ${{secrets.BOX_CLIENT_ID}}
|
||||
BOX_CLIENT_SECRET: ${{secrets.BOX_CLIENT_SECRET}}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -27,6 +27,8 @@ jobs:
|
||||
REMOTELYSAVE_CLIENT_ID: ${{secrets.REMOTELYSAVE_CLIENT_ID}}
|
||||
GOOGLEDRIVE_CLIENT_ID: ${{secrets.GOOGLEDRIVE_CLIENT_ID}}
|
||||
GOOGLEDRIVE_CLIENT_SECRET: ${{secrets.GOOGLEDRIVE_CLIENT_SECRET}}
|
||||
BOX_CLIENT_ID: ${{secrets.BOX_CLIENT_ID}}
|
||||
BOX_CLIENT_SECRET: ${{secrets.BOX_CLIENT_SECRET}}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
|
||||
@ -22,6 +22,8 @@ const DEFAULT_REMOTELYSAVE_CLIENT_ID = process.env.REMOTELYSAVE_CLIENT_ID || "";
|
||||
const DEFAULT_GOOGLEDRIVE_CLIENT_ID = process.env.GOOGLEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET =
|
||||
process.env.GOOGLEDRIVE_CLIENT_SECRET || "";
|
||||
const DEFAULT_BOX_CLIENT_ID = process.env.BOX_CLIENT_ID || "";
|
||||
const DEFAULT_BOX_CLIENT_SECRET = process.env.BOX_CLIENT_SECRET || "";
|
||||
|
||||
esbuild
|
||||
.context({
|
||||
@ -61,6 +63,8 @@ esbuild
|
||||
"process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`,
|
||||
"process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`,
|
||||
global: "window",
|
||||
"process.env.NODE_DEBUG": `undefined`, // ugly fix
|
||||
"process.env.DEBUG": `undefined`, // ugly fix
|
||||
|
||||
@ -71,6 +71,7 @@
|
||||
"aggregate-error": "^5.0.0",
|
||||
"assert": "^2.1.0",
|
||||
"aws-crt": "^1.21.2",
|
||||
"box-typescript-sdk-gen": "^1.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"dropbox": "^10.34.0",
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { base64url } from "rfc4648";
|
||||
import {
|
||||
OAUTH2_FORCE_EXPIRE_MILLISECONDS,
|
||||
type RemotelySavePluginSettings,
|
||||
@ -12,6 +11,7 @@ import {
|
||||
PRO_WEBSITE,
|
||||
type ProConfig,
|
||||
} from "./baseTypesPro";
|
||||
import { codeVerifier2CodeChallenge } from "./oauth2";
|
||||
|
||||
const site = PRO_WEBSITE;
|
||||
console.debug(`remotelysave official website: ${site}`);
|
||||
@ -25,31 +25,6 @@ export const DEFAULT_PRO_CONFIG: ProConfig = {
|
||||
email: "",
|
||||
};
|
||||
|
||||
/**
|
||||
* https://datatracker.ietf.org/doc/html/rfc7636
|
||||
* dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
|
||||
* => E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
|
||||
* @param x
|
||||
* @returns BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
|
||||
*/
|
||||
async function codeVerifier2CodeChallenge(x: string) {
|
||||
if (x === undefined || x === "") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return base64url.stringify(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(x))
|
||||
),
|
||||
{
|
||||
pad: false,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const generateAuthUrlAndCodeVerifierChallenge = async (
|
||||
hasCallback: boolean
|
||||
) => {
|
||||
@ -241,7 +216,6 @@ export const getAndSaveProEmail = async (
|
||||
* @returns
|
||||
*/
|
||||
export const checkProRunnableAndFixInplace = async (
|
||||
featuresToCheck: PRO_FEATURE_TYPE[],
|
||||
config: RemotelySavePluginSettings,
|
||||
pluginVersion: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
@ -276,48 +250,59 @@ export const checkProRunnableAndFixInplace = async (
|
||||
|
||||
const errorMsgs = [];
|
||||
|
||||
// check for the features
|
||||
if (featuresToCheck.contains("feature-smart_conflict")) {
|
||||
if (config.conflictAction === "smart_conflict") {
|
||||
if (
|
||||
config.pro.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-smart_conflict"
|
||||
).length === 1
|
||||
) {
|
||||
// good to go
|
||||
} else {
|
||||
errorMsgs.push(
|
||||
`You're trying to use "smart conflict" PRO feature but you haven't subscribe to it.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// check for smart_conflict
|
||||
if (config.conflictAction === "smart_conflict") {
|
||||
if (
|
||||
config.pro.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-smart_conflict"
|
||||
).length === 1
|
||||
) {
|
||||
// good to go
|
||||
} else {
|
||||
errorMsgs.push(
|
||||
`You're trying to use "smart conflict" PRO feature but you haven't subscribe to it.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// good to go
|
||||
}
|
||||
|
||||
if (featuresToCheck.contains("feature-google_drive")) {
|
||||
console.debug(
|
||||
`checking "feature-google_drive", serviceType=${config.serviceType}`
|
||||
);
|
||||
console.debug(
|
||||
`enabledProFeatures=${JSON.stringify(config.pro.enabledProFeatures)}`
|
||||
);
|
||||
|
||||
if (config.serviceType === "googledrive") {
|
||||
if (
|
||||
config.pro.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-google_drive"
|
||||
).length === 1
|
||||
) {
|
||||
// good to go
|
||||
} else {
|
||||
errorMsgs.push(
|
||||
`You're trying to use "sync with Google Drive" PRO feature but you haven't subscribe to it.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// check for google_drive
|
||||
console.debug(
|
||||
`checking "feature-google_drive", serviceType=${config.serviceType}`
|
||||
);
|
||||
if (config.serviceType === "googledrive") {
|
||||
if (
|
||||
config.pro.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-google_drive"
|
||||
).length === 1
|
||||
) {
|
||||
// good to go
|
||||
} else {
|
||||
errorMsgs.push(
|
||||
`You're trying to use "sync with Google Drive" PRO feature but you haven't subscribe to it.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// good to go
|
||||
}
|
||||
|
||||
// check for box
|
||||
console.debug(`checking "feature-box", serviceType=${config.serviceType}`);
|
||||
if (config.serviceType === "box") {
|
||||
if (
|
||||
config.pro.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-box"
|
||||
).length === 1
|
||||
) {
|
||||
// good to go
|
||||
} else {
|
||||
errorMsgs.push(
|
||||
`You're trying to use "sync with Box" PRO feature but you haven't subscribe to it.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// good to go
|
||||
}
|
||||
|
||||
if (errorMsgs.length !== 0) {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export const MERGABLE_SIZE = 1000 * 1000; // 1 MB
|
||||
///////////////////////////////////////////////////////////
|
||||
// PRO
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
export const COMMAND_CALLBACK_PRO = "remotely-save-cb-pro";
|
||||
export const PRO_CLIENT_ID = process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID;
|
||||
@ -6,7 +8,8 @@ export const PRO_WEBSITE = process.env.DEFAULT_REMOTELYSAVE_WEBSITE;
|
||||
|
||||
export type PRO_FEATURE_TYPE =
|
||||
| "feature-smart_conflict"
|
||||
| "feature-google_drive";
|
||||
| "feature-google_drive"
|
||||
| "feature-box";
|
||||
|
||||
export interface FeatureInfo {
|
||||
featureName: PRO_FEATURE_TYPE;
|
||||
@ -24,6 +27,16 @@ export interface ProConfig {
|
||||
credentialsShouldBeDeletedAtTimeMs?: number;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// smart conflict
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
export const MERGABLE_SIZE = 1000 * 1000; // 1 MB
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// Google Drive
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
export interface GoogleDriveConfig {
|
||||
accessToken: string;
|
||||
accessTokenExpiresInMs: number;
|
||||
@ -32,9 +45,28 @@ export interface GoogleDriveConfig {
|
||||
remoteBaseDir?: string;
|
||||
credentialsShouldBeDeletedAtTimeMs?: number;
|
||||
scope: "https://www.googleapis.com/auth/drive.file";
|
||||
kind: "googledrive";
|
||||
}
|
||||
|
||||
export const DEFAULT_GOOGLEDRIVE_CLIENT_ID =
|
||||
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID;
|
||||
export const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET =
|
||||
process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET;
|
||||
|
||||
///////////////////////////////////////////////////////////
|
||||
// box
|
||||
//////////////////////////////////////////////////////////
|
||||
|
||||
export const COMMAND_CALLBACK_BOX = "remotely-save-cb-box";
|
||||
export const BOX_CLIENT_ID = process.env.DEFAULT_BOX_CLIENT_ID;
|
||||
export const BOX_CLIENT_SECRET = process.env.DEFAULT_BOX_CLIENT_SECRET;
|
||||
|
||||
export interface BoxConfig {
|
||||
accessToken: string;
|
||||
accessTokenExpiresInMs: number;
|
||||
accessTokenExpiresAtTimeMs: number;
|
||||
refreshToken: string;
|
||||
remoteBaseDir?: string;
|
||||
credentialsShouldBeDeletedAtTimeMs?: number;
|
||||
kind: "box";
|
||||
}
|
||||
|
||||
927
pro/src/fsBox.ts
Normal file
927
pro/src/fsBox.ts
Normal file
@ -0,0 +1,927 @@
|
||||
import { BoxClient, BoxDeveloperTokenAuth } from "box-typescript-sdk-gen";
|
||||
import {
|
||||
BoxOAuth,
|
||||
OAuthConfig,
|
||||
} from "box-typescript-sdk-gen/lib/box/oauth.generated";
|
||||
import * as mime from "mime-types";
|
||||
import { DEFAULT_CONTENT_TYPE, type Entity } from "../../src/baseTypes";
|
||||
import { FakeFs } from "../../src/fsAll";
|
||||
import {
|
||||
BOX_CLIENT_ID,
|
||||
BOX_CLIENT_SECRET,
|
||||
type BoxConfig,
|
||||
COMMAND_CALLBACK_BOX,
|
||||
} from "./baseTypesPro";
|
||||
|
||||
import type { FileFull } from "box-typescript-sdk-gen/lib/schemas/fileFull.generated";
|
||||
import type { FileFullOrFolderMiniOrWebLink } from "box-typescript-sdk-gen/lib/schemas/fileFullOrFolderMiniOrWebLink.generated";
|
||||
import type { FolderFull } from "box-typescript-sdk-gen/lib/schemas/folderFull.generated";
|
||||
import type { Items } from "box-typescript-sdk-gen/lib/schemas/items.generated";
|
||||
import PQueue from "p-queue";
|
||||
import {
|
||||
delay,
|
||||
getFolderLevels,
|
||||
getSha1,
|
||||
splitFileSizeToChunkRanges,
|
||||
unixTimeToStr,
|
||||
} from "../../src/misc";
|
||||
|
||||
export const DEFAULT_BOX_CONFIG: BoxConfig = {
|
||||
accessToken: "",
|
||||
refreshToken: "",
|
||||
accessTokenExpiresInMs: 0,
|
||||
accessTokenExpiresAtTimeMs: 0,
|
||||
credentialsShouldBeDeletedAtTimeMs: 0, // 60 days https://developer.box.com/guides/authentication/tokens/refresh/
|
||||
kind: "box",
|
||||
};
|
||||
|
||||
export const generateAuthUrl = () => {
|
||||
const config = new OAuthConfig({
|
||||
clientId: BOX_CLIENT_ID ?? "",
|
||||
clientSecret: BOX_CLIENT_SECRET ?? "",
|
||||
});
|
||||
const oauth = new BoxOAuth({ config: config });
|
||||
|
||||
// the URL to redirect the user to
|
||||
const authorize_url = oauth.getAuthorizeUrl({
|
||||
redirectUri: `obsidian://${COMMAND_CALLBACK_BOX}`,
|
||||
});
|
||||
// console.debug(authorize_url)
|
||||
return authorize_url;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://developer.box.com/guides/authentication/oauth2/without-sdk/
|
||||
*/
|
||||
export const sendAuthReq = async (authCode: string, errorCallBack: any) => {
|
||||
try {
|
||||
const k = {
|
||||
code: authCode,
|
||||
grant_type: "authorization_code",
|
||||
client_id: BOX_CLIENT_ID ?? "",
|
||||
client_secret: BOX_CLIENT_SECRET ?? "",
|
||||
// redirect_uri: `obsidian://${COMMAND_CALLBACK_BOX}`,
|
||||
};
|
||||
// console.debug(k);
|
||||
const resp1 = await fetch(`https://api.box.com/oauth2/token`, {
|
||||
method: "POST",
|
||||
body: new URLSearchParams(k),
|
||||
});
|
||||
const resp2 = await resp1.json();
|
||||
return resp2;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (errorCallBack !== undefined) {
|
||||
await errorCallBack(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* https://developer.box.com/guides/authentication/tokens/refresh/
|
||||
*/
|
||||
export const sendRefreshTokenReq = async (refreshToken: string) => {
|
||||
console.debug(`refreshing token`);
|
||||
const x = await fetch("https://api.box.com/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: BOX_CLIENT_ID ?? "",
|
||||
client_secret: BOX_CLIENT_SECRET ?? "",
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (x.status === 200) {
|
||||
const y = await x.json();
|
||||
console.debug(`new token obtained`);
|
||||
return y;
|
||||
} else {
|
||||
throw Error(`cannot refresh an access token`);
|
||||
}
|
||||
};
|
||||
|
||||
export const setConfigBySuccessfullAuthInplace = async (
|
||||
config: BoxConfig,
|
||||
authRes: any,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
if (authRes.access_token === undefined || authRes.access_token === "") {
|
||||
throw Error(
|
||||
`you should not save the setting for ${JSON.stringify(authRes)}`
|
||||
);
|
||||
}
|
||||
|
||||
config.accessToken = authRes.access_token;
|
||||
config.accessTokenExpiresAtTimeMs =
|
||||
Date.now() + authRes.expires_in * 1000 - 5 * 60 * 1000;
|
||||
config.accessTokenExpiresInMs = authRes.expires_in * 1000;
|
||||
config.refreshToken = authRes.refresh_token || config.refreshToken;
|
||||
|
||||
// manually set it expired after 60 days;
|
||||
config.credentialsShouldBeDeletedAtTimeMs =
|
||||
Date.now() + 1000 * 60 * 60 * 24 * 59;
|
||||
|
||||
await saveUpdatedConfigFunc?.();
|
||||
|
||||
console.info("finish updating local info of Box token");
|
||||
};
|
||||
|
||||
interface CreateUploadSessionRawResponse {
|
||||
id: string;
|
||||
type: "upload_session";
|
||||
num_parts_processed: number;
|
||||
part_size: number;
|
||||
session_endpoints: {
|
||||
abort: string;
|
||||
commit: string;
|
||||
list_parts: string;
|
||||
log_event: string;
|
||||
status: string;
|
||||
upload_part: string;
|
||||
};
|
||||
session_expires_at: string;
|
||||
total_parts: number;
|
||||
}
|
||||
|
||||
interface UploadChunkRawResponse {
|
||||
part: {
|
||||
offset: number;
|
||||
part_id: string;
|
||||
sha1: string;
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BoxEntity extends Entity {
|
||||
id: string;
|
||||
parentID: string | undefined;
|
||||
parentIDPath: string | undefined;
|
||||
isFolder: boolean;
|
||||
hashSha1: string | undefined;
|
||||
}
|
||||
|
||||
const fromBoxItemToEntity = (
|
||||
boxItem: FileFullOrFolderMiniOrWebLink | FolderFull,
|
||||
parentID: string,
|
||||
parentFolderPath: string | undefined /* for bfs */
|
||||
): BoxEntity => {
|
||||
if (parentID === undefined || parentID === "" || parentID === "0") {
|
||||
throw Error(`parentID=${parentID} should not be in fromBoxItemToEntity`);
|
||||
}
|
||||
|
||||
let keyRaw = boxItem.name!;
|
||||
|
||||
if (
|
||||
parentFolderPath !== undefined &&
|
||||
parentFolderPath !== "" &&
|
||||
parentFolderPath !== "/"
|
||||
) {
|
||||
if (!parentFolderPath.endsWith("/")) {
|
||||
throw Error(
|
||||
`parentFolderPath=${parentFolderPath} should not be in fromFileToBoxEntity`
|
||||
);
|
||||
}
|
||||
keyRaw = `${parentFolderPath}${boxItem.name!}`;
|
||||
}
|
||||
|
||||
if (boxItem.type === "folder") {
|
||||
keyRaw = `${keyRaw}/`;
|
||||
const mtime =
|
||||
(boxItem as FolderFull).contentModifiedAt?.value.valueOf() ??
|
||||
(boxItem as FolderFull).modifiedAt?.value.valueOf() ??
|
||||
Date.now();
|
||||
return {
|
||||
key: keyRaw,
|
||||
keyRaw: keyRaw,
|
||||
mtimeCli: mtime,
|
||||
mtimeSvr: mtime,
|
||||
id: boxItem.id,
|
||||
parentID: parentID,
|
||||
isFolder: true,
|
||||
size: 0,
|
||||
sizeRaw: 0,
|
||||
hash: undefined,
|
||||
hashSha1: undefined,
|
||||
parentIDPath: parentFolderPath,
|
||||
};
|
||||
} else if (boxItem.type === "file") {
|
||||
const mtime =
|
||||
boxItem.contentModifiedAt?.value.valueOf() ??
|
||||
boxItem.modifiedAt?.value.valueOf() ??
|
||||
Date.now();
|
||||
return {
|
||||
key: keyRaw,
|
||||
keyRaw: keyRaw,
|
||||
mtimeCli: mtime,
|
||||
mtimeSvr: mtime,
|
||||
id: boxItem.id,
|
||||
parentID: parentID,
|
||||
isFolder: false,
|
||||
size: boxItem.size!,
|
||||
sizeRaw: boxItem.size!,
|
||||
hash: boxItem.sha1,
|
||||
hashSha1: boxItem.sha1,
|
||||
parentIDPath: parentFolderPath,
|
||||
};
|
||||
} else {
|
||||
throw Error(`we do not support web link Box item`);
|
||||
}
|
||||
};
|
||||
|
||||
const fromRawResponseToEntity = (
|
||||
boxItem: any,
|
||||
parentID: string,
|
||||
parentFolderPath: string | undefined /* for bfs */
|
||||
): BoxEntity => {
|
||||
if (parentID === undefined || parentID === "" || parentID === "0") {
|
||||
throw Error(
|
||||
`parentID=${parentID} should not be in fromRawResponseToEntity`
|
||||
);
|
||||
}
|
||||
|
||||
let keyRaw = boxItem.name!;
|
||||
|
||||
if (
|
||||
parentFolderPath !== undefined &&
|
||||
parentFolderPath !== "" &&
|
||||
parentFolderPath !== "/"
|
||||
) {
|
||||
if (!parentFolderPath.endsWith("/")) {
|
||||
throw Error(
|
||||
`parentFolderPath=${parentFolderPath} should not be in fromFileToBoxEntity`
|
||||
);
|
||||
}
|
||||
keyRaw = `${parentFolderPath}${boxItem.name!}`;
|
||||
}
|
||||
|
||||
if (boxItem.type === "folder") {
|
||||
keyRaw = `${keyRaw}/`;
|
||||
} else if (boxItem.type === "file") {
|
||||
// pass
|
||||
} else {
|
||||
throw Error(`we do not support web link Box item`);
|
||||
}
|
||||
|
||||
const mtimeStr = boxItem.content_modified_at ?? boxItem.modified_at;
|
||||
let mtime = Date.now();
|
||||
if (mtimeStr !== undefined) {
|
||||
mtime = new Date(mtimeStr).valueOf();
|
||||
}
|
||||
|
||||
return {
|
||||
key: keyRaw,
|
||||
keyRaw: keyRaw,
|
||||
mtimeCli: mtime,
|
||||
mtimeSvr: mtime,
|
||||
id: boxItem.id,
|
||||
parentID: parentID,
|
||||
isFolder: false,
|
||||
size: boxItem.size ?? 0,
|
||||
sizeRaw: boxItem.size ?? 0,
|
||||
hash: boxItem.sha1 ?? undefined,
|
||||
hashSha1: boxItem.sha1 ?? undefined,
|
||||
parentIDPath: parentFolderPath,
|
||||
};
|
||||
};
|
||||
|
||||
export class FakeFsBox extends FakeFs {
|
||||
kind: string;
|
||||
boxConfig: BoxConfig;
|
||||
remoteBaseDir: string;
|
||||
vaultFolderExists: boolean;
|
||||
saveUpdatedConfigFunc: () => Promise<any>;
|
||||
|
||||
keyToBoxEntity: Record<string, BoxEntity>;
|
||||
|
||||
baseDirID: string;
|
||||
|
||||
constructor(
|
||||
boxConfig: BoxConfig,
|
||||
vaultName: string,
|
||||
saveUpdatedConfigFunc: () => Promise<any>
|
||||
) {
|
||||
super();
|
||||
this.kind = "box";
|
||||
this.boxConfig = boxConfig;
|
||||
this.remoteBaseDir = this.boxConfig.remoteBaseDir || vaultName || "";
|
||||
this.vaultFolderExists = false;
|
||||
this.saveUpdatedConfigFunc = saveUpdatedConfigFunc;
|
||||
this.keyToBoxEntity = {};
|
||||
this.baseDirID = "";
|
||||
}
|
||||
|
||||
async _init() {
|
||||
const access = await this._getAccessToken();
|
||||
|
||||
if (this.vaultFolderExists) {
|
||||
// pass
|
||||
} else {
|
||||
const auth = new BoxDeveloperTokenAuth({ token: access });
|
||||
const client = new BoxClient({ auth });
|
||||
|
||||
// find
|
||||
let itemsInRoot: Items | undefined = undefined;
|
||||
|
||||
let offset = 0;
|
||||
const limitPerPage = 1000; // max 1000
|
||||
|
||||
while (!this.vaultFolderExists) {
|
||||
itemsInRoot = await client.folders.getFolderItems("0", {
|
||||
queryParams: {
|
||||
fields: [
|
||||
"id",
|
||||
"type",
|
||||
"name",
|
||||
"sha1",
|
||||
"size",
|
||||
"created_at",
|
||||
"modified_at",
|
||||
"expires_at",
|
||||
"parent",
|
||||
"content_created_at",
|
||||
"content_modified_at",
|
||||
"etag",
|
||||
],
|
||||
offset: offset,
|
||||
limit: limitPerPage,
|
||||
},
|
||||
});
|
||||
// console.debug(`this.remoteBaseDir=${this.remoteBaseDir}`);
|
||||
// console.debug(`itemsInRoot:`);
|
||||
// console.debug(itemsInRoot);
|
||||
if (
|
||||
(itemsInRoot.entries?.filter((x) => x.name === this.remoteBaseDir)
|
||||
.length ?? 0) > 0
|
||||
) {
|
||||
// we find it!
|
||||
const f = itemsInRoot.entries?.filter(
|
||||
(x) => x.name === this.remoteBaseDir
|
||||
)[0]!;
|
||||
this.baseDirID = f.id;
|
||||
this.vaultFolderExists = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ((itemsInRoot.offset ?? 0) >= (itemsInRoot.totalCount ?? 0)) {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += limitPerPage;
|
||||
}
|
||||
|
||||
if (!this.vaultFolderExists) {
|
||||
// create
|
||||
const f = await client.folders.createFolder({
|
||||
name: this.remoteBaseDir,
|
||||
parent: { id: "0" },
|
||||
});
|
||||
this.baseDirID = f.id;
|
||||
this.vaultFolderExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _getAccessToken() {
|
||||
if (this.boxConfig.refreshToken === "") {
|
||||
throw Error("The user has not manually auth yet.");
|
||||
}
|
||||
|
||||
const ts = Date.now();
|
||||
const comp = this.boxConfig.accessTokenExpiresAtTimeMs > ts;
|
||||
// console.debug(`this.boxConfig.accessTokenExpiresAtTimeMs=${this.boxConfig.accessTokenExpiresAtTimeMs},ts=${ts},comp=${comp}`)
|
||||
if (comp) {
|
||||
return this.boxConfig.accessToken;
|
||||
}
|
||||
|
||||
// refresh
|
||||
const k = await sendRefreshTokenReq(this.boxConfig.refreshToken);
|
||||
this.boxConfig.accessToken = k.access_token;
|
||||
this.boxConfig.accessTokenExpiresInMs = k.expires_in * 1000;
|
||||
this.boxConfig.accessTokenExpiresAtTimeMs =
|
||||
ts + k.expires_in * 1000 - 60 * 2 * 1000;
|
||||
await this.saveUpdatedConfigFunc();
|
||||
console.info("Box accessToken updated");
|
||||
return this.boxConfig.accessToken;
|
||||
|
||||
// const access = "tUf643YKLuGhUmXRBKPAK0hz9ZKv85kS";
|
||||
// this.boxConfig.accessToken = access;
|
||||
// return access;
|
||||
}
|
||||
|
||||
async walk(): Promise<Entity[]> {
|
||||
await this._init();
|
||||
|
||||
const allFiles: BoxEntity[] = [];
|
||||
|
||||
// bfs
|
||||
const queue = new PQueue({
|
||||
concurrency: 5, // TODO: make it configurable?
|
||||
autoStart: true,
|
||||
});
|
||||
queue.on("error", (error) => {
|
||||
queue.pause();
|
||||
queue.clear();
|
||||
throw error;
|
||||
});
|
||||
|
||||
let parents = [
|
||||
{
|
||||
id: this.baseDirID, // special init, from already created root folder ID
|
||||
folderPath: "",
|
||||
},
|
||||
];
|
||||
|
||||
while (parents.length !== 0) {
|
||||
// console.debug('enter while loop 1 of parents array');
|
||||
const children: typeof parents = [];
|
||||
for (const { id, folderPath } of parents) {
|
||||
queue.add(async () => {
|
||||
const filesUnderFolder = await this._walkFolder(id, folderPath);
|
||||
for (const f of filesUnderFolder) {
|
||||
allFiles.push(f);
|
||||
if (f.isFolder) {
|
||||
// keyRaw itself already has a tailing slash, no more slash here
|
||||
// keyRaw itself also already has full path
|
||||
const child = {
|
||||
id: f.id,
|
||||
folderPath: f.keyRaw,
|
||||
};
|
||||
// console.debug(
|
||||
// `looping result of _walkFolder(${id},${folderPath}), adding child=${JSON.stringify(
|
||||
// child
|
||||
// )}`
|
||||
// );
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
await queue.onIdle();
|
||||
parents = children;
|
||||
}
|
||||
|
||||
// console.debug(`in the end of walk:`);
|
||||
// console.debug(allFiles);
|
||||
// console.debug(this.keyToBoxEntity);
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async _walkFolder(
|
||||
parentID: string,
|
||||
parentFolderPath: string
|
||||
): Promise<BoxEntity[]> {
|
||||
// console.debug(
|
||||
// `input of single level: parentID=${parentID}, parentFolderPath=${parentFolderPath}`
|
||||
// );
|
||||
const filesOneLevel: BoxEntity[] = [];
|
||||
const access = await this._getAccessToken();
|
||||
const auth = new BoxDeveloperTokenAuth({ token: access });
|
||||
const client = new BoxClient({ auth });
|
||||
|
||||
if (parentID === undefined || parentID === "" || parentID === "root") {
|
||||
// we should never start from root
|
||||
// because we encapsulate the vault inside a folder
|
||||
throw Error(`something goes wrong walking folder`);
|
||||
}
|
||||
|
||||
let items: Items | undefined = undefined;
|
||||
|
||||
let offset = 0;
|
||||
const limitPerPage = 1000; // max 1000
|
||||
do {
|
||||
// console.debug(`entering paging of parentID=${parentID}, offset=${offset}`);
|
||||
items = await client.folders.getFolderItems(parentID, {
|
||||
queryParams: {
|
||||
fields: [
|
||||
"id",
|
||||
"type",
|
||||
"name",
|
||||
"sha1",
|
||||
"size",
|
||||
"created_at",
|
||||
"modified_at",
|
||||
"expires_at",
|
||||
"parent",
|
||||
"content_created_at",
|
||||
"content_modified_at",
|
||||
"etag",
|
||||
],
|
||||
offset: offset,
|
||||
limit: limitPerPage,
|
||||
},
|
||||
});
|
||||
// console.debug(`items of parentID=${parentID},offset=${offset}:`);
|
||||
// console.debug(items);
|
||||
|
||||
for (const item of items.entries ?? []) {
|
||||
const entity = fromBoxItemToEntity(item, parentID, parentFolderPath);
|
||||
this.keyToBoxEntity[entity.keyRaw] = entity; // build cache
|
||||
filesOneLevel.push(entity);
|
||||
}
|
||||
|
||||
offset += limitPerPage;
|
||||
// console.debug(`end of current loop parentID=${parentID}, and offset=${offset}`);
|
||||
} while (offset < (items?.totalCount ?? 0));
|
||||
|
||||
return filesOneLevel;
|
||||
}
|
||||
|
||||
async walkPartial(): Promise<Entity[]> {
|
||||
await this._init();
|
||||
const filesInLevel = await this._walkFolder(this.baseDirID, "");
|
||||
return filesInLevel;
|
||||
}
|
||||
|
||||
async stat(key: string): Promise<Entity> {
|
||||
await this._init();
|
||||
|
||||
// TODO: we already have a cache, should we call again?
|
||||
const cachedEntity = this.keyToBoxEntity[key];
|
||||
const fileID = cachedEntity?.id;
|
||||
if (cachedEntity === undefined || fileID === undefined) {
|
||||
throw Error(`no fileID found for key=${key}`);
|
||||
}
|
||||
|
||||
const access = await this._getAccessToken();
|
||||
const auth = new BoxDeveloperTokenAuth({ token: access });
|
||||
const client = new BoxClient({ auth });
|
||||
|
||||
let f: FileFull | FolderFull | undefined;
|
||||
if (cachedEntity.isFolder) {
|
||||
f = await client.folders.getFolderById(fileID);
|
||||
} else {
|
||||
f = await client.files.getFileById(fileID);
|
||||
}
|
||||
const entity = fromBoxItemToEntity(
|
||||
f,
|
||||
cachedEntity.parentID!,
|
||||
cachedEntity.parentIDPath!
|
||||
);
|
||||
// insert back to cache?? to update it??
|
||||
this.keyToBoxEntity[key] = entity;
|
||||
return entity;
|
||||
}
|
||||
|
||||
async mkdir(
|
||||
key: string,
|
||||
mtime?: number | undefined,
|
||||
ctime?: number | undefined
|
||||
): Promise<Entity> {
|
||||
if (!key.endsWith("/")) {
|
||||
throw Error(`you should not mkdir on key=${key}`);
|
||||
}
|
||||
|
||||
await this._init();
|
||||
|
||||
const cachedEntity = this.keyToBoxEntity[key];
|
||||
const fileID = cachedEntity?.id;
|
||||
if (cachedEntity !== undefined && fileID !== undefined) {
|
||||
return cachedEntity;
|
||||
}
|
||||
|
||||
// xxx/ => ["xxx"]
|
||||
// xxx/yyy/zzz/ => ["xxx", "xxx/yyy", "xxx/yyy/zzz"]
|
||||
const folderLevels = getFolderLevels(key);
|
||||
let parentFolderPath: string | undefined = undefined;
|
||||
let parentID: string | undefined = undefined;
|
||||
if (folderLevels.length === 0) {
|
||||
throw Error(`cannot getFolderLevels of ${key}`);
|
||||
} else if (folderLevels.length === 1) {
|
||||
parentID = this.baseDirID;
|
||||
parentFolderPath = ""; // ignore base dir
|
||||
} else {
|
||||
// length > 1
|
||||
parentFolderPath = `${folderLevels[folderLevels.length - 2]}/`;
|
||||
if (!(parentFolderPath in this.keyToBoxEntity)) {
|
||||
throw Error(
|
||||
`parent of ${key}: ${parentFolderPath} is not created before??`
|
||||
);
|
||||
}
|
||||
parentID = this.keyToBoxEntity[parentFolderPath].id;
|
||||
}
|
||||
|
||||
// xxx/yyy/zzz/ => ["xxx", "xxx/yyy", "xxx/yyy/zzz"] => "xxx/yyy/zzz" => "zzz"
|
||||
let folderItselfWithoutSlash = folderLevels[folderLevels.length - 1];
|
||||
folderItselfWithoutSlash = folderItselfWithoutSlash.split("/").pop()!;
|
||||
|
||||
const access = await this._getAccessToken();
|
||||
const auth = new BoxDeveloperTokenAuth({ token: access });
|
||||
const client = new BoxClient({ auth });
|
||||
const f = await client.folders.createFolder({
|
||||
name: folderItselfWithoutSlash,
|
||||
parent: { id: parentID },
|
||||
});
|
||||
|
||||
const entity = fromBoxItemToEntity(f, parentID, parentFolderPath);
|
||||
// insert into cache
|
||||
this.keyToBoxEntity[key] = entity;
|
||||
return entity;
|
||||
}
|
||||
|
||||
async writeFile(
|
||||
key: string,
|
||||
content: ArrayBuffer,
|
||||
mtime: number,
|
||||
ctime: number
|
||||
): Promise<Entity> {
|
||||
if (key.endsWith("/")) {
|
||||
throw Error(`should not call writeFile on ${key}`);
|
||||
}
|
||||
|
||||
await this._init();
|
||||
|
||||
const prevCachedEntity: BoxEntity | undefined = this.keyToBoxEntity[key];
|
||||
const prevFileID: string | undefined = prevCachedEntity?.id;
|
||||
|
||||
const contentType =
|
||||
mime.contentType(mime.lookup(key) || DEFAULT_CONTENT_TYPE) ||
|
||||
DEFAULT_CONTENT_TYPE;
|
||||
|
||||
let parentID: string | undefined = undefined;
|
||||
let parentFolderPath: string | undefined = undefined;
|
||||
|
||||
// "xxx" => []
|
||||
// "xxx/yyy/zzz.md" => ["xxx", "xxx/yyy"]
|
||||
const folderLevels = getFolderLevels(key);
|
||||
if (folderLevels.length === 0) {
|
||||
// root
|
||||
parentID = this.baseDirID;
|
||||
parentFolderPath = "";
|
||||
} else {
|
||||
parentFolderPath = `${folderLevels[folderLevels.length - 1]}/`;
|
||||
if (!(parentFolderPath in this.keyToBoxEntity)) {
|
||||
throw Error(
|
||||
`parent of ${key}: ${parentFolderPath} is not created before??`
|
||||
);
|
||||
}
|
||||
parentID = this.keyToBoxEntity[parentFolderPath].id;
|
||||
}
|
||||
|
||||
const fileItself = key.split("/").pop()!;
|
||||
|
||||
const BIG_FILE_THRESHOLD = 20000000; // box api hard coded...
|
||||
if (content.byteLength <= BIG_FILE_THRESHOLD) {
|
||||
const formData = new FormData();
|
||||
const attributes = {
|
||||
name: fileItself,
|
||||
parent: { id: parentID },
|
||||
content_created_at: unixTimeToStr(ctime),
|
||||
content_modified_at: unixTimeToStr(mtime),
|
||||
};
|
||||
formData.append("attributes", JSON.stringify(attributes));
|
||||
formData.append("file", new Blob([content], { type: contentType }));
|
||||
|
||||
let url = "";
|
||||
if (prevFileID === undefined) {
|
||||
// create new file
|
||||
// https://developer.box.com/reference/post-files-content/
|
||||
url = `https://upload.box.com/api/2.0/files/content`;
|
||||
} else {
|
||||
// update new file
|
||||
// https://developer.box.com/reference/post-files-id-content/
|
||||
url = `https://upload.box.com/api/2.0/files/${prevFileID}/content`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (res.status !== 200 && res.status !== 201) {
|
||||
throw Error(
|
||||
`create file ${key} failed! attributes=${JSON.stringify(attributes)}`
|
||||
);
|
||||
}
|
||||
|
||||
const res2 = await res.json();
|
||||
if (res2.entries === undefined) {
|
||||
throw Error(`upload small file ${key} failed!`);
|
||||
}
|
||||
const entity = fromRawResponseToEntity(
|
||||
res2.entries[0],
|
||||
parentID,
|
||||
parentFolderPath
|
||||
);
|
||||
this.keyToBoxEntity[key] = entity;
|
||||
// console.debug(`entity after upload=${JSON.stringify(entity, null, 2)}`);
|
||||
return entity;
|
||||
} else {
|
||||
// create session
|
||||
|
||||
let url = "";
|
||||
if (prevFileID === undefined) {
|
||||
// https://developer.box.com/reference/post-files-upload-sessions/
|
||||
url = "https://upload.box.com/api/2.0/files/upload_sessions";
|
||||
} else {
|
||||
// https://developer.box.com/reference/post-files-id-upload-sessions/
|
||||
url = `https://upload.box.com/api/2.0/files/${prevFileID}/upload_sessions`;
|
||||
}
|
||||
|
||||
const sessionRes1 = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_name: fileItself,
|
||||
file_size: content.byteLength,
|
||||
folder_id: parentID,
|
||||
}),
|
||||
});
|
||||
if (sessionRes1.status !== 200 && sessionRes1.status !== 201) {
|
||||
throw Error(
|
||||
`Create upload session for ${key} failed! Response header=${JSON.stringify(
|
||||
sessionRes1.headers
|
||||
)}`
|
||||
);
|
||||
}
|
||||
const sessionRes2: CreateUploadSessionRawResponse =
|
||||
await sessionRes1.json();
|
||||
// console.debug(sessionRes2);
|
||||
|
||||
// upload by chunks
|
||||
const sizePerChunk = sessionRes2.part_size;
|
||||
const chunkRanges = splitFileSizeToChunkRanges(
|
||||
content.byteLength,
|
||||
sizePerChunk
|
||||
);
|
||||
// TODO: parallel
|
||||
const partsResult: UploadChunkRawResponse[] = [];
|
||||
for (const { start, end } of chunkRanges) {
|
||||
const subContent = content.slice(start, end + 1);
|
||||
const sha1 = await getSha1(subContent, "base64");
|
||||
const res = await fetch(sessionRes2.session_endpoints.upload_part, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
// "Content-Length": `${end - start + 1}`, // the number of bytes in the current chunk
|
||||
"Content-Range": `bytes ${start}-${end}/${content.byteLength}`,
|
||||
"Content-Type": "application/octet-stream",
|
||||
digest: `sha=${sha1}`,
|
||||
},
|
||||
body: subContent,
|
||||
});
|
||||
if (res.status !== 200 && res.status !== 201) {
|
||||
throw Error(
|
||||
`Upload chunk for ${key}, ${start}-${end} failed! Response header=${JSON.stringify(
|
||||
res.headers
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
partsResult.push((await res.json()) as UploadChunkRawResponse);
|
||||
}
|
||||
// commit?
|
||||
const sha1 = await getSha1(content, "base64");
|
||||
let status = 202;
|
||||
let tries = 0;
|
||||
do {
|
||||
// console.debug(`begin commit key=${key} for tries=${tries}`)
|
||||
const commitRes1 = await fetch(sessionRes2.session_endpoints.commit, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
|
||||
"Content-Type": "application/json",
|
||||
digest: `sha=${sha1}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
parts: partsResult.map((p) => p.part),
|
||||
attributes: {
|
||||
content_modified_at: unixTimeToStr(mtime, false),
|
||||
content_created_at: unixTimeToStr(ctime, false),
|
||||
},
|
||||
}),
|
||||
});
|
||||
status = commitRes1.status;
|
||||
// console.debug(`status===${status} for tries=${tries},key=${key}`)
|
||||
if (status === 200 || status === 201) {
|
||||
const commitRes2 = await commitRes1.json();
|
||||
if (commitRes2.entries === undefined) {
|
||||
throw Error(`Upload big file ${key} failed!`);
|
||||
}
|
||||
const entity = fromRawResponseToEntity(
|
||||
commitRes2.entries[0],
|
||||
parentID,
|
||||
parentFolderPath
|
||||
);
|
||||
this.keyToBoxEntity[key] = entity;
|
||||
// console.debug(
|
||||
// `entity after upload=${JSON.stringify(entity, null, 2)}`
|
||||
// );
|
||||
return entity;
|
||||
} else if (status === 202) {
|
||||
await delay(500);
|
||||
tries += 1;
|
||||
} else {
|
||||
throw Error(
|
||||
`Commit all chunks for ${key} failed! Response header=${JSON.stringify(
|
||||
commitRes1.headers
|
||||
)}`
|
||||
);
|
||||
}
|
||||
// console.debug(`end commit key=${key}, currently status===${status}, tries===${tries} for next loop`)
|
||||
} while (status === 202 && tries < 4);
|
||||
throw Error(`Commit all chunks for ${key} failed! No idea what happened`);
|
||||
}
|
||||
}
|
||||
|
||||
async readFile(key: string): Promise<ArrayBuffer> {
|
||||
await this._init();
|
||||
|
||||
const cachedEntity = this.keyToBoxEntity[key];
|
||||
const fileID = cachedEntity?.id;
|
||||
if (cachedEntity === undefined || fileID === undefined) {
|
||||
throw Error(`no fileID found for key=${key}`);
|
||||
}
|
||||
|
||||
const res1 = await fetch(
|
||||
`https://api.box.com/2.0/files/${fileID}/content`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${await this._getAccessToken()}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
if (res1.status !== 200) {
|
||||
throw Error(
|
||||
`Cannot download file ${key} with id ${fileID}. Response header=${JSON.stringify(
|
||||
res1.headers
|
||||
)}`
|
||||
);
|
||||
}
|
||||
const res2 = await res1.arrayBuffer();
|
||||
return res2;
|
||||
}
|
||||
|
||||
async rename(key1: string, key2: string): Promise<void> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async rm(key: string): Promise<void> {
|
||||
await this._init();
|
||||
|
||||
const cachedEntity = this.keyToBoxEntity[key];
|
||||
const fileID = cachedEntity?.id;
|
||||
if (cachedEntity === undefined || fileID === undefined) {
|
||||
throw Error(`no fileID found for key=${key}`);
|
||||
}
|
||||
|
||||
const access = await this._getAccessToken();
|
||||
const auth = new BoxDeveloperTokenAuth({ token: access });
|
||||
const client = new BoxClient({ auth });
|
||||
|
||||
if (cachedEntity.isFolder) {
|
||||
await client.folders.deleteFolderById(fileID, {
|
||||
queryParams: { recursive: true },
|
||||
});
|
||||
} else {
|
||||
await client.files.deleteFileById(fileID);
|
||||
}
|
||||
}
|
||||
|
||||
async checkConnect(callbackFunc?: any): Promise<boolean> {
|
||||
// if we can init, we can connect
|
||||
try {
|
||||
await this._init();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
callbackFunc?.(err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async getUserDisplayName(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* https://developer.box.com/guides/authentication/tokens/revoke/
|
||||
*/
|
||||
async revokeAuth(): Promise<any> {
|
||||
await fetch(`https://api.box.com/oauth2/revoke`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: BOX_CLIENT_ID ?? "",
|
||||
client_secret: BOX_CLIENT_SECRET ?? "",
|
||||
token: this.boxConfig.refreshToken,
|
||||
}).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
allowEmptyFile(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ export const DEFAULT_GOOGLEDRIVE_CONFIG: GoogleDriveConfig = {
|
||||
accessTokenExpiresAtTimeMs: 0,
|
||||
credentialsShouldBeDeletedAtTimeMs: 0,
|
||||
scope: "https://www.googleapis.com/auth/drive.file",
|
||||
kind: "googledrive",
|
||||
};
|
||||
|
||||
const FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
|
||||
|
||||
@ -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_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?",
|
||||
"protocol_box_connect_succ_revoke": "You've connected. If you want to disconnect, click this button.",
|
||||
|
||||
"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!",
|
||||
@ -23,6 +28,22 @@
|
||||
"modal_googledriverevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_googledriverevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
|
||||
"modal_boxauth_tutorial": "<p>Please firstly go to the address, then go on the auth flow. In the end, you will be redirected to here.</p>",
|
||||
"modal_boxauth_copybutton": "Click to copy the auth url",
|
||||
"modal_boxauth_copynotice": "The auth url is copied to the clipboard!",
|
||||
"modal_box_maualinput": "The Code from the website",
|
||||
"modal_box_maualinput_desc": "Please input the code here from the end of auth flow, and press confirm.",
|
||||
"modal_box_maualinput_notice": "We are trying to connect to Box and update the credentials...",
|
||||
"modal_box_maualinput_succ_notice": "Great! The credentials are updated!",
|
||||
"modal_box_maualinput_fail_notice": "Oops! Failed to update the credentials. Please try again later.",
|
||||
"modal_boxrevokeauth_step1": "Step 1: Go to the following address, you can remove the connection there.",
|
||||
"modal_boxrevokeauth_step2": "Step 2: Click the button below, to clean the locally-saved login credentials.",
|
||||
"modal_boxrevokeauth_clean": "Clean Locally-Saved Login Credentials",
|
||||
"modal_boxrevokeauth_clean_desc": "You need to click the button.",
|
||||
"modal_boxrevokeauth_clean_button": "Clean",
|
||||
"modal_boxrevokeauth_clean_notice": "Cleaned!",
|
||||
"modal_boxrevokeauth_clean_fail": "Something goes wrong while revoking.",
|
||||
|
||||
"modal_prorevokeauth": "Revoke auth by clicking here and follow the steps.",
|
||||
"modal_prorevokeauth_clean": "Clean",
|
||||
"modal_prorevokeauth_clean_desc": "Clean local auth record",
|
||||
@ -54,7 +75,26 @@
|
||||
"settings_googledrive_connect_succ": "Great! We can connect to Google Drive!",
|
||||
"settings_googledrive_connect_fail": "We cannot connect to Google Drive.",
|
||||
|
||||
"settings_box": "Box (PRO) (beta)",
|
||||
"settings_chooseservice_box": "Box (PRO) (beta)",
|
||||
"settings_box_disclaimer1": "Disclaimer: This app is NOT an official Box product. The app just uses Box's public api.",
|
||||
"settings_box_disclaimer2": "Disclaimer: The information is stored locally. Other malicious/harmful/faulty plugins could read the info. If you see any unintentional access to your Box, please immediately disconnect this app on https://app.box.com/account/security .",
|
||||
"settings_box_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_box_notshowuphint": "Box Settings Not Available",
|
||||
"settings_box_notshowuphint_desc": "Box settings are not available, because you haven't subscribed to the PRO feature in your Remotely Save account.",
|
||||
"settings_box_notshowuphint_view_pro": "View PRO Settings",
|
||||
"settings_box_folder": "We will create and sync inside the folder {{remoteBaseDir}} on your Box. DO NOT create this folder by yourself manually.",
|
||||
"settings_box_revoke": "Revoke Auth",
|
||||
"settings_box_revoke_desc": "You've connected. If you want to disconnect, click this button.",
|
||||
"settings_box_revoke_button": "Revoke Auth",
|
||||
"settings_box_auth": "Auth",
|
||||
"settings_box_auth_desc": "Auth.",
|
||||
"settings_box_auth_button": "Auth",
|
||||
"settings_box_connect_succ": "Great! We can connect to Box!",
|
||||
"settings_box_connect_fail": "We cannot connect to Box.",
|
||||
|
||||
"settings_export_googledrive_button": "Export Google Drive Part",
|
||||
"settings_export_box_button": "Export Box Part",
|
||||
|
||||
"settings_pro": "Account (for PRO features)",
|
||||
"settings_pro_tutorial": "<p>Using <stong>basic</strong> features of Remotely Save is <strong>FREE</strong> and do <strong>NOT</strong> need an account.</p><p>However, you will <strong>need</strong> an online account and <strong>PAY</strong> for the <strong>PRO</strong> features such as smart conflict.</p><p>Firstly please click the button to sign up and sign in to the website: <a href=\"https://remotelysave.com\">https://remotelysave.com</a>. Notice: It's different from, and NOT affiliated with Obsidian account.</p><p>Secondly please \"connect\" your local device to your online account.",
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
"protocol_pro_connect_fail": "Remotely Save 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
|
||||
"protocol_pro_connect_succ_revoke": "您已连接上账号 {{email}}。如果要取消连接,请点击此按钮。",
|
||||
|
||||
"protocol_box_connecting": "正在连接",
|
||||
"protocol_box_connect_manualinput_succ": "连接成功",
|
||||
"protocol_box_connect_fail": "Box 官网返回错误。可能是网络连接不稳定。也可能是您拒绝了授权?",
|
||||
"protocol_box_connect_succ_revoke": "您已连接上账号。如果要取消连接,请点击此按钮。",
|
||||
|
||||
"modal_googledriveauth_tutorial": "<p>请访问此网址,然后会进入授权流程。最后,您会看到一个码,请复制粘贴到这里然后提交。</p>",
|
||||
"modal_googledriveauth_copybutton": "点击以复制网址",
|
||||
"modal_googledriveauth_copynotice": "网址已复制!",
|
||||
@ -23,6 +28,22 @@
|
||||
"modal_googledriverevokeauth_clean_notice": "已清理!",
|
||||
"modal_googledriverevokeauth_clean_fail": "清理授权时候发生了错误。",
|
||||
|
||||
"modal_boxauth_tutorial": "<p>请访问此网址,然后会进入授权流程。最后,您会被重定向回来。</p>",
|
||||
"modal_boxauth_copybutton": "点击以复制网址",
|
||||
"modal_boxauth_copynotice": "网址已复制!",
|
||||
"modal_box_maualinput": "网站上的码",
|
||||
"modal_box_maualinput_desc": "请粘贴授权流程最后的那个码,然后点击确认。",
|
||||
"modal_box_maualinput_notice": "正在尝试连接 Box 并更新授权信息......",
|
||||
"modal_box_maualinput_succ_notice": "很好!授权信息已更新!",
|
||||
"modal_box_maualinput_fail_notice": "更新授权信息失败。请稍后重试。",
|
||||
"modal_boxrevokeauth_step1": "第 1 步:访问以下网址,可以删除连接。",
|
||||
"modal_boxrevokeauth_step2": "第 2 步:点击以下按钮,从而清理本地的登录信息。",
|
||||
"modal_boxrevokeauth_clean": "清理本地登录信息",
|
||||
"modal_boxrevokeauth_clean_desc": "您需要点击此按钮。",
|
||||
"modal_boxrevokeauth_clean_button": "清理",
|
||||
"modal_boxrevokeauth_clean_notice": "已清理!",
|
||||
"modal_boxrevokeauth_clean_fail": "清理授权时候发生了错误。",
|
||||
|
||||
"modal_prorevokeauth": "点击这里和按照步骤取消授权。",
|
||||
"modal_prorevokeauth_clean": "清理",
|
||||
"modal_prorevokeauth_clean_desc": "清理本地授权记录",
|
||||
@ -54,7 +75,26 @@
|
||||
"settings_googledrive_connect_succ": "很好!我们可连接上 Google Drive!",
|
||||
"settings_googledrive_connect_fail": "我们未能连接上 Google Drive。",
|
||||
|
||||
"settings_box": "Box (PRO) (beta)",
|
||||
"settings_chooseservice_box": "Box (PRO) (beta)",
|
||||
"settings_box_disclaimer1": "声明:本插件不是 Box 的官方产品。只是用到了它的公开 API。",
|
||||
"settings_box_disclaimer2": "声明:您所输入的信息存储于本地。其它有害的或者出错的插件,是有可能读取到这些信息的。如果您发现任何不符合预期的 Box 访问,请立刻在以下网站操作断开连接: https://app.box.com/account/security 。",
|
||||
"settings_box_pro_desc": "<p><strong>!!这是 PRO(付费)功能! 您需要在线账号来使用此功能!!</strong>(<a href=\"#settings-pro\">向下滑</a>可以看到 PRO 账号的更多信息。)</p>",
|
||||
"settings_box_notshowuphint": "Box 设置不可用",
|
||||
"settings_box_notshowuphint_desc": "Box 设置不可用,因为您没有在 Remotely Save 账号里开启这个 PRO 功能。",
|
||||
"settings_box_notshowuphint_view_pro": "查看 PRO 相关设置",
|
||||
"settings_box_folder": "我们会在 Box 创建此文件夹并同步内容进去: {{remoteBaseDir}} 。请不要手动在网站上创建。",
|
||||
"settings_box_revoke": "撤回鉴权",
|
||||
"settings_box_revoke_desc": "您现在已连接。如果想取消连接,请点击此按钮。",
|
||||
"settings_box_revoke_button": "撤回鉴权",
|
||||
"settings_box_auth": "鉴权",
|
||||
"settings_box_auth_desc": "鉴权.",
|
||||
"settings_box_auth_button": "鉴权",
|
||||
"settings_box_connect_succ": "很好!我们可连接上 Box!",
|
||||
"settings_box_connect_fail": "我们未能连接上 Box。",
|
||||
|
||||
"settings_export_googledrive_button": "导出 Google Drive 部分",
|
||||
"settings_export_box_button": "导出 Box 部分",
|
||||
|
||||
"settings_pro": "账号(PRO 付费功能)",
|
||||
"settings_pro_tutorial": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免费的</strong>,而且<strong>不</strong>需要注册对应账号。</p><p>但是,您<strong>需要</strong>注册账号和对<strong>PRO</strong>功能<strong>付费</strong>使用,如智能处理冲突功能。</p><p>第一步:点击按钮从而注册和登录网站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:这和 Obsidian 官方账号无关,是不同的账号。</p><p>第二部:点击“连接”按钮,从而连接本设备和在线账号。",
|
||||
|
||||
@ -7,6 +7,11 @@
|
||||
"protocol_pro_connect_fail": "Remotely Save 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
|
||||
"protocol_pro_connect_succ_revoke": "您已連線上賬號 {{email}}。如果要取消連線,請點選此按鈕。",
|
||||
|
||||
"protocol_box_connecting": "正在連線",
|
||||
"protocol_box_connect_manualinput_succ": "連線成功",
|
||||
"protocol_box_connect_fail": "Box 官網返回錯誤。可能是網路連線不穩定。也可能是您拒絕了授權?",
|
||||
"protocol_box_connect_succ_revoke": "您已連線上賬號。如果要取消連線,請點選此按鈕。",
|
||||
|
||||
"modal_googledriveauth_tutorial": "<p>請訪問此網址,然後會進入授權流程。最後,您會看到一個碼,請複製貼上到這裡然後提交。</p>",
|
||||
"modal_googledriveauth_copybutton": "點選以複製網址",
|
||||
"modal_googledriveauth_copynotice": "網址已複製!",
|
||||
@ -54,7 +59,42 @@
|
||||
"settings_googledrive_connect_succ": "很好!我們可連線上 Google Drive!",
|
||||
"settings_googledrive_connect_fail": "我們未能連線上 Google Drive。",
|
||||
|
||||
"modal_boxauth_tutorial": "<p>請訪問此網址,然後會進入授權流程。最後,您會被重定向回來。</p>",
|
||||
"modal_boxauth_copybutton": "點選以複製網址",
|
||||
"modal_boxauth_copynotice": "網址已複製!",
|
||||
"modal_box_maualinput": "網站上的碼",
|
||||
"modal_box_maualinput_desc": "請貼上授權流程最後的那個碼,然後點選確認。",
|
||||
"modal_box_maualinput_notice": "正在嘗試連線 Box 並更新授權資訊......",
|
||||
"modal_box_maualinput_succ_notice": "很好!授權資訊已更新!",
|
||||
"modal_box_maualinput_fail_notice": "更新授權資訊失敗。請稍後重試。",
|
||||
"modal_boxrevokeauth_step1": "第 1 步:訪問以下網址,可以刪除連線。",
|
||||
"modal_boxrevokeauth_step2": "第 2 步:點選以下按鈕,從而清理本地的登入資訊。",
|
||||
"modal_boxrevokeauth_clean": "清理本地登入資訊",
|
||||
"modal_boxrevokeauth_clean_desc": "您需要點選此按鈕。",
|
||||
"modal_boxrevokeauth_clean_button": "清理",
|
||||
"modal_boxrevokeauth_clean_notice": "已清理!",
|
||||
"modal_boxrevokeauth_clean_fail": "清理授權時候發生了錯誤。",
|
||||
|
||||
"settings_box": "Box (PRO) (beta)",
|
||||
"settings_chooseservice_box": "Box (PRO) (beta)",
|
||||
"settings_box_disclaimer1": "宣告:本外掛不是 Box 的官方產品。只是用到了它的公開 API。",
|
||||
"settings_box_disclaimer2": "宣告:您所輸入的資訊儲存於本地。其它有害的或者出錯的外掛,是有可能讀取到這些資訊的。如果您發現任何不符合預期的 Box 訪問,請立刻在以下網站操作斷開連線: https://app.box.com/account/security 。",
|
||||
"settings_box_pro_desc": "<p><strong>!!這是 PRO(付費)功能! 您需要線上賬號來使用此功能!!</strong>(<a href=\"#settings-pro\">向下滑</a>可以看到 PRO 賬號的更多資訊。)</p>",
|
||||
"settings_box_notshowuphint": "Box 設定不可用",
|
||||
"settings_box_notshowuphint_desc": "Box 設定不可用,因為您沒有在 Remotely Save 賬號裡開啟這個 PRO 功能。",
|
||||
"settings_box_notshowuphint_view_pro": "檢視 PRO 相關設定",
|
||||
"settings_box_folder": "我們會在 Box 建立此資料夾並同步內容進去: {{remoteBaseDir}} 。請不要手動在網站上建立。",
|
||||
"settings_box_revoke": "撤回鑑權",
|
||||
"settings_box_revoke_desc": "您現在已連線。如果想取消連線,請點選此按鈕。",
|
||||
"settings_box_revoke_button": "撤回鑑權",
|
||||
"settings_box_auth": "鑑權",
|
||||
"settings_box_auth_desc": "鑑權.",
|
||||
"settings_box_auth_button": "鑑權",
|
||||
"settings_box_connect_succ": "很好!我們可連線上 Box!",
|
||||
"settings_box_connect_fail": "我們未能連線上 Box。",
|
||||
|
||||
"settings_export_googledrive_button": "匯出 Google Drive 部分",
|
||||
"settings_export_box_button": "匯出 Box 部分",
|
||||
|
||||
"settings_pro": "賬號(PRO 付費功能)",
|
||||
"settings_pro_tutorial": "<p>使用 Remotely Save 的<stong>基本</strong>功能是<strong>免費的</strong>,而且<strong>不</strong>需要註冊對應賬號。</p><p>但是,您<strong>需要</strong>註冊賬號和對<strong>PRO</strong>功能<strong>付費</strong>使用,如智慧處理衝突功能。</p><p>第一步:點選按鈕從而註冊和登入網站:<a href=\"https://remotelysave.com\">https://remotelysave.com</a>。注意:這和 Obsidian 官方賬號無關,是不同的賬號。</p><p>第二部:點選“連線”按鈕,從而連線本裝置和線上賬號。",
|
||||
|
||||
26
pro/src/oauth2.ts
Normal file
26
pro/src/oauth2.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { base64url } from "rfc4648";
|
||||
|
||||
/**
|
||||
* https://datatracker.ietf.org/doc/html/rfc7636
|
||||
* dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
|
||||
* => E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
|
||||
* @param x
|
||||
* @returns BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
|
||||
*/
|
||||
export async function codeVerifier2CodeChallenge(x: string) {
|
||||
if (x === undefined || x === "") {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return base64url.stringify(
|
||||
new Uint8Array(
|
||||
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(x))
|
||||
),
|
||||
{
|
||||
pad: false,
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
367
pro/src/settingsBox.ts
Normal file
367
pro/src/settingsBox.ts
Normal file
@ -0,0 +1,367 @@
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { type App, Modal, Notice, 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_BOX_CONFIG,
|
||||
generateAuthUrl,
|
||||
sendRefreshTokenReq,
|
||||
} from "./fsBox";
|
||||
|
||||
class BoxAuthModal extends Modal {
|
||||
readonly plugin: RemotelySavePlugin;
|
||||
readonly authDiv: HTMLDivElement;
|
||||
readonly revokeAuthDiv: HTMLDivElement;
|
||||
readonly revokeAuthSetting: Setting;
|
||||
readonly t: (x: TransItemType, vars?: any) => string;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
authDiv: HTMLDivElement,
|
||||
revokeAuthDiv: HTMLDivElement,
|
||||
revokeAuthSetting: Setting,
|
||||
t: (x: TransItemType, vars?: any) => string
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.authDiv = authDiv;
|
||||
this.revokeAuthDiv = revokeAuthDiv;
|
||||
this.revokeAuthSetting = revokeAuthSetting;
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const { contentEl } = this;
|
||||
const t = this.t;
|
||||
|
||||
const authUrl = generateAuthUrl();
|
||||
const div2 = contentEl.createDiv();
|
||||
div2.createDiv({
|
||||
text: stringToFragment(t("modal_boxauth_tutorial")),
|
||||
});
|
||||
div2.createEl(
|
||||
"button",
|
||||
{
|
||||
text: t("modal_boxauth_copybutton"),
|
||||
},
|
||||
(el) => {
|
||||
el.onclick = async () => {
|
||||
await navigator.clipboard.writeText(authUrl);
|
||||
new Notice(t("modal_boxauth_copynotice"));
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: authUrl,
|
||||
text: authUrl,
|
||||
});
|
||||
|
||||
// let refreshToken = "";
|
||||
// new Setting(contentEl)
|
||||
// .setName(t("modal_box_maualinput"))
|
||||
// .setDesc(t("modal_box_maualinput_desc"))
|
||||
// .addText((text) =>
|
||||
// text
|
||||
// .setPlaceholder("")
|
||||
// .setValue("")
|
||||
// .onChange((val) => {
|
||||
// refreshToken = val.trim();
|
||||
// })
|
||||
// )
|
||||
// .addButton(async (button) => {
|
||||
// button.setButtonText(t("submit"));
|
||||
// button.onClick(async () => {
|
||||
// new Notice(t("modal_box_maualinput_notice"));
|
||||
|
||||
// try {
|
||||
// if (this.plugin.settings.box === undefined) {
|
||||
// this.plugin.settings.box = cloneDeep(
|
||||
// DEFAULT_BOX_CONFIG
|
||||
// );
|
||||
// }
|
||||
// this.plugin.settings.box.refreshToken = refreshToken;
|
||||
// this.plugin.settings.box.accessToken = "access";
|
||||
// this.plugin.settings.box.accessTokenExpiresAtTimeMs = 1;
|
||||
// this.plugin.settings.box.accessTokenExpiresInMs = 1;
|
||||
|
||||
// // TODO: abstraction leaking now, how to fix?
|
||||
// const k = await sendRefreshTokenReq(refreshToken);
|
||||
// const ts = Date.now();
|
||||
// this.plugin.settings.box.accessToken = k.access_token;
|
||||
// this.plugin.settings.box.accessTokenExpiresInMs =
|
||||
// k.expires_in * 1000;
|
||||
// this.plugin.settings.box.accessTokenExpiresAtTimeMs =
|
||||
// ts + k.expires_in * 1000 - 60 * 2 * 1000;
|
||||
// await this.plugin.saveSettings();
|
||||
|
||||
// // try to remove data in clipboard
|
||||
// await navigator.clipboard.writeText("");
|
||||
|
||||
// new Notice(t("modal_box_maualinput_succ_notice"));
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// new Notice(t("modal_box_maualinput_fail_notice"));
|
||||
// } finally {
|
||||
// this.authDiv.toggleClass(
|
||||
// "box-auth-button-hide",
|
||||
// this.plugin.settings.box.refreshToken !== ""
|
||||
// );
|
||||
// this.revokeAuthDiv.toggleClass(
|
||||
// "box-revoke-auth-button-hide",
|
||||
// this.plugin.settings.box.refreshToken === ""
|
||||
// );
|
||||
// this.close();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class BoxRevokeAuthModal extends Modal {
|
||||
readonly plugin: RemotelySavePlugin;
|
||||
readonly authDiv: HTMLDivElement;
|
||||
readonly revokeAuthDiv: HTMLDivElement;
|
||||
readonly t: (x: TransItemType, vars?: any) => string;
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
authDiv: HTMLDivElement,
|
||||
revokeAuthDiv: HTMLDivElement,
|
||||
t: (x: TransItemType, vars?: any) => string
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.authDiv = authDiv;
|
||||
this.revokeAuthDiv = revokeAuthDiv;
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const t = this.t;
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: t("modal_boxrevokeauth_step1"),
|
||||
});
|
||||
const consentUrl = "https://app.box.com/account/security";
|
||||
contentEl.createEl("p").createEl("a", {
|
||||
href: consentUrl,
|
||||
text: consentUrl,
|
||||
});
|
||||
|
||||
contentEl.createEl("p", {
|
||||
text: t("modal_boxrevokeauth_step2"),
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName(t("modal_boxrevokeauth_clean"))
|
||||
.setDesc(t("modal_boxrevokeauth_clean_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("modal_boxrevokeauth_clean_button"));
|
||||
button.onClick(async () => {
|
||||
try {
|
||||
this.plugin.settings.box = cloneDeep(DEFAULT_BOX_CONFIG);
|
||||
|
||||
await this.plugin.saveSettings();
|
||||
this.authDiv.toggleClass(
|
||||
"box-auth-button-hide",
|
||||
this.plugin.settings.box.refreshToken !== ""
|
||||
);
|
||||
this.revokeAuthDiv.toggleClass(
|
||||
"box-revoke-auth-button-hide",
|
||||
this.plugin.settings.box.refreshToken === ""
|
||||
);
|
||||
new Notice(t("modal_boxrevokeauth_clean_notice"));
|
||||
this.close();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
new Notice(t("modal_boxrevokeauth_clean_fail"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
export const generateBoxSettingsPart = (
|
||||
containerEl: HTMLElement,
|
||||
t: (x: TransItemType, vars?: any) => string,
|
||||
app: App,
|
||||
plugin: RemotelySavePlugin,
|
||||
saveUpdatedConfigFunc: () => Promise<any> | undefined
|
||||
) => {
|
||||
const boxDiv = containerEl.createEl("div", {
|
||||
cls: "box-hide",
|
||||
});
|
||||
boxDiv.toggleClass("box-hide", plugin.settings.serviceType !== "box");
|
||||
boxDiv.createEl("h2", { text: t("settings_box") });
|
||||
|
||||
const boxLongDescDiv = boxDiv.createEl("div", {
|
||||
cls: "settings-long-desc",
|
||||
});
|
||||
for (const c of [
|
||||
t("settings_box_disclaimer1"),
|
||||
t("settings_box_disclaimer2"),
|
||||
]) {
|
||||
boxLongDescDiv.createEl("p", {
|
||||
text: c,
|
||||
cls: "box-disclaimer",
|
||||
});
|
||||
}
|
||||
|
||||
boxLongDescDiv.createEl("p", {
|
||||
text: t("settings_box_folder", {
|
||||
remoteBaseDir: plugin.settings.box.remoteBaseDir || app.vault.getName(),
|
||||
}),
|
||||
});
|
||||
|
||||
boxLongDescDiv.createDiv({
|
||||
text: stringToFragment(t("settings_box_pro_desc")),
|
||||
cls: "box-disclaimer",
|
||||
});
|
||||
|
||||
const boxNotShowUpHintSetting = new Setting(boxDiv)
|
||||
.setName(t("settings_box_notshowuphint"))
|
||||
.setDesc(t("settings_box_notshowuphint_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_box_notshowuphint_view_pro"));
|
||||
button.onClick(async () => {
|
||||
window.location.href = "#settings-pro";
|
||||
});
|
||||
});
|
||||
|
||||
const boxAllowedToUsedDiv = boxDiv.createDiv();
|
||||
// if pro enabled, show up; otherwise hide.
|
||||
const allowBox =
|
||||
plugin.settings.pro?.enabledProFeatures.filter(
|
||||
(x) => x.featureName === "feature-box"
|
||||
).length === 1;
|
||||
console.debug(`allow to show up box settings? ${allowBox}`);
|
||||
if (allowBox) {
|
||||
boxAllowedToUsedDiv.removeClass("box-allow-to-use-hide");
|
||||
boxNotShowUpHintSetting.settingEl.addClass("box-allow-to-use-hide");
|
||||
} else {
|
||||
boxAllowedToUsedDiv.addClass("box-allow-to-use-hide");
|
||||
boxNotShowUpHintSetting.settingEl.removeClass("box-allow-to-use-hide");
|
||||
}
|
||||
|
||||
const boxSelectAuthDiv = boxAllowedToUsedDiv.createDiv();
|
||||
const boxAuthDiv = boxSelectAuthDiv.createDiv({
|
||||
cls: "box-auth-button-hide settings-auth-related",
|
||||
});
|
||||
const boxRevokeAuthDiv = boxSelectAuthDiv.createDiv({
|
||||
cls: "box-revoke-auth-button-hide settings-auth-related",
|
||||
});
|
||||
|
||||
const boxRevokeAuthSetting = new Setting(boxRevokeAuthDiv)
|
||||
.setName(t("settings_box_revoke"))
|
||||
.setDesc(t("settings_box_revoke_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_box_revoke_button"));
|
||||
button.onClick(async () => {
|
||||
new BoxRevokeAuthModal(
|
||||
app,
|
||||
plugin,
|
||||
boxAuthDiv,
|
||||
boxRevokeAuthDiv,
|
||||
t
|
||||
).open();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(boxAuthDiv)
|
||||
.setName(t("settings_box_auth"))
|
||||
.setDesc(t("settings_box_auth_desc"))
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_box_auth_button"));
|
||||
button.onClick(async () => {
|
||||
const modal = new BoxAuthModal(
|
||||
app,
|
||||
plugin,
|
||||
boxAuthDiv,
|
||||
boxRevokeAuthDiv,
|
||||
boxRevokeAuthSetting,
|
||||
t
|
||||
);
|
||||
plugin.oauth2Info.helperModal = modal;
|
||||
plugin.oauth2Info.authDiv = boxAuthDiv;
|
||||
plugin.oauth2Info.revokeDiv = boxRevokeAuthDiv;
|
||||
plugin.oauth2Info.revokeAuthSetting = boxRevokeAuthSetting;
|
||||
modal.open();
|
||||
});
|
||||
});
|
||||
|
||||
boxAuthDiv.toggleClass(
|
||||
"box-auth-button-hide",
|
||||
plugin.settings.box.refreshToken !== ""
|
||||
);
|
||||
boxRevokeAuthDiv.toggleClass(
|
||||
"box-revoke-auth-button-hide",
|
||||
plugin.settings.box.refreshToken === ""
|
||||
);
|
||||
|
||||
let newboxRemoteBaseDir = plugin.settings.box.remoteBaseDir || "";
|
||||
new Setting(boxAllowedToUsedDiv)
|
||||
.setName(t("settings_remotebasedir"))
|
||||
.setDesc(t("settings_remotebasedir_desc"))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder(app.vault.getName())
|
||||
.setValue(newboxRemoteBaseDir)
|
||||
.onChange((value) => {
|
||||
newboxRemoteBaseDir = value.trim();
|
||||
})
|
||||
)
|
||||
.addButton((button) => {
|
||||
button.setButtonText(t("confirm"));
|
||||
button.onClick(() => {
|
||||
new ChangeRemoteBaseDirModal(
|
||||
app,
|
||||
plugin,
|
||||
newboxRemoteBaseDir,
|
||||
"box"
|
||||
).open();
|
||||
});
|
||||
});
|
||||
new Setting(boxAllowedToUsedDiv)
|
||||
.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_box_connect_succ"));
|
||||
} else {
|
||||
new Notice(t("settings_box_connect_fail"));
|
||||
new Notice(errors.msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
boxDiv: boxDiv,
|
||||
boxAllowedToUsedDiv: boxAllowedToUsedDiv,
|
||||
boxNotShowUpHintSetting: boxNotShowUpHintSetting,
|
||||
};
|
||||
};
|
||||
@ -3,7 +3,11 @@
|
||||
* To avoid circular dependency.
|
||||
*/
|
||||
|
||||
import type { GoogleDriveConfig, ProConfig } from "../pro/src/baseTypesPro";
|
||||
import type {
|
||||
BoxConfig,
|
||||
GoogleDriveConfig,
|
||||
ProConfig,
|
||||
} from "../pro/src/baseTypesPro";
|
||||
import type { LangTypeAndAuto } from "./i18n";
|
||||
|
||||
export const DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
||||
@ -14,14 +18,16 @@ export type SUPPORTED_SERVICES_TYPE =
|
||||
| "dropbox"
|
||||
| "onedrive"
|
||||
| "webdis"
|
||||
| "googledrive";
|
||||
| "googledrive"
|
||||
| "box";
|
||||
|
||||
export type SUPPORTED_SERVICES_TYPE_WITH_REMOTE_BASE_DIR =
|
||||
| "webdav"
|
||||
| "dropbox"
|
||||
| "onedrive"
|
||||
| "webdis"
|
||||
| "googledrive";
|
||||
| "googledrive"
|
||||
| "box";
|
||||
|
||||
export interface S3Config {
|
||||
s3Endpoint: string;
|
||||
@ -116,7 +122,8 @@ export type QRExportType =
|
||||
| "onedrive"
|
||||
| "webdav"
|
||||
| "webdis"
|
||||
| "googledrive";
|
||||
| "googledrive"
|
||||
| "box";
|
||||
|
||||
export interface ProfilerConfig {
|
||||
enablePrinting?: boolean;
|
||||
@ -130,6 +137,7 @@ export interface RemotelySavePluginSettings {
|
||||
onedrive: OnedriveConfig;
|
||||
webdis: WebdisConfig;
|
||||
googledrive: GoogleDriveConfig;
|
||||
box: BoxConfig;
|
||||
password: string;
|
||||
serviceType: SUPPORTED_SERVICES_TYPE;
|
||||
currLogLevel?: string;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { FakeFsBox } from "../pro/src/fsBox";
|
||||
import { FakeFsGoogleDrive } from "../pro/src/fsGoogleDrive";
|
||||
import type { RemotelySavePluginSettings } from "./baseTypes";
|
||||
import type { FakeFs } from "./fsAll";
|
||||
@ -48,6 +49,8 @@ export function getClient(
|
||||
vaultName,
|
||||
saveUpdatedConfigFunc
|
||||
);
|
||||
case "box":
|
||||
return new FakeFsBox(settings.box, vaultName, saveUpdatedConfigFunc);
|
||||
default:
|
||||
throw Error(`cannot init client for serviceType=${settings.serviceType}`);
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ export const exportQrCodeUri = async (
|
||||
delete settings2.webdav;
|
||||
delete settings2.webdis;
|
||||
delete settings2.googledrive;
|
||||
delete settings2.box;
|
||||
delete settings2.pro;
|
||||
} else if (exportFields === "s3") {
|
||||
settings2 = { s3: cloneDeep(settings.s3) };
|
||||
@ -38,6 +39,8 @@ export const exportQrCodeUri = async (
|
||||
settings2 = { webdis: cloneDeep(settings.webdis) };
|
||||
} else if (exportFields === "googledrive") {
|
||||
settings2 = { googledrive: cloneDeep(settings.googledrive) };
|
||||
} else if (exportFields === "box") {
|
||||
settings2 = { box: cloneDeep(settings.box) };
|
||||
}
|
||||
|
||||
delete settings2.vaultRandomID;
|
||||
|
||||
92
src/main.ts
92
src/main.ts
@ -63,7 +63,16 @@ import { SyncAlgoV3Modal } from "./syncAlgoV3Notice";
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||
import AggregateError from "aggregate-error";
|
||||
import throttle from "lodash/throttle";
|
||||
import { COMMAND_CALLBACK_PRO } from "../pro/src/baseTypesPro";
|
||||
import {
|
||||
COMMAND_CALLBACK_BOX,
|
||||
COMMAND_CALLBACK_PRO,
|
||||
} from "../pro/src/baseTypesPro";
|
||||
import {
|
||||
DEFAULT_BOX_CONFIG,
|
||||
FakeFsBox,
|
||||
sendAuthReq as sendAuthReqBox,
|
||||
setConfigBySuccessfullAuthInplace as setConfigBySuccessfullAuthInplaceBox,
|
||||
} from "../pro/src/fsBox";
|
||||
import { DEFAULT_GOOGLEDRIVE_CONFIG } from "../pro/src/fsGoogleDrive";
|
||||
import { exportVaultSyncPlansToFiles } from "./debugMode";
|
||||
import { FakeFsEncrypt } from "./fsEncrypt";
|
||||
@ -81,6 +90,7 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
onedrive: DEFAULT_ONEDRIVE_CONFIG,
|
||||
webdis: DEFAULT_WEBDIS_CONFIG,
|
||||
googledrive: DEFAULT_GOOGLEDRIVE_CONFIG,
|
||||
box: DEFAULT_BOX_CONFIG,
|
||||
password: "",
|
||||
serviceType: "s3",
|
||||
currLogLevel: "info",
|
||||
@ -782,6 +792,62 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
}
|
||||
);
|
||||
|
||||
this.registerObsidianProtocolHandler(
|
||||
COMMAND_CALLBACK_BOX,
|
||||
async (inputParams) => {
|
||||
if (this.oauth2Info.helperModal !== undefined) {
|
||||
const k = this.oauth2Info.helperModal.contentEl;
|
||||
k.empty();
|
||||
|
||||
t("protocol_box_connecting")
|
||||
.split("\n")
|
||||
.forEach((val) => {
|
||||
k.createEl("p", {
|
||||
text: val,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.debug(inputParams);
|
||||
const authRes = await sendAuthReqBox(
|
||||
inputParams.code,
|
||||
async (e: any) => {
|
||||
new Notice(t("protocol_box_connect_fail"));
|
||||
new Notice(`${e}`);
|
||||
throw e;
|
||||
}
|
||||
);
|
||||
console.debug(authRes);
|
||||
|
||||
const self = this;
|
||||
await setConfigBySuccessfullAuthInplaceBox(
|
||||
this.settings.box!,
|
||||
authRes,
|
||||
() => self.saveSettings()
|
||||
);
|
||||
|
||||
this.oauth2Info.verifier = ""; // reset it
|
||||
this.oauth2Info.helperModal?.close(); // close it
|
||||
this.oauth2Info.helperModal = undefined;
|
||||
|
||||
this.oauth2Info.authDiv?.toggleClass(
|
||||
"box-auth-button-hide",
|
||||
this.settings.box?.refreshToken !== ""
|
||||
);
|
||||
this.oauth2Info.authDiv = undefined;
|
||||
|
||||
this.oauth2Info.revokeAuthSetting?.setDesc(
|
||||
t("protocol_box_connect_succ_revoke")
|
||||
);
|
||||
this.oauth2Info.revokeAuthSetting = undefined;
|
||||
this.oauth2Info.revokeDiv?.toggleClass(
|
||||
"box-revoke-auth-button-hide",
|
||||
this.settings.box?.refreshToken === ""
|
||||
);
|
||||
this.oauth2Info.revokeDiv = undefined;
|
||||
}
|
||||
);
|
||||
|
||||
this.syncRibbon = this.addRibbonIcon(
|
||||
iconNameSyncWait,
|
||||
`${this.manifest.name}`,
|
||||
@ -1068,6 +1134,10 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
this.settings.googledrive = DEFAULT_GOOGLEDRIVE_CONFIG;
|
||||
}
|
||||
|
||||
if (this.settings.box === undefined) {
|
||||
this.settings.box = DEFAULT_BOX_CONFIG;
|
||||
}
|
||||
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
@ -1136,6 +1206,26 @@ export default class RemotelySavePlugin extends Plugin {
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
let googleDriveExpired = false;
|
||||
if (
|
||||
this.settings.googledrive.refreshToken !== "" &&
|
||||
current >= this.settings!.googledrive!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
googleDriveExpired = true;
|
||||
this.settings.googledrive = cloneDeep(DEFAULT_GOOGLEDRIVE_CONFIG);
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
let boxExpired = false;
|
||||
if (
|
||||
this.settings.box.refreshToken !== "" &&
|
||||
current >= this.settings!.box!.credentialsShouldBeDeletedAtTimeMs!
|
||||
) {
|
||||
boxExpired = true;
|
||||
this.settings.box = cloneDeep(DEFAULT_BOX_CONFIG);
|
||||
needSave = true;
|
||||
}
|
||||
|
||||
if (this.settings.pro === undefined) {
|
||||
this.settings.pro = cloneDeep(DEFAULT_PRO_CONFIG);
|
||||
}
|
||||
|
||||
11
src/misc.ts
11
src/misc.ts
@ -716,3 +716,14 @@ export const splitFileSizeToChunkRanges = (
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getSha1 = async (x: ArrayBuffer, stringify: "base64" | "hex") => {
|
||||
const y = await window.crypto.subtle.digest("SHA-1", x);
|
||||
|
||||
if (stringify === "base64") {
|
||||
return arrayBufferToBase64(y);
|
||||
} else if (stringify === "hex") {
|
||||
return arrayBufferToHex(y);
|
||||
}
|
||||
throw Error(`not supported stringify option = ${stringify}`);
|
||||
};
|
||||
|
||||
@ -21,6 +21,7 @@ import type {
|
||||
} from "./baseTypes";
|
||||
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import { generateBoxSettingsPart } from "../pro/src/settingsBox";
|
||||
import { generateGoogleDriveSettingsPart } from "../pro/src/settingsGoogleDrive";
|
||||
import { generateProSettingsPart } from "../pro/src/settingsPro";
|
||||
import { API_VER_ENSURE_REQURL_OK, VALID_REQURL } from "./baseTypesObs";
|
||||
@ -1808,6 +1809,15 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
() => this.plugin.saveSettings()
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// below for box
|
||||
//////////////////////////////////////////////////
|
||||
|
||||
const { boxDiv, boxAllowedToUsedDiv, boxNotShowUpHintSetting } =
|
||||
generateBoxSettingsPart(containerEl, t, this.app, this.plugin, () =>
|
||||
this.plugin.saveSettings()
|
||||
);
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// below for general chooser (part 2/2)
|
||||
//////////////////////////////////////////////////
|
||||
@ -1827,6 +1837,7 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
"googledrive",
|
||||
t("settings_chooseservice_googledrive")
|
||||
);
|
||||
dropdown.addOption("box", t("settings_chooseservice_box"));
|
||||
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.serviceType)
|
||||
@ -1856,6 +1867,10 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
"googledrive-hide",
|
||||
this.plugin.settings.serviceType !== "googledrive"
|
||||
);
|
||||
boxDiv.toggleClass(
|
||||
"box-hide",
|
||||
this.plugin.settings.serviceType !== "box"
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
@ -2418,6 +2433,12 @@ export class RemotelySaveSettingTab extends PluginSettingTab {
|
||||
"googledrive"
|
||||
).open();
|
||||
});
|
||||
})
|
||||
.addButton(async (button) => {
|
||||
button.setButtonText(t("settings_export_box_button"));
|
||||
button.onClick(async () => {
|
||||
new ExportSettingsQrCodeModal(this.app, this.plugin, "box").open();
|
||||
});
|
||||
});
|
||||
|
||||
let importSettingVal = "";
|
||||
|
||||
@ -1507,12 +1507,7 @@ export async function syncer(
|
||||
try {
|
||||
// check pro feature
|
||||
// if anything goes wrong, it will throw
|
||||
await checkProRunnableAndFixInplace(
|
||||
["feature-smart_conflict", "feature-google_drive"],
|
||||
settings,
|
||||
pluginVersion,
|
||||
configSaver
|
||||
);
|
||||
await checkProRunnableAndFixInplace(settings, pluginVersion, configSaver);
|
||||
|
||||
// try mode?
|
||||
await notifyFunc?.(triggerSource, step);
|
||||
|
||||
19
styles.css
19
styles.css
@ -91,6 +91,25 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.box-disclaimer {
|
||||
font-weight: bold;
|
||||
}
|
||||
.box-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.box-allow-to-use-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.box-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.box-revoke-auth-button-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.qrcode-img {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
|
||||
@ -22,6 +22,9 @@ const DEFAULT_SETTINGS: RemotelySavePluginSettings = {
|
||||
googledrive: {
|
||||
refreshToken: "xxx",
|
||||
} as any,
|
||||
box: {
|
||||
refreshToken: "xxx",
|
||||
} as any,
|
||||
password: "password",
|
||||
serviceType: "s3",
|
||||
currLogLevel: "info",
|
||||
|
||||
@ -11,6 +11,8 @@ const DEFAULT_REMOTELYSAVE_CLIENT_ID = process.env.REMOTELYSAVE_CLIENT_ID || "";
|
||||
const DEFAULT_GOOGLEDRIVE_CLIENT_ID = process.env.GOOGLEDRIVE_CLIENT_ID || "";
|
||||
const DEFAULT_GOOGLEDRIVE_CLIENT_SECRET =
|
||||
process.env.GOOGLEDRIVE_CLIENT_SECRET || "";
|
||||
const DEFAULT_BOX_CLIENT_ID = process.env.BOX_CLIENT_ID || "";
|
||||
const DEFAULT_BOX_CLIENT_SECRET = process.env.BOX_CLIENT_SECRET || "";
|
||||
|
||||
module.exports = {
|
||||
entry: "./src/main.ts",
|
||||
@ -29,6 +31,8 @@ module.exports = {
|
||||
"process.env.DEFAULT_REMOTELYSAVE_CLIENT_ID": `"${DEFAULT_REMOTELYSAVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_ID": `"${DEFAULT_GOOGLEDRIVE_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_GOOGLEDRIVE_CLIENT_SECRET": `"${DEFAULT_GOOGLEDRIVE_CLIENT_SECRET}"`,
|
||||
"process.env.DEFAULT_BOX_CLIENT_ID": `"${DEFAULT_BOX_CLIENT_ID}"`,
|
||||
"process.env.DEFAULT_BOX_CLIENT_SECRET": `"${DEFAULT_BOX_CLIENT_SECRET}"`,
|
||||
}),
|
||||
// Work around for Buffer is undefined:
|
||||
// https://github.com/webpack/changelog-v5/issues/10
|
||||
|
||||
Loading…
Reference in New Issue
Block a user