Files
query-awesome/query/query-resources/index.ts

250 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { adapter, DataOpts, Result } from '@kevisual/query';
import path from 'path-browserify-esm';
import { hashContent } from './utils';
type QueryResourcesOptions = {
prefix?: string;
storage?: Storage;
username?: string;
[key: string]: any;
};
export class QueryResources {
prefix: string; // root/resources
storage: Storage;
constructor(opts: QueryResourcesOptions) {
if (opts.username) {
this.prefix = `/${opts.username}/resources/`;
} else {
this.prefix = opts.prefix || '';
}
this.storage = opts.storage || localStorage;
}
setUsername(username: string) {
this.prefix = `/${username}/resources/`;
}
setPrefix(prefix: string) {
this.prefix = prefix;
}
header(headers?: Record<string, string>, json = true): Record<string, string> {
const token = this.storage.getItem('token');
const _headers: Record<string, string> = {
'Content-Type': 'application/json',
...headers,
};
if (!json) {
delete _headers['Content-Type'];
}
if (!token) {
return _headers;
}
return {
..._headers,
Authorization: `Bearer ${token}`,
};
}
async get(data: any, opts: DataOpts): Promise<any> {
return adapter({
url: opts.url!,
method: 'GET',
body: data,
...opts,
headers: this.header(opts?.headers),
});
}
async getList(prefix: string, data?: { recursive?: boolean }, opts?: DataOpts): Promise<Result<any[]>> {
return this.get(data, {
url: `${this.prefix}${prefix}`,
body: data,
...opts,
});
}
async fetchFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
const url = `${this.prefix}${filepath}`;
return this.get({}, { url, method: 'GET', ...opts, headers: this.header(opts?.headers, false), isText: true });
}
async uploadFile(filepath: string, content: string | Blob, opts?: DataOpts): Promise<Result<any>> {
const pathname = `${this.prefix}${filepath}`;
const filename = path.basename(pathname);
const type = getContentType(filename);
const url = new URL(pathname, window.location.origin);
const hashResult = hashContent(content);
// Blob 类型时 hashContent 返回 Promise
const hash = hashResult instanceof Promise ? await hashResult : hashResult;
url.searchParams.set('hash', hash);
// 判断是否需要分块上传文件大于20MB
const isBlob = content instanceof Blob;
const fileSize = isBlob ? content.size : new Blob([content]).size;
const CHUNK_THRESHOLD = 20 * 1024 * 1024; // 20MB
if (fileSize > CHUNK_THRESHOLD && isBlob) {
// 使用分块上传
return this.uploadChunkedFile(filepath, content, hash, opts);
}
const formData = new FormData();
if (isBlob) {
formData.append('file', content);
} else {
formData.append('file', new Blob([content], { type }));
}
return adapter({
url: url.toString(),
isPostFile: true,
method: 'POST',
body: formData,
timeout: 5 * 60 * 1000, // 5分钟超时
...opts,
headers: { ...opts?.headers, ...this.header(opts?.headers, false) },
params: {
hash: hash,
...opts?.params,
},
});
}
async uploadChunkedFile(filepath: string, file: Blob, hash: string, opts?: DataOpts): Promise<Result<any>> {
const pathname = `${this.prefix}${filepath}`;
const filename = path.basename(pathname);
const url = new URL(pathname, window.location.origin);
url.searchParams.set('hash', hash);
url.searchParams.set('chunk', '1');
console.log(`url,`, url, hash);
// 预留 eventSource 支持(暂不处理)
// const createEventSource = opts?.createEventSource;
const chunkSize = 5 * 1024 * 1024; // 5MB
const totalChunks = Math.ceil(file.size / chunkSize);
for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunkBlob = file.slice(start, end);
// 转换为 File 类型
const chunkFile = new File([chunkBlob], filename, { type: file.type || 'application/octet-stream' });
const formData = new FormData();
formData.append('file', chunkFile, filename);
formData.append('chunkIndex', currentChunk.toString());
formData.append('totalChunks', totalChunks.toString());
console.log(`Uploading chunk ${currentChunk + 1}/${totalChunks}`, url.toString());
try {
const res = await adapter({
url: url.toString(),
isPostFile: true,
method: 'POST',
body: formData,
timeout: 5 * 60 * 1000, // 5分钟超时
...opts,
headers: { ...opts?.headers, ...this.header(opts?.headers, false) },
params: {
hash: hash,
chunk: '1',
...opts?.params,
},
});
console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res);
} catch (error) {
console.error(`Error uploading chunk ${currentChunk + 1}/${totalChunks}`, error);
throw error;
}
}
return { code: 200, message: '上传成功' };
}
async createFolder(folderpath: string, opts?: DataOpts): Promise<Result<any>> {
const filepath = folderpath.endsWith('/') ? `${folderpath}keep.txt` : `${folderpath}/keep.txt`;
return this.uploadFile(filepath, '文件夹占位,其他文件不存在,文件夹不存在,如果有其他文件夹,删除当前文件夹占位文件即可', opts);
}
async rename(oldpath: string, newpath: string, opts?: DataOpts): Promise<Result<any>> {
const pathname = `${this.prefix}${oldpath}`;
const newName = `${this.prefix}${newpath}`;
const params = {
newName: newName,
};
const url = pathname
return adapter({
url,
method: 'PUT' as any,
headers: this.header(opts?.headers),
params,
});
}
async deleteFile(filepath: string, opts?: DataOpts): Promise<Result<any>> {
const url = `${this.prefix}${filepath}`;
return adapter({
url,
method: 'DELETE' as any,
headers: this.header(opts?.headers),
});
}
}
export const getContentType = (filename: string): string => {
const ext = path.extname(filename);
let type = 'text/plain';
switch (ext) {
case '':
type = 'application/octet-stream';
break;
case '.json':
type = 'application/json';
break;
case '.txt':
type = 'text/plain';
break;
case '.csv':
type = 'text/csv';
break;
case '.md':
type = 'text/markdown';
break;
case '.html':
case '.htm':
type = 'text/html';
break;
case '.xml':
type = 'application/xml';
break;
case '.js':
type = 'application/javascript';
break;
case '.css':
type = 'text/css';
break;
case '.ts':
type = 'application/typescript';
break;
case '.pdf':
type = 'application/pdf';
break;
case '.zip':
type = 'application/zip';
break;
case '.docx':
type = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
break;
case '.xlsx':
type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
break;
case '.mp3':
type = 'audio/mpeg';
break;
case '.mp4':
type = 'video/mp4';
break;
case '.png':
case '.jpg':
case '.jpeg':
case '.gif':
case '.webp':
type = `image/${ext.slice(1)}`;
break;
case '.svg':
type = 'image/svg+xml';
break;
}
return type;
};