添加filesystem

This commit is contained in:
王一之 2025-04-14 18:04:04 +08:00
parent 3b2e72127f
commit b76a685988
18 changed files with 1173 additions and 25 deletions

View File

@ -0,0 +1,8 @@
# 文件系统
用于同步和备份至云端
- zip
- webdav
- 百度网盘
- onedrive

114
packages/filesystem/auth.ts Normal file
View 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);
}

View 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
)}`
);
}
}

View 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();
});
}
}

View 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;
}

View 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();
});
}
}

View 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>;
}

View 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.");
}
}

View 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,
});
}
}

View 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;
}

View 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"));
}
}

View 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);
}
}

View 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();
}
}

View 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.");
}
}

View File

@ -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
); );

View File

@ -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);

View File

@ -2,6 +2,33 @@ import { createAppSlice } from "../hooks";
import { PayloadAction } from "@reduxjs/toolkit"; import { PayloadAction } from "@reduxjs/toolkit";
import { editor } from "monaco-editor"; 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({ export const settingSlice = createAppSlice({
name: "setting", name: "setting",
initialState: { initialState: {
@ -12,27 +39,22 @@ export const settingSlice = createAppSlice({
}, },
scriptListColumnWidth: {} as { [key: string]: number }, scriptListColumnWidth: {} as { [key: string]: number },
menuExpandNum: 5, menuExpandNum: 5,
}, } as SystemConfig,
reducers: (create) => { 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(); 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;

View File

@ -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) {