remotely-save/pro/src/fsBox.ts
2024-06-14 00:41:50 +08:00

927 lines
27 KiB
TypeScript

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 { BoxDeveloperTokenAuth } from "box-typescript-sdk-gen/lib/box/developerTokenAuth.generated";
import { BoxClient } from "box-typescript-sdk-gen/lib/client.generated";
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;
this.boxConfig.refreshToken =
k.refresh_token || this.boxConfig.refreshToken;
await this.saveUpdatedConfigFunc();
console.info("Box accessToken updated");
return this.boxConfig.accessToken;
}
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;
}
}