添加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 { IconQuestionCircleFill } from "@arco-design/web-react/icon";
|
||||||
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FileSystemType } from "@Packages/filesystem/factory";
|
||||||
|
|
||||||
function Tools() {
|
function Tools() {
|
||||||
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
|
||||||
const syncCtrl = IoC.instance(SynchronizeController) as SynchronizeController;
|
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
|
|
||||||
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
|
||||||
systemConfig.backup.filesystem
|
systemConfig.backup.filesystem
|
||||||
);
|
);
|
||||||
|
@ -777,14 +777,13 @@ function ScriptEditor() {
|
|||||||
setEditors((prev) => {
|
setEditors((prev) => {
|
||||||
const i = parseInt(index, 10);
|
const i = parseInt(index, 10);
|
||||||
if (prev[i].isChanged) {
|
if (prev[i].isChanged) {
|
||||||
// eslint-disable-next-line no-restricted-globals, no-alert
|
|
||||||
if (!confirm("脚本已修改, 关闭后会丢失修改, 是否继续?")) {
|
if (!confirm("脚本已修改, 关闭后会丢失修改, 是否继续?")) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prev.length === 1) {
|
if (prev.length === 1) {
|
||||||
// 如果是id打开的回退到列表
|
// 如果是uuid打开的回退到列表
|
||||||
if (id) {
|
if (uuid) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
@ -801,7 +800,7 @@ function ScriptEditor() {
|
|||||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
||||||
} else {
|
} else {
|
||||||
prev[i + 1].active = true;
|
prev[i + 1].active = true;
|
||||||
setSelectSciptButtonAndTab(prev[i - 1].script.uuid);
|
setSelectSciptButtonAndTab(prev[i + 1].script.uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prev.splice(i, 1);
|
prev.splice(i, 1);
|
||||||
|
@ -2,20 +2,7 @@ import { createAppSlice } from "../hooks";
|
|||||||
import { PayloadAction } from "@reduxjs/toolkit";
|
import { PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { editor } from "monaco-editor";
|
import { editor } from "monaco-editor";
|
||||||
|
|
||||||
export const settingSlice = createAppSlice({
|
function setAutoMode() {
|
||||||
name: "setting",
|
|
||||||
initialState: {
|
|
||||||
lightMode: localStorage.lightMode || "auto",
|
|
||||||
eslint: {
|
|
||||||
enable: true,
|
|
||||||
config: "",
|
|
||||||
},
|
|
||||||
scriptListColumnWidth: {} as { [key: string]: number },
|
|
||||||
menuExpandNum: 5,
|
|
||||||
},
|
|
||||||
reducers: (create) => {
|
|
||||||
// 初始化黑夜模式
|
|
||||||
const setAutoMode = () => {
|
|
||||||
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
const darkTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
const isMatch = (match: boolean) => {
|
const isMatch = (match: boolean) => {
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -30,9 +17,44 @@ export const settingSlice = createAppSlice({
|
|||||||
isMatch(e.matches);
|
isMatch(e.matches);
|
||||||
});
|
});
|
||||||
isMatch(darkTheme.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: {
|
||||||
|
lightMode: localStorage.lightMode || "auto",
|
||||||
|
eslint: {
|
||||||
|
enable: true,
|
||||||
|
config: "",
|
||||||
|
},
|
||||||
|
scriptListColumnWidth: {} as { [key: string]: number },
|
||||||
|
menuExpandNum: 5,
|
||||||
|
} as SystemConfig,
|
||||||
|
reducers: (create) => {
|
||||||
|
// 初始化黑夜模式
|
||||||
setAutoMode();
|
setAutoMode();
|
||||||
|
// 加载配置
|
||||||
|
chrome.storage.sync.get("systemSetting", (result) => {
|
||||||
|
const systemSetting = result.systemSetting as SystemConfig;
|
||||||
|
settingSlice.actions.initSetting(systemSetting);
|
||||||
|
if (systemSetting) {
|
||||||
|
localStorage.lightMode = systemSetting.lightMode;
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
|
initSetting: create.reducer((state, action: PayloadAction<SystemConfig>) => {
|
||||||
|
state.menuExpandNum = action.payload.menuExpandNum;
|
||||||
|
}),
|
||||||
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
|
setDarkMode: create.reducer((state, action: PayloadAction<"light" | "dark" | "auto">) => {
|
||||||
localStorage.loghtMode = action.payload;
|
localStorage.loghtMode = action.payload;
|
||||||
state.lightMode = action.payload;
|
state.lightMode = action.payload;
|
||||||
|
@ -4,8 +4,6 @@ import { languages } from "monaco-editor";
|
|||||||
// 注册eslint
|
// 注册eslint
|
||||||
// const linterWorker = new Worker("/src/linter.worker.js");
|
// const linterWorker = new Worker("/src/linter.worker.js");
|
||||||
|
|
||||||
console.log(dts, dts.length);
|
|
||||||
|
|
||||||
export default function registerEditor() {
|
export default function registerEditor() {
|
||||||
window.MonacoEnvironment = {
|
window.MonacoEnvironment = {
|
||||||
getWorkerUrl(moduleId: any, label: any) {
|
getWorkerUrl(moduleId: any, label: any) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user