添加filesystem
This commit is contained in:
parent
3b2e72127f
commit
b76a685988
8
packages/filesystem/README.md
Normal file
8
packages/filesystem/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# 文件系统
|
||||
|
||||
用于同步和备份至云端
|
||||
|
||||
- zip
|
||||
- webdav
|
||||
- 百度网盘
|
||||
- onedrive
|
114
packages/filesystem/auth.ts
Normal file
114
packages/filesystem/auth.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/* eslint-disable camelcase */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { ExtServer } from "@App/app/const";
|
||||
import { api } from "@App/pkg/axios";
|
||||
import { WarpTokenError } from "./error";
|
||||
|
||||
type NetDiskType = "baidu" | "onedrive";
|
||||
|
||||
export function GetNetDiskToken(netDiskType: NetDiskType): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { token: { access_token: string; refresh_token: string } };
|
||||
}> {
|
||||
return api
|
||||
.get(`/auth/net-disk/token?netDiskType=${netDiskType}`)
|
||||
.then((resp) => {
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
export function RefreshToken(
|
||||
netDiskType: NetDiskType,
|
||||
refreshToken: string
|
||||
): Promise<{
|
||||
code: number;
|
||||
msg: string;
|
||||
data: { token: { access_token: string; refresh_token: string } };
|
||||
}> {
|
||||
return api
|
||||
.post(`/auth/net-disk/token/refresh?netDiskType=${netDiskType}`, {
|
||||
netDiskType,
|
||||
refreshToken,
|
||||
})
|
||||
.then((resp) => {
|
||||
return resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
export function NetDisk(netDiskType: NetDiskType) {
|
||||
return new Promise<void>((resolve) => {
|
||||
const loginWindow = window.open(
|
||||
`${ExtServer}api/v1/auth/net-disk?netDiskType=${netDiskType}`
|
||||
);
|
||||
const t = setInterval(() => {
|
||||
try {
|
||||
if (loginWindow!.closed) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
clearInterval(t);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
export type Token = {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
createtime: number;
|
||||
};
|
||||
|
||||
export async function AuthVerify(netDiskType: NetDiskType, invalid?: boolean) {
|
||||
let token: Token | undefined;
|
||||
try {
|
||||
token = JSON.parse(localStorage[`netdisk:token:${netDiskType}`]);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
// token不存在,或者没有accessToken,重新获取
|
||||
if (!token || !token.accessToken) {
|
||||
// 强制重新获取token
|
||||
await NetDisk(netDiskType);
|
||||
const resp = await GetNetDiskToken(netDiskType);
|
||||
if (resp.code !== 0) {
|
||||
return Promise.reject(new WarpTokenError(new Error(resp.msg)));
|
||||
}
|
||||
token = {
|
||||
accessToken: resp.data.token.access_token,
|
||||
refreshToken: resp.data.token.refresh_token,
|
||||
createtime: Date.now(),
|
||||
};
|
||||
invalid = false;
|
||||
localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token);
|
||||
}
|
||||
// token过期或者失效
|
||||
if (Date.now() >= token.createtime + 3600000 || invalid) {
|
||||
// 大于一小时刷新token
|
||||
try {
|
||||
const resp = await RefreshToken(netDiskType, token.refreshToken);
|
||||
if (resp.code !== 0) {
|
||||
localStorage.removeItem(`netdisk:token:${netDiskType}`);
|
||||
// 刷新失败,并且标记失效,尝试重新获取token
|
||||
if (invalid) {
|
||||
return AuthVerify(netDiskType);
|
||||
}
|
||||
return Promise.reject(new WarpTokenError(new Error(resp.msg)));
|
||||
}
|
||||
token = {
|
||||
accessToken: resp.data.token.access_token,
|
||||
refreshToken: resp.data.token.refresh_token,
|
||||
createtime: Date.now(),
|
||||
};
|
||||
localStorage[`netdisk:token:${netDiskType}`] = JSON.stringify(token);
|
||||
} catch (e) {
|
||||
// 报错返回原token
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
||||
} else {
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
||||
return Promise.resolve(token.accessToken);
|
||||
}
|
154
packages/filesystem/baidu/baidu.ts
Normal file
154
packages/filesystem/baidu/baidu.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import IoC from "@App/app/ioc";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { AuthVerify } from "../auth";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { BaiduFileReader, BaiduFileWriter } from "./rw";
|
||||
|
||||
export default class BaiduFileSystem implements FileSystem {
|
||||
accessToken?: string;
|
||||
|
||||
path: string;
|
||||
|
||||
systemConfig: SystemConfig;
|
||||
|
||||
constructor(path?: string, accessToken?: string) {
|
||||
this.path = path || "/apps";
|
||||
this.accessToken = accessToken;
|
||||
this.systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
const token = await AuthVerify("baidu");
|
||||
this.accessToken = token;
|
||||
return this.list().then();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
// 获取fsid
|
||||
return Promise.resolve(new BaiduFileReader(this, file));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(
|
||||
new BaiduFileSystem(joinPath(this.path, path), this.accessToken)
|
||||
);
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(
|
||||
new BaiduFileWriter(this, joinPath(this.path, path))
|
||||
);
|
||||
}
|
||||
|
||||
createDir(dir: string): Promise<void> {
|
||||
dir = joinPath(this.path, dir);
|
||||
const urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", dir);
|
||||
urlencoded.append("size", "0");
|
||||
urlencoded.append("isdir", "1");
|
||||
urlencoded.append("rtype", "3");
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
return this.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${this.accessToken}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
redirect: "follow",
|
||||
}
|
||||
).then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
request(url: string, config?: RequestInit) {
|
||||
config = config || {};
|
||||
const headers = <Headers>config.headers || new Headers();
|
||||
// 利用GM函数的匿名实现不发送cookie,因为某些情况cookie会导致-6错误
|
||||
headers.append(`${this.systemConfig.scriptCatFlag}-gm-xhr`, "true");
|
||||
headers.append(`${this.systemConfig.scriptCatFlag}-anonymous`, "true");
|
||||
config.headers = headers;
|
||||
return fetch(url, config)
|
||||
.then((data) => data.json())
|
||||
.then(async (data) => {
|
||||
if (data.errno === 111 || data.errno === -6) {
|
||||
const token = await AuthVerify("baidu", true);
|
||||
this.accessToken = token;
|
||||
url = url.replace(/access_token=[^&]+/, `access_token=${token}`);
|
||||
return fetch(url, config)
|
||||
.then((data2) => data2.json())
|
||||
.then((data2) => {
|
||||
if (data2.errno === 111 || data2.errno === -6) {
|
||||
throw new Error(JSON.stringify(data2));
|
||||
}
|
||||
return data2;
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
const filelist = [joinPath(this.path, path)];
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
return this.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=filemanager&access_token=${this.accessToken}&opera=delete`,
|
||||
{
|
||||
method: "POST",
|
||||
body: `async=0&filelist=${encodeURIComponent(
|
||||
JSON.stringify(filelist)
|
||||
)}`,
|
||||
headers: myHeaders,
|
||||
}
|
||||
).then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
return this.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=list&dir=${encodeURIComponent(
|
||||
this.path
|
||||
)}&order=time&access_token=${this.accessToken}`
|
||||
).then((data) => {
|
||||
if (data.errno) {
|
||||
if (data.errno === -9) {
|
||||
return [];
|
||||
}
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
const list: File[] = [];
|
||||
data.list.forEach((val: any) => {
|
||||
list.push({
|
||||
fsid: val.fs_id,
|
||||
name: val.server_filename,
|
||||
path: this.path,
|
||||
size: val.size,
|
||||
digest: val.md5,
|
||||
createtime: val.server_ctime * 1000,
|
||||
updatetime: val.server_mtime * 1000,
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
return Promise.resolve(
|
||||
`https://pan.baidu.com/disk/main#/index?category=all&path=${encodeURIComponent(
|
||||
this.path
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
144
packages/filesystem/baidu/rw.ts
Normal file
144
packages/filesystem/baidu/rw.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||
import { MD5 } from "crypto-js";
|
||||
import { File, FileReader, FileWriter } from "../filesystem";
|
||||
import BaiduFileSystem from "./baidu";
|
||||
|
||||
export class BaiduFileReader implements FileReader {
|
||||
file: File;
|
||||
|
||||
fs: BaiduFileSystem;
|
||||
|
||||
constructor(fs: BaiduFileSystem, file: File) {
|
||||
this.fs = fs;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
// 查询文件信息获取dlink
|
||||
const data = await this.fs.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/multimedia?method=filemetas&access_token=${
|
||||
this.fs.accessToken
|
||||
}&fsids=[${this.file.fsid!}]&dlink=1`
|
||||
);
|
||||
if (!data.list.length) {
|
||||
return Promise.reject(new Error("file not found"));
|
||||
}
|
||||
switch (type) {
|
||||
case "string":
|
||||
return fetch(
|
||||
`${data.list[0].dlink}&access_token=${this.fs.accessToken}`
|
||||
).then((resp) => resp.text());
|
||||
default: {
|
||||
return fetch(
|
||||
`${data.list[0].dlink}&access_token=${this.fs.accessToken}`
|
||||
).then((resp) => resp.blob());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BaiduFileWriter implements FileWriter {
|
||||
path: string;
|
||||
|
||||
fs: BaiduFileSystem;
|
||||
|
||||
constructor(fs: BaiduFileSystem, path: string) {
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
size(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return content.size;
|
||||
}
|
||||
return new Blob([content]).size;
|
||||
}
|
||||
|
||||
async md5(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return calculateMd5(content);
|
||||
}
|
||||
return MD5(content).toString();
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
// 预上传获取id
|
||||
const size = this.size(content).toString();
|
||||
const md5 = await this.md5(content);
|
||||
const blockList: string[] = [md5];
|
||||
let urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", this.path);
|
||||
urlencoded.append("size", size);
|
||||
urlencoded.append("isdir", "0");
|
||||
urlencoded.append("autoinit", "1");
|
||||
urlencoded.append("rtype", "3");
|
||||
urlencoded.append("block_list", JSON.stringify(blockList));
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
|
||||
const uploadid = await this.fs
|
||||
.request(
|
||||
`http://pan.baidu.com/rest/2.0/xpan/file?method=precreate&access_token=${this.fs.accessToken}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data.uploadid;
|
||||
});
|
||||
const body = new FormData();
|
||||
if (content instanceof Blob) {
|
||||
// 分片上传
|
||||
body.append("file", content);
|
||||
} else {
|
||||
body.append("file", new Blob([content]));
|
||||
}
|
||||
|
||||
await this.fs
|
||||
.request(
|
||||
`${
|
||||
`https://d.pcs.baidu.com/rest/2.0/pcs/superfile2?method=upload&access_token=${this.fs.accessToken}` +
|
||||
`&type=tmpfile&path=`
|
||||
}${encodeURIComponent(this.path)}&uploadid=${uploadid}&partseq=0`,
|
||||
{
|
||||
method: "POST",
|
||||
body,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
// 创建文件
|
||||
urlencoded = new URLSearchParams();
|
||||
urlencoded.append("path", this.path);
|
||||
urlencoded.append("size", size);
|
||||
urlencoded.append("isdir", "0");
|
||||
urlencoded.append("block_list", JSON.stringify(blockList));
|
||||
urlencoded.append("uploadid", uploadid);
|
||||
urlencoded.append("rtype", "3");
|
||||
return this.fs
|
||||
.request(
|
||||
`https://pan.baidu.com/rest/2.0/xpan/file?method=create&access_token=${this.fs.accessToken}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: urlencoded,
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
24
packages/filesystem/error.ts
Normal file
24
packages/filesystem/error.ts
Normal file
@ -0,0 +1,24 @@
|
||||
// eslint-disable-next-line import/prefer-default-export, max-classes-per-file
|
||||
export class WarpTokenError {
|
||||
error: Error;
|
||||
|
||||
constructor(error: Error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isWarpTokenError(error: any): error is WarpTokenError {
|
||||
return error instanceof WarpTokenError;
|
||||
}
|
||||
|
||||
export class WarpNetworkError {
|
||||
error: Error;
|
||||
|
||||
constructor(error: Error) {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNetworkError(error: any): error is WarpNetworkError {
|
||||
return error instanceof WarpNetworkError;
|
||||
}
|
84
packages/filesystem/factory.ts
Normal file
84
packages/filesystem/factory.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import i18next from "i18next";
|
||||
import BaiduFileSystem from "./baidu/baidu";
|
||||
import FileSystem from "./filesystem";
|
||||
import OneDriveFileSystem from "./onedrive/onedrive";
|
||||
import WebDAVFileSystem from "./webdav/webdav";
|
||||
import ZipFileSystem from "./zip/zip";
|
||||
|
||||
export type FileSystemType = "zip" | "webdav" | "baidu-netdsik" | "onedrive";
|
||||
|
||||
export type FileSystemParams = {
|
||||
[key: string]: {
|
||||
title: string;
|
||||
type?: "select" | "authorize" | "password";
|
||||
options?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export default class FileSystemFactory {
|
||||
static create(type: FileSystemType, params: any): Promise<FileSystem> {
|
||||
let fs: FileSystem;
|
||||
switch (type) {
|
||||
case "zip":
|
||||
fs = new ZipFileSystem(params);
|
||||
break;
|
||||
case "webdav":
|
||||
fs = new WebDAVFileSystem(
|
||||
params.authType,
|
||||
params.url,
|
||||
params.username,
|
||||
params.password
|
||||
);
|
||||
break;
|
||||
case "baidu-netdsik":
|
||||
fs = new BaiduFileSystem();
|
||||
break;
|
||||
case "onedrive":
|
||||
fs = new OneDriveFileSystem();
|
||||
break;
|
||||
default:
|
||||
throw new Error("not found filesystem");
|
||||
}
|
||||
return fs.verify().then(() => fs);
|
||||
}
|
||||
|
||||
static params(): { [key: string]: FileSystemParams } {
|
||||
return {
|
||||
webdav: {
|
||||
authType: {
|
||||
title: i18next.t("auth_type"),
|
||||
type: "select",
|
||||
options: ["password", "digest", "none", "token"],
|
||||
},
|
||||
url: { title: i18next.t("url") },
|
||||
username: { title: i18next.t("username") },
|
||||
password: { title: i18next.t("password"), type: "password" },
|
||||
},
|
||||
"baidu-netdsik": {},
|
||||
onedrive: {},
|
||||
};
|
||||
}
|
||||
|
||||
static async mkdirAll(fs: FileSystem, path: string) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const dirs = path.split("/");
|
||||
let i = 0;
|
||||
const mkdir = () => {
|
||||
if (i >= dirs.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const dir = dirs.slice(0, i + 1).join("/");
|
||||
fs.createDir(dir)
|
||||
.then(() => {
|
||||
i += 1;
|
||||
mkdir();
|
||||
})
|
||||
.catch(() => {
|
||||
reject();
|
||||
});
|
||||
};
|
||||
mkdir();
|
||||
});
|
||||
}
|
||||
}
|
48
packages/filesystem/filesystem.ts
Normal file
48
packages/filesystem/filesystem.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export interface File {
|
||||
fsid?: number;
|
||||
// 文件名
|
||||
name: string;
|
||||
// 文件路径
|
||||
path: string;
|
||||
// 文件大小
|
||||
size: number;
|
||||
// 文件摘要
|
||||
digest: string;
|
||||
// 文件创建时间
|
||||
createtime: number;
|
||||
// 文件修改时间
|
||||
updatetime: number;
|
||||
}
|
||||
|
||||
type ReadType = "string" | "blob";
|
||||
export interface FileReader {
|
||||
// 读取文件内容
|
||||
read(type?: ReadType): Promise<any>;
|
||||
}
|
||||
|
||||
export interface FileWriter {
|
||||
// 写入文件内容
|
||||
write(content: string | Blob): Promise<void>;
|
||||
}
|
||||
|
||||
export type FileReadWriter = FileReader & FileWriter;
|
||||
|
||||
// 文件读取
|
||||
export default interface FileSystem {
|
||||
// 授权验证
|
||||
verify(): Promise<void>;
|
||||
// 打开文件
|
||||
open(file: File): Promise<FileReader>;
|
||||
// 打开目录
|
||||
openDir(path: string): Promise<FileSystem>;
|
||||
// 创建文件
|
||||
create(path: string): Promise<FileWriter>;
|
||||
// 创建目录
|
||||
createDir(dir: string): Promise<void>;
|
||||
// 删除文件
|
||||
delete(path: string): Promise<void>;
|
||||
// 文件列表
|
||||
list(): Promise<File[]>;
|
||||
// getDirUrl 获取目录的url
|
||||
getDirUrl(): Promise<string>;
|
||||
}
|
168
packages/filesystem/onedrive/onedrive.ts
Normal file
168
packages/filesystem/onedrive/onedrive.ts
Normal file
@ -0,0 +1,168 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import IoC from "@App/app/ioc";
|
||||
import { SystemConfig } from "@App/pkg/config/config";
|
||||
import { AuthVerify } from "../auth";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { OneDriveFileReader, OneDriveFileWriter } from "./rw";
|
||||
|
||||
export default class OneDriveFileSystem implements FileSystem {
|
||||
accessToken?: string;
|
||||
|
||||
path: string;
|
||||
|
||||
systemConfig: SystemConfig;
|
||||
|
||||
constructor(path?: string, accessToken?: string) {
|
||||
this.path = path || "/";
|
||||
this.accessToken = accessToken;
|
||||
this.systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
const token = await AuthVerify("onedrive");
|
||||
this.accessToken = token;
|
||||
return this.list().then();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
return Promise.resolve(new OneDriveFileReader(this, file));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
if (path.startsWith("ScriptCat")) {
|
||||
path = path.substring(9);
|
||||
}
|
||||
return Promise.resolve(
|
||||
new OneDriveFileSystem(joinPath(this.path, path), this.accessToken)
|
||||
);
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(
|
||||
new OneDriveFileWriter(this, joinPath(this.path, path))
|
||||
);
|
||||
}
|
||||
|
||||
createDir(dir: string): Promise<void> {
|
||||
if (dir && dir.startsWith("ScriptCat")) {
|
||||
dir = dir.substring(9);
|
||||
if (dir.startsWith("/")) {
|
||||
dir = dir.substring(1);
|
||||
}
|
||||
}
|
||||
if (!dir) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
dir = joinPath(this.path, dir);
|
||||
const dirs = dir.split("/");
|
||||
let parent = "";
|
||||
if (dirs.length > 2) {
|
||||
parent = dirs.slice(0, dirs.length - 1).join("/");
|
||||
}
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
if (parent !== "") {
|
||||
parent = `:${parent}:`;
|
||||
}
|
||||
return this.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot${parent}/children`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: JSON.stringify({
|
||||
name: dirs[dirs.length - 1],
|
||||
folder: {},
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
}),
|
||||
}
|
||||
).then((data: any) => {
|
||||
if (data.errno) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
request(url: string, config?: RequestInit, nothen?: boolean) {
|
||||
config = config || {};
|
||||
const headers = <Headers>config.headers || new Headers();
|
||||
if (url.indexOf("uploadSession") === -1) {
|
||||
headers.append(`Authorization`, `Bearer ${this.accessToken}`);
|
||||
}
|
||||
config.headers = headers;
|
||||
const ret = fetch(url, config);
|
||||
if (nothen) {
|
||||
return <Promise<Response>>ret;
|
||||
}
|
||||
return ret
|
||||
.then((data) => data.json())
|
||||
.then(async (data) => {
|
||||
if (data.error) {
|
||||
if (data.error.code === "InvalidAuthenticationToken") {
|
||||
const token = await AuthVerify("onedrive", true);
|
||||
this.accessToken = token;
|
||||
headers.set(`Authorization`, `Bearer ${this.accessToken}`);
|
||||
return fetch(url, config)
|
||||
.then((retryData) => retryData.json())
|
||||
.then((retryData) => {
|
||||
if (retryData.error) {
|
||||
throw new Error(JSON.stringify(retryData));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
return this.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${joinPath(
|
||||
this.path,
|
||||
path
|
||||
)}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
true
|
||||
).then(async (resp) => {
|
||||
if (resp.status !== 204) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
return resp;
|
||||
});
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
let { path } = this;
|
||||
if (path === "/") {
|
||||
path = "";
|
||||
} else {
|
||||
path = `:${path}:`;
|
||||
}
|
||||
return this.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot${path}/children`
|
||||
).then((data) => {
|
||||
const list: File[] = [];
|
||||
data.value.forEach((val: any) => {
|
||||
list.push({
|
||||
name: val.name,
|
||||
path: this.path,
|
||||
size: val.size,
|
||||
digest: val.eTag,
|
||||
createtime: new Date(val.createdDateTime).getTime(),
|
||||
updatetime: new Date(val.lastModifiedDateTime).getTime(),
|
||||
});
|
||||
});
|
||||
return list;
|
||||
});
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
105
packages/filesystem/onedrive/rw.ts
Normal file
105
packages/filesystem/onedrive/rw.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { calculateMd5 } from "@App/pkg/utils/utils";
|
||||
import { MD5 } from "crypto-js";
|
||||
import { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import OneDriveFileSystem from "./onedrive";
|
||||
|
||||
export class OneDriveFileReader implements FileReader {
|
||||
file: File;
|
||||
|
||||
fs: OneDriveFileSystem;
|
||||
|
||||
constructor(fs: OneDriveFileSystem, file: File) {
|
||||
this.fs = fs;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
const data = await this.fs.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${joinPath(
|
||||
this.file.path,
|
||||
this.file.name
|
||||
)}:/content`,
|
||||
{},
|
||||
true
|
||||
);
|
||||
if (data.status !== 200) {
|
||||
return Promise.reject(await data.text());
|
||||
}
|
||||
switch (type) {
|
||||
case "string":
|
||||
return data.text();
|
||||
default: {
|
||||
return data.blob();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OneDriveFileWriter implements FileWriter {
|
||||
path: string;
|
||||
|
||||
fs: OneDriveFileSystem;
|
||||
|
||||
constructor(fs: OneDriveFileSystem, path: string) {
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
size(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return content.size;
|
||||
}
|
||||
return new Blob([content]).size;
|
||||
}
|
||||
|
||||
async md5(content: string | Blob) {
|
||||
if (content instanceof Blob) {
|
||||
return calculateMd5(content);
|
||||
}
|
||||
return MD5(content).toString();
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
// 预上传获取id
|
||||
const size = this.size(content).toString();
|
||||
let myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
const uploadUrl = await this.fs
|
||||
.request(
|
||||
`https://graph.microsoft.com/v1.0/me/drive/special/approot:${this.path}:/createUploadSession`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: myHeaders,
|
||||
body: JSON.stringify({
|
||||
item: {
|
||||
"@microsoft.graph.conflictBehavior": "replace",
|
||||
// description: "description",
|
||||
// fileSystemInfo: {
|
||||
// "@odata.type": "microsoft.graph.fileSystemInfo",
|
||||
// },
|
||||
// name: this.path.substring(this.path.lastIndexOf("/") + 1),
|
||||
},
|
||||
}),
|
||||
}
|
||||
)
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
throw new Error(JSON.stringify(data));
|
||||
}
|
||||
return data.uploadUrl;
|
||||
});
|
||||
myHeaders = new Headers();
|
||||
myHeaders.append(
|
||||
"Content-Range",
|
||||
`bytes 0-${parseInt(size, 10) - 1}/${size}`
|
||||
);
|
||||
return this.fs.request(uploadUrl, {
|
||||
method: "PUT",
|
||||
body: content,
|
||||
headers: myHeaders,
|
||||
});
|
||||
}
|
||||
}
|
18
packages/filesystem/utils.ts
Normal file
18
packages/filesystem/utils.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
export function joinPath(...paths: string[]): string {
|
||||
let path = "";
|
||||
paths.forEach((value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (!value.startsWith("/")) {
|
||||
value = `/${value}`;
|
||||
}
|
||||
if (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length - 1);
|
||||
}
|
||||
path += value;
|
||||
});
|
||||
return path;
|
||||
}
|
57
packages/filesystem/webdav/rw.ts
Normal file
57
packages/filesystem/webdav/rw.ts
Normal file
@ -0,0 +1,57 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { WebDAVClient } from "webdav/web";
|
||||
import { FileReader, FileWriter } from "../filesystem";
|
||||
|
||||
export class WebDAVFileReader implements FileReader {
|
||||
client: WebDAVClient;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(client: WebDAVClient, path: string) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return this.client.getFileContents(this.path, {
|
||||
format: "text",
|
||||
}) as Promise<string>;
|
||||
default: {
|
||||
const resp = (await this.client.getFileContents(this.path, {
|
||||
format: "binary",
|
||||
})) as ArrayBuffer;
|
||||
return Promise.resolve(new Blob([resp]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class WebDAVFileWriter implements FileWriter {
|
||||
client: WebDAVClient;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(client: WebDAVClient, path: string) {
|
||||
this.client = client;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
async write(content: string | Blob): Promise<void> {
|
||||
let resp;
|
||||
if (content instanceof Blob) {
|
||||
resp = await this.client.putFileContents(
|
||||
this.path,
|
||||
await content.arrayBuffer()
|
||||
);
|
||||
} else {
|
||||
resp = await this.client.putFileContents(this.path, content);
|
||||
}
|
||||
if (resp) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error("write error"));
|
||||
}
|
||||
}
|
106
packages/filesystem/webdav/webdav.ts
Normal file
106
packages/filesystem/webdav/webdav.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { AuthType, createClient, FileStat, WebDAVClient } from "webdav/web";
|
||||
import FileSystem, { File, FileReader, FileWriter } from "../filesystem";
|
||||
import { joinPath } from "../utils";
|
||||
import { WebDAVFileReader, WebDAVFileWriter } from "./rw";
|
||||
import { WarpTokenError } from "../error";
|
||||
|
||||
export default class WebDAVFileSystem implements FileSystem {
|
||||
client: WebDAVClient;
|
||||
|
||||
url: string;
|
||||
|
||||
basePath: string = "/";
|
||||
|
||||
constructor(
|
||||
authType: AuthType | WebDAVClient,
|
||||
url?: string,
|
||||
username?: string,
|
||||
password?: string
|
||||
) {
|
||||
if (typeof authType === "object") {
|
||||
this.client = authType;
|
||||
this.basePath = joinPath(url || "");
|
||||
this.url = username!;
|
||||
} else {
|
||||
this.url = url!;
|
||||
this.client = createClient(url!, {
|
||||
authType,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async verify(): Promise<void> {
|
||||
try {
|
||||
await this.client.getQuota();
|
||||
} catch (e: any) {
|
||||
if (e.response && e.response.status === 401) {
|
||||
throw new WarpTokenError(e);
|
||||
}
|
||||
throw new Error("verify failed");
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
open(file: File): Promise<FileReader> {
|
||||
return Promise.resolve(
|
||||
new WebDAVFileReader(this.client, joinPath(file.path, file.name))
|
||||
);
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(
|
||||
new WebDAVFileSystem(this.client, joinPath(this.basePath, path), this.url)
|
||||
);
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(
|
||||
new WebDAVFileWriter(this.client, joinPath(this.basePath, path))
|
||||
);
|
||||
}
|
||||
|
||||
async createDir(path: string): Promise<void> {
|
||||
try {
|
||||
return Promise.resolve(
|
||||
await this.client.createDirectory(joinPath(this.basePath, path))
|
||||
);
|
||||
} catch (e: any) {
|
||||
// 如果是405错误,则忽略
|
||||
if (e.message.includes("405")) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(path: string): Promise<void> {
|
||||
return this.client.deleteFile(joinPath(this.basePath, path));
|
||||
}
|
||||
|
||||
async list(): Promise<File[]> {
|
||||
const dir = (await this.client.getDirectoryContents(
|
||||
this.basePath
|
||||
)) as FileStat[];
|
||||
const ret: File[] = [];
|
||||
dir.forEach((item: FileStat) => {
|
||||
if (item.type !== "file") {
|
||||
return;
|
||||
}
|
||||
ret.push({
|
||||
name: item.basename,
|
||||
path: this.basePath,
|
||||
digest: item.etag || "",
|
||||
size: item.size,
|
||||
createtime: new Date(item.lastmod).getTime(),
|
||||
updatetime: new Date(item.lastmod).getTime(),
|
||||
});
|
||||
});
|
||||
return Promise.resolve(ret);
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
return Promise.resolve(this.url + this.basePath);
|
||||
}
|
||||
}
|
32
packages/filesystem/zip/rw.ts
Normal file
32
packages/filesystem/zip/rw.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import JSZip, { JSZipObject } from "jszip";
|
||||
import { FileReader, FileWriter } from "../filesystem";
|
||||
|
||||
export class ZipFileReader implements FileReader {
|
||||
zipObject: JSZipObject;
|
||||
|
||||
constructor(zipObject: JSZipObject) {
|
||||
this.zipObject = zipObject;
|
||||
}
|
||||
|
||||
read(type?: "string" | "blob"): Promise<string | Blob> {
|
||||
return this.zipObject.async(type || "string");
|
||||
}
|
||||
}
|
||||
|
||||
export class ZipFileWriter implements FileWriter {
|
||||
zip: JSZip;
|
||||
|
||||
path: string;
|
||||
|
||||
constructor(zip: JSZip, path: string) {
|
||||
this.zip = zip;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
write(content: string): Promise<void> {
|
||||
this.zip.file(this.path, content);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
68
packages/filesystem/zip/zip.ts
Normal file
68
packages/filesystem/zip/zip.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import JSZip from "jszip";
|
||||
import FileSystem, {
|
||||
File,
|
||||
FileReader,
|
||||
FileWriter,
|
||||
} from "@Pkg/filesystem/filesystem";
|
||||
import { ZipFileReader, ZipFileWriter } from "./rw";
|
||||
|
||||
export default class ZipFileSystem implements FileSystem {
|
||||
zip: JSZip;
|
||||
|
||||
basePath: string;
|
||||
|
||||
// zip为空时,创建一个空的zip
|
||||
constructor(zip?: JSZip, basePath?: string) {
|
||||
this.zip = zip || new JSZip();
|
||||
this.basePath = basePath || "";
|
||||
}
|
||||
|
||||
verify(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
open(info: File): Promise<FileReader> {
|
||||
const path = info.name;
|
||||
const file = this.zip.file(path);
|
||||
if (file) {
|
||||
return Promise.resolve(new ZipFileReader(file));
|
||||
}
|
||||
return Promise.reject(new Error("File not found"));
|
||||
}
|
||||
|
||||
openDir(path: string): Promise<FileSystem> {
|
||||
return Promise.resolve(new ZipFileSystem(this.zip, path));
|
||||
}
|
||||
|
||||
create(path: string): Promise<FileWriter> {
|
||||
return Promise.resolve(new ZipFileWriter(this.zip, path));
|
||||
}
|
||||
|
||||
createDir(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
delete(path: string): Promise<void> {
|
||||
this.zip.remove(path);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
list(): Promise<File[]> {
|
||||
const files: File[] = [];
|
||||
Object.keys(this.zip.files).forEach((key) => {
|
||||
files.push({
|
||||
name: key,
|
||||
path: key,
|
||||
size: 0,
|
||||
digest: "",
|
||||
createtime: this.zip.files[key].date.getTime(),
|
||||
updatetime: this.zip.files[key].date.getTime(),
|
||||
});
|
||||
});
|
||||
return Promise.resolve(files);
|
||||
}
|
||||
|
||||
getDirUrl(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
@ -17,12 +17,11 @@ import FileSystemParams from "@App/pages/components/FileSystemParams";
|
||||
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FileSystemType } from "@Packages/filesystem/factory";
|
||||
|
||||
function Tools() {
|
||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||
const syncCtrl = IoC.instance(SynchronizeController) as SynchronizeController;
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||
systemConfig.backup.filesystem
|
||||
);
|
||||
|
@ -777,14 +777,13 @@ function ScriptEditor() {
|
||||
setEditors((prev) => {
|
||||
const i = parseInt(index, 10);
|
||||
if (prev[i].isChanged) {
|
||||
// eslint-disable-next-line no-restricted-globals, no-alert
|
||||
if (!confirm("脚本已修改, 关闭后会丢失修改, 是否继续?")) {
|
||||
return prev;
|
||||
}
|
||||
}
|
||||
if (prev.length === 1) {
|
||||
// 如果是id打开的回退到列表
|
||||
if (id) {
|
||||
// 如果是uuid打开的回退到列表
|
||||
if (uuid) {
|
||||
navigate("/");
|
||||
return prev;
|
||||
}
|
||||
@ -801,7 +800,7 @@ function ScriptEditor() {
|
||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||
} else {
|
||||
prev[i + 1].active = true;
|
||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||
setSelectSciptButtonAndTab(prev[i + 1].script.uuid);
|
||||
}
|
||||
}
|
||||
prev.splice(i, 1);
|
||||
|
@ -2,6 +2,33 @@ import { createAppSlice } from "../hooks";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { editor } from "monaco-editor";
|
||||
|
||||
function setAutoMode() {
|
||||
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const isMatch = (match: boolean) => {
|
||||
if (match) {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
editor.setTheme("vs-dark");
|
||||
} else {
|
||||
document.body.removeAttribute("arco-theme");
|
||||
editor.setTheme("vs");
|
||||
}
|
||||
};
|
||||
darkTheme.addEventListener("change", (e) => {
|
||||
isMatch(e.matches);
|
||||
});
|
||||
isMatch(darkTheme.matches);
|
||||
}
|
||||
|
||||
export type SystemConfig = {
|
||||
lightMode: "light" | "dark" | "auto";
|
||||
eslint: {
|
||||
enable: boolean;
|
||||
config: string;
|
||||
};
|
||||
scriptListColumnWidth: { [key: string]: number };
|
||||
menuExpandNum: number;
|
||||
};
|
||||
|
||||
export const settingSlice = createAppSlice({
|
||||
name: "setting",
|
||||
initialState: {
|
||||
@ -12,27 +39,22 @@ export const settingSlice = createAppSlice({
|
||||
},
|
||||
scriptListColumnWidth: {} as { [key: string]: number },
|
||||
menuExpandNum: 5,
|
||||
},
|
||||
} as SystemConfig,
|
||||
reducers: (create) => {
|
||||
// 初始化黑夜模式
|
||||
const setAutoMode = () => {
|
||||
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const isMatch = (match: boolean) => {
|
||||
if (match) {
|
||||
document.body.setAttribute("arco-theme", "dark");
|
||||
editor.setTheme("vs-dark");
|
||||
} else {
|
||||
document.body.removeAttribute("arco-theme");
|
||||
editor.setTheme("vs");
|
||||
}
|
||||
};
|
||||
darkTheme.addEventListener("change", (e) => {
|
||||
isMatch(e.matches);
|
||||
});
|
||||
isMatch(darkTheme.matches);
|
||||
};
|
||||
setAutoMode();
|
||||
// 加载配置
|
||||
chrome.storage.sync.get("systemSetting", (result) => {
|
||||
const systemSetting = result.systemSetting as SystemConfig;
|
||||
settingSlice.actions.initSetting(systemSetting);
|
||||
if (systemSetting) {
|
||||
localStorage.lightMode = systemSetting.lightMode;
|
||||
}
|
||||
});
|
||||
return {
|
||||
initSetting: create.reducer((state, action: PayloadAction<SystemConfig>) => {
|
||||
state.menuExpandNum = action.payload.menuExpandNum;
|
||||
}),
|
||||
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
|
||||
localStorage.loghtMode = action.payload;
|
||||
state.lightMode = action.payload;
|
||||
|
@ -4,8 +4,6 @@ import { languages } from "monaco-editor";
|
||||
// 注册eslint
|
||||
// const linterWorker = new Worker("/src/linter.worker.js");
|
||||
|
||||
console.log(dts, dts.length);
|
||||
|
||||
export default function registerEditor() {
|
||||
window.MonacoEnvironment = {
|
||||
getWorkerUrl(moduleId: any, label: any) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user