From 176bdac99325bcfbf70c06ea87bdf26d9bd8959b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=90=8C=E8=90=8C=E5=93=92=E8=B5=AB=E8=90=9D?= Date: Mon, 20 Feb 2023 10:25:59 +0800 Subject: [PATCH] :sparkles: Feature: s3-compatible storage is supported now --- README.md | 9 +- package.json | 3 + src/main/manage/apis/aliyun.ts | 13 +- src/main/manage/apis/api.ts | 4 +- src/main/manage/apis/qiniu.ts | 15 +- src/main/manage/apis/s3plist.ts | 615 +++++++++++ src/main/manage/apis/smms.ts | 9 +- src/main/manage/apis/tcyun.ts | 4 +- src/main/manage/apis/upyun.ts | 4 +- src/main/manage/manageApi.ts | 12 + src/main/manage/utils/common.ts | 14 +- src/renderer/apis/awss3.ts | 7 +- src/renderer/apis/imgur.ts | 2 +- src/renderer/apis/smms.ts | 2 +- src/renderer/manage/pages/assets/s3plist.png | Bin 0 -> 14514 bytes src/renderer/manage/pages/bucketPage.vue | 141 ++- src/renderer/manage/pages/logIn.vue | 50 +- src/renderer/manage/pages/manageMain.vue | 4 +- src/renderer/manage/utils/common.ts | 37 +- src/renderer/manage/utils/constants.ts | 228 +++- src/renderer/pages/Gallery.vue | 1 - src/universal/utils/common.ts | 2 +- yarn.lock | 1039 +++++++++++++++++- 23 files changed, 2060 insertions(+), 155 deletions(-) create mode 100644 src/main/manage/apis/s3plist.ts create mode 100644 src/renderer/manage/pages/assets/s3plist.png diff --git a/README.md b/README.md index a484145..1200aa6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ | 阿里云OSS | :heavy_check_mark: | :heavy_check_mark: | | 又拍云 | :heavy_check_mark: | :heavy_check_mark: | | 七牛云 | :heavy_check_mark: | :heavy_check_mark: | +| S3 API兼容平台 | :heavy_check_mark: | :heavy_check_mark: | | 插件 | 相册云删除 | | :--: | :--: | @@ -48,13 +49,7 @@ ### CDN加速下载地址 -- [PicList-1.0.1-arm64.dmg](https://release.piclist.cn/1.0.1/PicList-1.0.1-arm64.dmg) -- [PicList-1.0.1-x64.dmg](https://release.piclist.cn/1.0.1/PicList-1.0.1-x64.dmg) -- [PicList-1.0.1.AppImage](https://release.piclist.cn/1.0.1/PicList-1.0.1.AppImage) -- [PicList-Setup-1.0.1-ia32.exe](https://release.piclist.cn/1.0.1/PicList-Setup-1.0.1-ia32.exe) -- [PicList-Setup-1.0.1-x64.exe](https://release.piclist.cn/1.0.1/PicList-Setup-1.0.1-x64.exe) -- [PicList-Setup-1.0.1.exe](https://release.piclist.cn/1.0.1/PicList-Setup-1.0.1.exe) -- [piclist_1.0.1_amd64.snap](https://release.piclist.cn/1.0.1/piclist_1.0.1_amd64.snap) +[https://github.com/Kuingsmile/PicList/releases/latest](https://github.com/Kuingsmile/PicList/releases/latest) ## 应用截图 diff --git a/package.json b/package.json index 765bf9c..975c955 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "link": "node ./scripts/cos-link.js" }, "dependencies": { + "@aws-sdk/client-s3": "^3.272.0", + "@aws-sdk/lib-storage": "^3.272.0", + "@aws-sdk/s3-request-presigner": "^3.272.0", "@element-plus/icons-vue": "^2.0.10", "@imengyu/vue3-context-menu": "^1.2.2", "@octokit/rest": "^19.0.7", diff --git a/src/main/manage/apis/aliyun.ts b/src/main/manage/apis/aliyun.ts index 9ce1ee7..a03a25c 100644 --- a/src/main/manage/apis/aliyun.ts +++ b/src/main/manage/apis/aliyun.ts @@ -20,7 +20,7 @@ class AliyunApi { ctx: OSS accessKeyId: string accessKeySecret: string - timeOut = 60000 + timeOut = 30000 logger: ManageLogger constructor (accessKeyId: string, accessKeySecret: string, logger: ManageLogger) { @@ -308,12 +308,10 @@ class AliyunApi { item.size !== 0 && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) }) result.isTruncated = res.isTruncated - result.nextMarker = res.nextContinuationToken === null ? '' : res.nextContinuationToken + result.nextMarker = res.nextContinuationToken || '' result.success = true - return result - } else { - return result } + return result } /** @@ -375,7 +373,7 @@ class AliyunApi { delimiter: '/', 'max-keys': '1000' }, { - timeout: 60000 + timeout: this.timeOut }) as any if (res && res.res.statusCode === 200) { res.prefixes !== null && allFileList.CommonPrefixes.push(...res.prefixes) @@ -498,8 +496,7 @@ class AliyunApi { progress: Math.floor(p * 100), status: uploadTaskSpecialStatus.uploading }) - }, - timeout: 60000 + } } ).then((res: any) => { const id = `${bucketName}-${region}-${key}-${filePath}` diff --git a/src/main/manage/apis/api.ts b/src/main/manage/apis/api.ts index fabfb14..b183431 100644 --- a/src/main/manage/apis/api.ts +++ b/src/main/manage/apis/api.ts @@ -5,6 +5,7 @@ import UpyunApi from './upyun' import SmmsApi from './smms' import GithubApi from './github' import ImgurApi from './imgur' +import S3plistApi from './s3plist' export default { TcyunApi, @@ -13,5 +14,6 @@ export default { UpyunApi, SmmsApi, GithubApi, - ImgurApi + ImgurApi, + S3plistApi } diff --git a/src/main/manage/apis/qiniu.ts b/src/main/manage/apis/qiniu.ts index cfcf0d1..d0e73f9 100644 --- a/src/main/manage/apis/qiniu.ts +++ b/src/main/manage/apis/qiniu.ts @@ -21,6 +21,7 @@ class QiniuApi { commonType = 'application/x-www-form-urlencoded' host = 'uc.qiniuapi.com' logger: ManageLogger + timeout = 30000 hostList = { getBucketList: 'https://uc.qiniuapi.com/buckets', @@ -100,7 +101,7 @@ class QiniuApi { Authorization: authorization, 'Content-Type': this.commonType }, - timeout: 10000 + timeout: this.timeout }) if (res && res.status === 200) { if (res.data && res.data.length) { @@ -145,7 +146,7 @@ class QiniuApi { 'Content-Type': 'application/json', Host: this.host }, - timeout: 10000 + timeout: this.timeout }) if (res && res.status === 200) { return { @@ -175,7 +176,7 @@ class QiniuApi { Authorization: authorization, 'Content-Type': this.commonType }, - timeout: 10000 + timeout: this.timeout }) if (res && res.status === 200) { return res.data && res.data.length ? res.data : [] @@ -206,7 +207,7 @@ class QiniuApi { 'Content-Type': this.commonType, Host: this.host }, - timeout: 10000 + timeout: this.timeout }) return res && res.status === 200 } @@ -233,7 +234,7 @@ class QiniuApi { 'Content-Type': 'application/json', Host: this.host }, - timeout: 10000 + timeout: this.timeout }) if (res && res.status === 200) { const changeAclRes = await this.setBucketAclPolicy({ @@ -364,10 +365,8 @@ class QiniuApi { result.isTruncated = !!(res.respBody && res.respBody.marker) result.nextMarker = res.respBody && res.respBody.marker ? res.respBody.marker : '' result.success = true - return result - } else { - return result } + return result } /** diff --git a/src/main/manage/apis/s3plist.ts b/src/main/manage/apis/s3plist.ts new file mode 100644 index 0000000..c5d8e03 --- /dev/null +++ b/src/main/manage/apis/s3plist.ts @@ -0,0 +1,615 @@ +import { + S3Client, + ListBucketsCommand, + ListObjectsV2Command, + GetBucketLocationCommand, + S3ClientConfig, + _Object, + CommonPrefix, + ListObjectsV2CommandOutput, + CopyObjectCommand, + GetObjectCommand, + DeleteObjectCommand, + DeleteObjectsCommand, + PutObjectCommand +} from '@aws-sdk/client-s3' +import { Upload, Progress } from '@aws-sdk/lib-storage' +import { getSignedUrl } from '@aws-sdk/s3-request-presigner' +import https from 'https' +import http from 'http' +import { ManageLogger } from '../utils/logger' +import { formatError, getAgent, getFileMimeType, gotDownload } from '../utils/common' +import { isImage } from '@/manage/utils/common' +import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent' +import windowManager from 'apis/app/window/windowManager' +import { IWindowList } from '#/types/enum' +import { ipcMain, IpcMainEvent } from 'electron' +import UpDownTaskQueue, +{ + uploadTaskSpecialStatus, + commonTaskStatus +} from '../datastore/upDownTaskQueue' +import fs from 'fs-extra' +import path from 'path' + +interface S3plistApiOptions { + credentials: { + accessKeyId: string + secretAccessKey: string + } + endpoint?: string + sslEnabled: boolean + s3ForcePathStyle: boolean + httpOptions?: { + agent: https.Agent + } +} + +class S3plistApi { + baseOptions: S3plistApiOptions + logger: ManageLogger + agent: any + + constructor ( + accessKeyId: string, + secretAccessKey: string, + endpoint: string | undefined, + sslEnabled: boolean, + s3ForcePathStyle: boolean, + proxy: string | undefined, + logger: ManageLogger + ) { + this.baseOptions = { + credentials: { + accessKeyId, + secretAccessKey + }, + endpoint: endpoint ? this.formatEndpoint(endpoint, sslEnabled) : undefined, + sslEnabled, + s3ForcePathStyle, + httpOptions: { + agent: this.setAgent(proxy, sslEnabled) + } + } as S3plistApiOptions + this.logger = logger + this.agent = this.setAgent(proxy, sslEnabled) + } + + formatEndpoint = (endpoint: string, sslEnabled: boolean): string => + !/^https?:\/\//.test(endpoint) ? `${sslEnabled ? 'https' : 'http'}://${endpoint}` : endpoint + + setAgent (proxy: string | undefined, sslEnabled: boolean) : HttpProxyAgent | HttpsProxyAgent | undefined { + if (sslEnabled) { + const agent = getAgent(proxy, true).https + return agent ?? new https.Agent({ + keepAlive: true, + rejectUnauthorized: false + }) + } else { + const agent = getAgent(proxy, false).http + return agent ?? new http.Agent({ + keepAlive: true + }) + } + } + + logParam = (error:any, method: string) => + this.logger.error(formatError(error, { class: 'S3plistApi', method })) + + formatFolder (item: CommonPrefix, slicedPrefix: string): any { + return { + Key: item.Prefix, + fileSize: 0, + formatedTime: '', + fileName: item.Prefix?.replace(slicedPrefix, '').replace('/', ''), + isDir: true, + checked: false, + isImage: false, + match: false, + key: item.Prefix + } + } + + formatFile (item: _Object, slicedPrefix: string, urlPrefix: string): any { + return { + ...item, + key: item.Key, + url: `${urlPrefix}/${item.Key}`, + fileName: item.Key?.replace(slicedPrefix, ''), + fileSize: item.Size, + formatedTime: new Date(item.LastModified!).toLocaleString(), + isDir: false, + checked: false, + match: false, + isImage: isImage(item.Key?.replace(slicedPrefix, '') || '') + } + } + + /** + * 获取存储桶列表 + */ + async getBucketList (): Promise { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = 'us-east-1' + const result = [] as IStringKeyMap[] + try { + const client = new S3Client(options) + const command = new ListBucketsCommand({}) + const data = await client.send(command) + if (data.$metadata.httpStatusCode === 200) { + if (data.Buckets) { + for (let i = 0; i < data.Buckets.length; i++) { + const bucket = data.Buckets[i] + const bucketName = bucket.Name + const command = new GetBucketLocationCommand({ + Bucket: bucketName + }) + const bucketConfig = await client.send(command) + if (bucketConfig.$metadata.httpStatusCode === 200) { + result.push({ + Name: bucketName, + CreationDate: bucket.CreationDate, + Location: bucketConfig.LocationConstraint || 'us-east-1' + }) + } else { + this.logParam(bucketConfig, 'getBucketList') + result.push({ + Name: bucketName, + CreationDate: bucket.CreationDate, + Location: 'us-east-1' + }) + } + } + } + } else { + this.logParam(data, 'getBucketList') + } + } catch (error) { + this.logParam(error, 'getBucketList') + } + return result + } + + async getBucketListBackstage (configMap: IStringKeyMap): Promise { + const window = windowManager.get(IWindowList.SETTING_WINDOW)! + const { bucketName: bucket, bucketConfig: { Location: region }, prefix, cancelToken } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com` + let marker + const cancelTask = [false] + ipcMain.on('cancelLoadingFileList', (_evt: IpcMainEvent, token: string) => { + if (token === cancelToken) { + cancelTask[0] = true + ipcMain.removeAllListeners('cancelLoadingFileList') + } + }) + let res = {} as ListObjectsV2CommandOutput + const result = { + fullList: [], + success: false, + finished: false + } + try { + do { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new ListObjectsV2Command({ + Bucket: bucket, + Prefix: slicedPrefix === '' ? undefined : slicedPrefix, + MaxKeys: 1000, + ContinuationToken: marker, + Delimiter: '/' + }) + res = await client.send(command) + if (res.$metadata.httpStatusCode === 200) { + res.CommonPrefixes && res.CommonPrefixes.forEach((item: CommonPrefix) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + res.Contents && res.Contents.forEach((item: _Object) => { + result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + window.webContents.send('refreshFileTransferList', result) + } else { + this.logParam(res, 'getBucketFileList') + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + return + } + marker = res.NextContinuationToken + } while (res.IsTruncated && !cancelTask[0]) + } catch (error) { + this.logParam(error, 'getBucketFileList') + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + result.success = true + result.finished = true + window.webContents.send('refreshFileTransferList', result) + ipcMain.removeAllListeners('cancelLoadingFileList') + } + + async getBucketFileList (configMap: IStringKeyMap): Promise { + const { bucketName: bucket, bucketConfig: { Location: region }, prefix, marker, itemsPerPage } = configMap + const slicedPrefix = prefix.slice(1) + const urlPrefix = configMap.customUrl || `https://${bucket}.s3.amazonaws.com` + const result = { + fullList: [], + isTruncated: false, + nextMarker: '', + success: false + } + try { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new ListObjectsV2Command({ + Bucket: bucket, + Prefix: slicedPrefix, + ContinuationToken: marker === '' ? undefined : marker, + Delimiter: '/', + MaxKeys: itemsPerPage + }) + const data = await client.send(command) + if (data.$metadata.httpStatusCode === 200) { + data.CommonPrefixes && data.CommonPrefixes.forEach((item: CommonPrefix) => { + result.fullList.push(this.formatFolder(item, slicedPrefix)) + }) + data.Contents && data.Contents.forEach((item: _Object) => { + result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix)) + }) + result.isTruncated = data.IsTruncated || false + result.nextMarker = data.NextContinuationToken || '' + result.success = true + } + } catch (error) { + this.logParam(error, 'getBucketFileList') + } + return result + } + + /** + * 重命名文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * oldKey: string, + * newKey: string + * } + */ + async renameBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, oldKey, newKey } = configMap + let result = false + try { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new CopyObjectCommand({ + Bucket: bucketName, + CopySource: encodeURI(`${bucketName}/${oldKey}`), + Key: newKey + }) + const data = await client.send(command) + if (data.$metadata.httpStatusCode === 200) { + const deleteCommand = new DeleteObjectCommand({ + Bucket: bucketName, + Key: oldKey + }) + const deleteData = await client.send(deleteCommand) + if (deleteData.$metadata.httpStatusCode === 204) { + result = true + } else { + this.logParam(deleteData, 'renameBucketFile') + } + } else { + this.logParam(data, 'renameBucketFile') + } + } catch (error) { + this.logParam(error, 'renameBucketFile') + } + return result + } + + /** + * 删除文件 + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string + * } + */ + async deleteBucketFile (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + let result = false + try { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new DeleteObjectCommand({ + Bucket: bucketName, + Key: key + }) + const data = await client.send(command) + if (data.$metadata.httpStatusCode === 204) { + result = true + } else { + this.logParam(data, 'deleteBucketFile') + } + } catch (error) { + this.logParam(error, 'deleteBucketFile') + } + return result + } + + /** + * 删除文件夹 + * @param configMap + */ + async deleteBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + let marker + let result = false + let IsTruncated + let res + const allFileList = { + CommonPrefixes: [] as any[], + Contents: [] as any[] + } + try { + do { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new ListObjectsV2Command({ + Bucket: bucketName, + Prefix: key, + ContinuationToken: marker === '' ? undefined : marker, + Delimiter: '/', + MaxKeys: 1000 + }) + res = await client.send(command) as ListObjectsV2CommandOutput + if (res.$metadata.httpStatusCode === 200) { + res.CommonPrefixes && allFileList.CommonPrefixes.push(...res.CommonPrefixes) + res.Contents && allFileList.Contents.push(...res.Contents) + IsTruncated = res.IsTruncated || false + marker = res.NextContinuationToken || '' + } else { + this.logParam(res, 'deleteBucketFolder') + return result + } + } while (IsTruncated) + if (allFileList.CommonPrefixes.length > 0) { + for (const item of allFileList.CommonPrefixes) { + res = await this.deleteBucketFolder({ + bucketName, + region, + key: item.Prefix + }) + if (!res) { + return result + } + } + } + if (allFileList.Contents.length > 0) { + const cycle = Math.ceil(allFileList.Contents.length / 1000) + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + for (let i = 0; i < cycle; i++) { + const deleteList = allFileList.Contents.slice(i * 1000, (i + 1) * 1000) + const deleteCommand = new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: deleteList.map((item) => { + return { + Key: item.Key + } + }) + } + }) + res = await client.send(deleteCommand) + if (res.$metadata.httpStatusCode !== 200) { + this.logParam(res, 'deleteBucketFolder') + return result + } + } + } + result = true + return result + } catch (error) { + this.logParam(error, 'deleteBucketFolder') + return result + } + } + + /** + * 获取预签名url + * @param configMap + * configMap = { + * bucketName: string, + * region: string, + * key: string, + * expires: number, + * customUrl: string + * } + */ + async getPreSignedUrl (configMap: IStringKeyMap): Promise { + const { bucketName, region, key, expires } = configMap + try { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const signedUrl = await getSignedUrl(client, new GetObjectCommand({ + Bucket: bucketName, + Key: key + }), { + expiresIn: expires || 3600 + }) + return signedUrl + } catch (error) { + this.logParam(error, 'getPreSignedUrl') + return 'error' + } + } + + /** + * 新建文件夹 + * @param configMap + */ + async createBucketFolder (configMap: IStringKeyMap): Promise { + const { bucketName, region, key } = configMap + let result = false + try { + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const command = new PutObjectCommand({ + Bucket: bucketName, + Key: key + }) + const data = await client.send(command) + if (data.$metadata.httpStatusCode === 200) { + result = true + } else { + this.logParam(data, 'createBucketFolder') + } + } catch (error) { + this.logParam(error, 'createBucketFolder') + } + return result + } + + /** + * upload file + * @param configMap + */ + async uploadBucketFile (configMap: IStringKeyMap): Promise { + const { fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // filePath: string + // fileSize: number + // }] + const instance = UpDownTaskQueue.getInstance() + fileArray.forEach((item: any) => { + item.key.startsWith('/') && (item.key = item.key.slice(1)) + }) + const allowedAcl = ['private', 'public-read', 'public-read-write', 'aws-exec-read', 'authenticated-read', 'bucket-owner-read', 'bucket-owner-full-control'] + for (const item of fileArray) { + const { bucketName, region, key, filePath, fileName, aclForUpload } = item + const options = Object.assign({}, this.baseOptions) as S3ClientConfig + options.region = region || 'us-east-1' + const client = new S3Client(options) + const id = `${bucketName}-${region}-${key}-${filePath}` + if (instance.getUploadTask(id)) { + continue + } + const fileStream = fs.createReadStream(filePath) + instance.addUploadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + sourceFilePath: filePath, + targetFilePath: key, + targetFileBucket: bucketName, + targetFileRegion: region + }) + const parallelUploads3 = new Upload({ + client, + params: { + Bucket: bucketName, + Key: key, + Body: fileStream, + ContentType: getFileMimeType(fileName), + ACL: allowedAcl.includes(aclForUpload) ? aclForUpload : 'private', + Metadata: { + description: 'uploaded by PicList' + } + } + }) + parallelUploads3.on('httpUploadProgress', (progress: Progress) => { + instance.updateUploadTask({ + id, + progress: progress.loaded && progress.total ? Math.floor(progress.loaded / progress.total * 100) : 0, + status: uploadTaskSpecialStatus.uploading + }) + }) + parallelUploads3.done().then((data) => { + if (data.$metadata.httpStatusCode === 200) { + instance.updateUploadTask({ + id, + progress: 100, + status: uploadTaskSpecialStatus.uploaded, + finishTime: new Date().toLocaleString() + }) + } else { + instance.updateUploadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + finishTime: new Date().toLocaleString() + }) + } + }).catch((error) => { + this.logParam(error, 'uploadBucketFile') + instance.updateUploadTask({ + id, + progress: 0, + status: commonTaskStatus.failed, + response: JSON.stringify(error), + finishTime: new Date().toLocaleString() + }) + }) + } + return true + } + + /** + * 下载文件 + * @param configMap + */ + async downloadBucketFile (configMap: IStringKeyMap): Promise { + const { downloadPath, fileArray } = configMap + // fileArray = [{ + // bucketName: string, + // region: string, + // key: string, + // fileName: string + // }] + const instance = UpDownTaskQueue.getInstance() + for (const item of fileArray) { + const { bucketName, region, key, fileName, customUrl } = item + const savedFilePath = path.join(downloadPath, fileName) + const fileStream = fs.createWriteStream(savedFilePath) + const id = `${bucketName}-${region}-${key}-${savedFilePath}` + if (instance.getDownloadTask(id)) { + continue + } + instance.addDownloadTask({ + id, + progress: 0, + status: commonTaskStatus.queuing, + sourceFileName: fileName, + targetFilePath: savedFilePath + }) + const preSignedUrl = await this.getPreSignedUrl({ + bucketName, + region, + key, + expires: 36000, + customUrl + }) + gotDownload(instance, preSignedUrl, fileStream, id, savedFilePath, this.logger) + } + return true + } +} + +export default S3plistApi diff --git a/src/main/manage/apis/smms.ts b/src/main/manage/apis/smms.ts index 8696bec..42db895 100644 --- a/src/main/manage/apis/smms.ts +++ b/src/main/manage/apis/smms.ts @@ -15,15 +15,20 @@ class SmmsApi { token: string axiosInstance: AxiosInstance logger: ManageLogger + timeout = 30000 constructor (token: string, logger: ManageLogger) { this.token = token this.axiosInstance = axios.create({ baseURL: this.baseUrl, - timeout: 30000, + timeout: this.timeout, headers: { Authorization: this.token - } + }, + httpsAgent: new (require('https').Agent)({ + keepAlive: true, + timeout: this.timeout + }) }) this.logger = logger } diff --git a/src/main/manage/apis/tcyun.ts b/src/main/manage/apis/tcyun.ts index 4ec8c00..0a238e7 100644 --- a/src/main/manage/apis/tcyun.ts +++ b/src/main/manage/apis/tcyun.ts @@ -211,10 +211,8 @@ class TcyunApi { result.isTruncated = res.IsTruncated === 'true' result.nextMarker = res.NextMarker || '' result.success = true - return result - } else { - return result } + return result } /** diff --git a/src/main/manage/apis/upyun.ts b/src/main/manage/apis/upyun.ts index 9f0f594..cc51e72 100644 --- a/src/main/manage/apis/upyun.ts +++ b/src/main/manage/apis/upyun.ts @@ -173,10 +173,8 @@ class UpyunApi { result.isTruncated = res.next !== this.stopMarker result.nextMarker = res.next result.success = true - return result - } else { - return result } + return result } /** diff --git a/src/main/manage/manageApi.ts b/src/main/manage/manageApi.ts index dc7e383..5458c23 100644 --- a/src/main/manage/manageApi.ts +++ b/src/main/manage/manageApi.ts @@ -67,6 +67,8 @@ export class ManageApi extends EventEmitter implements ManageApiType { return new API.GithubApi(this.currentPicBedConfig.token, this.currentPicBedConfig.githubUsername, this.currentPicBedConfig.proxy, this.logger) case 'imgur': return new API.ImgurApi(this.currentPicBedConfig.imgurUserName, this.currentPicBedConfig.accessToken, this.currentPicBedConfig.proxy, this.logger) + case 's3plist': + return new API.S3plistApi(this.currentPicBedConfig.accessKeyId, this.currentPicBedConfig.secretAccessKey, this.currentPicBedConfig.endpoint, this.currentPicBedConfig.sslEnabled, this.currentPicBedConfig.s3ForcePathStyle, this.currentPicBedConfig.proxy, this.logger) default: return {} as any } @@ -150,6 +152,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'qiniu': case 'github': case 'imgur': + case 's3plist': try { client = this.createClient() return await client.getBucketList() @@ -305,6 +308,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'smms': case 'github': case 'imgur': + case 's3plist': try { client = this.createClient() as any return await client.getBucketListBackstage(param!) @@ -348,6 +352,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'qiniu': case 'upyun': case 'smms': + case 's3plist': try { client = this.createClient() return await client.getBucketFileList(param!) @@ -372,6 +377,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'smms': case 'github': case 'imgur': + case 's3plist': try { client = this.createClient() as any const res = await client.deleteBucketFile(param!) @@ -395,6 +401,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'qiniu': case 'upyun': case 'github': + case 's3plist': try { client = this.createClient() as any return await client.deleteBucketFolder(param!) @@ -416,6 +423,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'aliyun': case 'qiniu': case 'upyun': + case 's3plist': try { client = this.createClient() as any return await client.renameBucketFile(param!) @@ -440,6 +448,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'smms': case 'github': case 'imgur': + case 's3plist': try { client = this.createClient() as any const res = await client.downloadBucketFile(param!) @@ -470,6 +479,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'qiniu': case 'upyun': case 'github': + case 's3plist': try { client = this.createClient() as any return await client.createBucketFolder(param!) @@ -494,6 +504,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'smms': case 'github': case 'imgur': + case 's3plist': try { client = this.createClient() as any return await client.uploadBucketFile(param!) @@ -515,6 +526,7 @@ export class ManageApi extends EventEmitter implements ManageApiType { case 'aliyun': case 'qiniu': case 'github': + case 's3plist': try { client = this.createClient() as any return await client.getPreSignedUrl(param!) diff --git a/src/main/manage/utils/common.ts b/src/main/manage/utils/common.ts index 746c7d6..f04f1c8 100644 --- a/src/main/manage/utils/common.ts +++ b/src/main/manage/utils/common.ts @@ -101,13 +101,10 @@ export const gotDownload = async ( got( preSignedUrl, { - timeout: { - request: 30000 - }, isStream: true, throwHttpErrors: false, searchParams: param, - agent + agent: agent || {} } ) .on('downloadProgress', (progress: any) => { @@ -118,7 +115,7 @@ export const gotDownload = async ( }) }) .pipe(fileStream) - .on('finish', () => { + .on('close', () => { instance.updateDownloadTask({ id, progress: 100, @@ -158,7 +155,7 @@ export const gotUpload = async ( method, body, timeout: { - request: timeout + lookup: timeout }, throwHttpErrors, agent @@ -227,8 +224,7 @@ export const getAgent = (proxy:any, https: boolean = true) => { https: new HttpsProxyAgent({ keepAlive: true, keepAliveMsecs: 1000, - maxSockets: 256, - maxFreeSockets: 256, + rejectUnauthorized: false, scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, proxy: formatProxy.replace('127.0.0.1', 'localhost') }) @@ -240,8 +236,6 @@ export const getAgent = (proxy:any, https: boolean = true) => { http: new HttpProxyAgent({ keepAlive: true, keepAliveMsecs: 1000, - maxSockets: 256, - maxFreeSockets: 256, scheduling: 'lifo' as 'lifo' | 'fifo' | undefined, proxy: formatProxy.replace('127.0.0.1', 'localhost') }) diff --git a/src/renderer/apis/awss3.ts b/src/renderer/apis/awss3.ts index 2a7d2f2..d15d9b3 100644 --- a/src/renderer/apis/awss3.ts +++ b/src/renderer/apis/awss3.ts @@ -4,11 +4,11 @@ export default class AwsS3Api { static async delete (configMap: IStringKeyMap): Promise { const { imgUrl, config: { accessKeyID, secretAccessKey, bucketName, region, endpoint, pathStyleAccess, bucketEndpoint, rejectUnauthorized } } = configMap try { - const url = new URL((!imgUrl.startsWith('http') && !imgUrl.startsWith('https')) ? `http://${imgUrl}` : imgUrl) + const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl) const fileKey = url.pathname let endpointUrl if (endpoint) { - if (!endpoint.startsWith('http') && !endpoint.startsWith('https')) { + if (!/^https?:\/\//.test(endpoint)) { endpointUrl = `http://${endpoint}` } else { endpointUrl = endpoint @@ -29,7 +29,8 @@ export default class AwsS3Api { s3BucketEndpoint: bucketEndpoint, httpOptions: { agent: new http.Agent({ - rejectUnauthorized + rejectUnauthorized, + timeout: 30000 }) } }) diff --git a/src/renderer/apis/imgur.ts b/src/renderer/apis/imgur.ts index 7cdc46f..0517c0e 100644 --- a/src/renderer/apis/imgur.ts +++ b/src/renderer/apis/imgur.ts @@ -11,7 +11,7 @@ export default class ImgurApi { try { const res = await axios.delete(fullUrl, { headers, - timeout: 10000 + timeout: 30000 }) return res.status === 200 } catch (error) { diff --git a/src/renderer/apis/smms.ts b/src/renderer/apis/smms.ts index c159095..e324f3b 100644 --- a/src/renderer/apis/smms.ts +++ b/src/renderer/apis/smms.ts @@ -15,7 +15,7 @@ export default class SmmsApi { hash, format: 'json' }, - timeout: 10000 + timeout: 30000 }) return res.status === 200 } diff --git a/src/renderer/manage/pages/assets/s3plist.png b/src/renderer/manage/pages/assets/s3plist.png new file mode 100644 index 0000000000000000000000000000000000000000..7f036afe8e8c025f242edc38921b48afa957287e GIT binary patch literal 14514 zcmdVB2T+q;_b(cX2qMzu1q1;h0@6#69(wN}9i@aGLWj^55J5$HRhsnPLkXY=NR{3a z2ug2}5(!H4#P9sy_x!&(=eu)f&di;;naMo+*?X$ld9H`Ldly1{Y-002;F zX{s6n07OZD{^VqYoTwhqDB;2EuV&$I0(15cviF4ml$>A=5DqPFsP9I9~z-xs5gNd0FYA*_O^HQfcSGbKwR9s5Sk{D8%2MBN*!G zY+){rf06im$aDSAMp@_?a;U(3AsmuIAVEhFQ3(zyX(3Tbkd(BP0EZaiU0CFyu$YLT zh_sBPxQvJx$AA5}2(|e-Im;NUs{dDAgg1FESATzR8DZg|pdg_jaUqzmi?FD)w6w5@ zn6Q|bAb~>A5ANk}A1vtQckgcrst`X%UpH@mH<%a4ABpx3umFE~E<#KHXBVK}|A_VS z`>$al4481Ry|=KakjS4d{Y|K+_y0@^h5keB=Wh)8U+w)r3idOFdqaecA%3s`Uq`~k zIp6zJmA8zFFT~y-=4%RrdHy|$hORJwn4c@mn?ptAU!%sst7q@%=Jh9&?_XScdNNvG ze*X4eju0(Xc`kw+AvZTC8D$kob=8OBs!C#_Qlg@263QS6RV5GzBnA>yQBsvu`I}c2 z<`@8lc=`X$>-4{P)&3*zABBK=6KYn4___r`oYZ|`P>z3fT*mD`w?+9s%KI-~r~lj* zmH)^qOlXYopXvQ?)BSfAL5Kb%|50AT%RlN5@gnHFFG0CaZeKqI03ra@x_29q){`smAO-7l5(lZclumW?0EFUD~L1E-jEr?MH< zJ`kmAU1QMbgZELqBKeZ=qG0=SPp4+u)2rOg6@=HbCqgkse znU?0}hlF))wWCWhyp6{v;01VNBU2OZtb7lW+7-X8CH=( zKUy0_Nl5_-VheY^L))kxMt7YY<2w+u7_U=2`8NA^4iyNRWOc~mQ!XI|TLk(JY*2g`oxb9jZoqJN>ckb0+}X|x<`Oo7EdUt2XUvXSRShzDI`cu-lj z!?P&B`sa*Idrc8w@ZKKu{a>e(U&TM6ZD8}DUftGW&u>`Qz45g!wy&!-yU2a43`{Dx zb0FOnPDw;Gm~kak9r?!J{_*?J;nTXC3OGGoyRsqi1B3Vh0u$D_`?m+i8QwVwq^RTK z0E|VGr8z<$Rc=&V`i4Pk>QomK7iC9I&EJ7rew}nf&1gH?_Ek`kONSiGdrtl#X8aPb$wFOf~y~*g-K6% znX7}MyA1?ijy8YXN+kO}j+x8t8cN=%$Yt-Wq?yA9OMX!~CBP%pQd z>UMiT;YX|Evx2^Zl3a@C#4P;r*|xdWiW}Ly62Z?YbapS}qrl?tEq7?>so>0^)7-y5 zM+>O}^h8DZRBm5%do6FC7UF-?1P`A1Ri12hl9Pj>uJ6vl2rp(8ag&6P0i%wzqI|{_ ziBOxt5yMD!=gW55z!OlfAqmY1KzrKwsxM0$#6~X2495v47mZB=Rqij(Rf|{)# z)yM=hzpqi``Ac@1C*H#SvI0_jt#&}wl$4a^wY=@Z&pG1(KyINjNFd0b#4daK#{Dra zj}WQ7@sD%)*C+O~3$IM*D&76ug!dUqNpq+uJ~JW1V@e0v1mi|e@wSK zyJ42vmyxLlouG{NhlV#;sGu|vGqDztV>XeS*g6Rvq4F=8T;WqoafZE1Wj4IV59iQo ziFAVS!rh6x9f3DgSD%8#IYgP=NG7y|1YUxD8L~@w1UBy2V6m}p?aWDX@J*&No2pD1 zf}GL^o%Q}DJX$Pk%pHvm$j??D|3Tm_UHJY2dd z1|(qP^S+$K#1OS_2tkU2$yu`ec%eI_eP5?RO?nR{1_cCNBeiu+$&Z}&#=HHkbC;Yh zE09U|9fMeiiFxqc`aYbc@~=bmhh^rUW4W0cGvo0TB%^n5pgjFxVaB(29EgA5$s$%3 zQFiC7ZZm#U3a`W_4RE00SbmZh12PAqZULL}GqvR8Y0R<7y9*PsLfq{Yq@Tt4#-3ka zi~>!O6*()lvq_=AzYTjsv0e_ehQ&eKoj^QR8jPXC|B_Zs~&NMc*of-Wo+{ z(&Nii*FMIaqWaHp4142av^2cpITh^ezarl~^{KKp-#p7svvWxbcT)##>jpj@)2%zk zd-sNFO{8xJl~Ue6h0^|d2ZER>IGf3euhS;~HXrCzjuu9&$Y(o5=6O2!T9Dw&8b{4Y z_J$dNMDqK7byqW(Z*$2jH3Th7CgmPRZ)Ur`J`KS=ntI}`s(Xvy2fYj_@`2wFpMK}5 z&TpW?yeiv#6hxP#0Q|w~khkojQk|fCi{aNvm^RkCEq-V)<>inA{p%sjcLRGmkzR!@ zjm6)vL)qt1;P*E)(=v5+YwH&+t&&w#pofzz=TCWD>q2c7ZkjWv6qle84a_^tpwyMZ z1dkMQO$LbWio+%RWfj*_vvxC4-5}CYM@+g+aa9_Vb7*Is{W-Yw#6^-Pl7`0h22p&` z@l8rkV3qJnE}#5MGKE;O2WMrEpQ5%!>0dDei?q~Kir^hopqA(0G9C^i(@wW;%1{v9 z=XqmysWGJ4ta&?7?MC@TUK0MSwsqXcc=F-YnMvwXd3Pi`(bH1;B;FCvi7BzfIh1+v z51aJj{-auj>r8G*Mq&#At<}lvcV6g0og-SuMpM_voH#j*nyUj+;Xt|(DP#9>T99bg z*Ijy+i!{nf?;KKB6*(=pOQE@Zm&H)+lo-O+VM3{y=7QCc86#_y37&r|1_RvhAf zLccezZTjtsPyQY;F}bF!c(7VSG5ktRO*gGAYrtMZI4e{1Qv{HfYQz4H^9Zw50{VV* z;Ft;6YW%Cq;`-M_hJL3uiv7N{KBsp1t(&U+AWN*+YD&dR(F*ecXrTu*l<-@~NiKqU8$ zZlzCs#4M>}#)-=m{{S78nt>affA4~al$0ikC3wd9%p9c)5)_pl=su)JXZ`AdM31EN zy?8#+&QgW~r*5(9(Gql%ZqIWu+xo-QvA~Aq%soup!)8Ek5pP}@AJDd&cw&5 zBL7ItlJg?@;GZeS-Ku;)cSyf}asOWOQ{c`}WX@~%WxElR6Yb{$IZ>+cVU#87A)+jJ zMY|kvh7+Z~dEz!jbrq3(b;q;}qyh#IX*~_~yj}J28-}>W0ETm4hca;16%G>9sV$<- zEh8~z1y@Vsswx2&_viKO^!X$9RX1KgxoWo?*?pmQiljvmeT zicfn7uuHfx(K$U45h?fD)^vBQc7{n>9UbN}^k}n*68ilsVme4NrA|hZXHgR4 z@L1N9OX)$z({+nKox3w3%@ov`bSWeX=Z+0^hikJQqIVe!MyVD1X(@29{d0+ilt&yseL2JUne0PcfRr#!AV- z>Bl&!nTOJbwA!%-`H;(Y%OT7;+V-z#fUnwhHa1A9_tG`cp3UMHNxFoU&U)PzMm-DO zW~6^{c5hU=bI9e$1^O|$8*#Gtt1H*JlXS`a z?5y2X3lu(QRR;rh4Kx-ftbp)jXMQR+9&UF7Pxw8g5#b~SprHW(2+bzghC~3spNxMJ z|5oyEB>$BBllYtYPsYEb{BI@yS7QG!m^l$eL^B&?G|i4i(JlZtpyg+ zIa8f^vJsC_pRNvA3xtVx*?z+|Yzxb@*y!$oRm47^-7o`iR*OTI^h0^{mjgtj@OS-d z5!BT1+%>Ei?4?-S?<^Jzh<;U#=?b5=e%L_7Je)c6OG8}JB+UeWjOF2nM@of;_PnRG=zZq=^>)*k ztp!R3-RC5mQ|kO^VbpZQM5?1cCQFKPjBuKM1qV7JvEb90QU1O0ks8Iv6&ntOV@!My z($=H3Dt>SWl)C(4(~W6N^FI0s&R;2S#}+o9JtQkT7Yi9l#(+vjP#Yk^neC3wDkg?U z0I+oy%DRUYFwehCDH5`mcglNCdWxYtak&foY#GV^C~jVOmu~L8kY`MlqRC=eF9{Jj zNy&a3<~d=NMO0E2mW@5fOO~RluLe@O<35iH9S0Vll*x^C<<=|a`*HT!ew&dh4#%nE z!6Df@(LXw(8=I<5wo+>c0#I50t(Mijnv5)OH_k%3Zy8-tuKPJQDG~MM5L-kisL|{;g3T+?|b%% z8=J=H-qaI0*WJ2`h?T50{S~6Y{HmzXvJlkF7%1DAAiN+8`Z?ARG3Mx7p9<$rMN-WN zl{*m^6^lThKi=r|Wz26+Dt|ZxeCH;xx5dXn*rlopA(bx2UdTp@7rl70#jj#B0$y=- zKS1c%fR9yOU4uatLUwDMAj&(dSXWqXA2M_4NBdmJb^y;xbv5QtPXve$)mFYp_>i_I_$urWkg- z0eI)EU5BjhD6bvoH>P5l^68w#LAhvgKTj0J4JESlDiR$-5xc8QTHm0^v*;gIXD2>R ziGO}(@{vkV&9#{8#tRnnWD%>3DVYr-3W7aj2*TC?xp&LYe+uywRS8nvY>595CG*3j z1e6M-ICZ)RL3J`i#TQeN0pmtsjIX_$c2UL6BeSpz566SC+}iaH zq)GDtUqaNKCnHQ}t9<)j=fo|O;hSgU6^Kp?E>nbSTlmpTo&1d-p43@;it{l%I<0}6 zyIZ6<-416C4iBhzvbQT=MtGO!xrv=$$p=ToE7El<9B3v#_SRWNW|Ze%ufjm`9A~A= zbOq_%0u9f@NnQNyZi784@7?i_peOjijlunhr3})Ud`n@R>tVi=!&FD(zE|xf6;zEv z{@dK+?yf~SDm|}^Bihk-d7J5XV2&1h6h|spo6hV@g${|pPKr2Xm`+nnURji@xAYQ= z&B$MnI&0jEu?w&IMNgg7@TW7GaaNI)GP{X9k&1Mw*_&?sZL6PdKCa5AjEB$#uXR*RsOsAanUw18r#`W9!)WC%%nYErTBYwIDIGoWfbp@&iP+f zzvuGQSIDja^7he*WOt!`J=0MKKIXZkgVPg@Ggnkg`38%{PfFoHn9EPAPoAJNtLyA9 z$bjjyc4NV|uFrF>Hz42j7%8Ma)4ZJ-Uae^Yma$$VoaDToyWfDKWA1W{32dULUd%0w zgs7HfFvM9zCL*$ZPl5XeSUrLI?4oB6?3`bbYHEP-El)R8tp~O>QfuRUX`9W9Dl@It z!YaOwcg9it9{OWn#qJeCxYKF#9WKx4^a#hX6MsqbkY+(g*LZjFoCbco#*bS!Si|gi zKZpm2np<5;FU2mL)@)Z+o|~|5n&J!xcgaYdNO)&-bCrjOCQ994If9~A2agzD4_CO- zN4O={+3t)t1i!8*yr9IUo8mv_m1-4aUsGi*$@eR)jd+~HXEL`?aF27c6id&Hd=7H& zab11$#A)Qb<_zbPL&}o=1$&{-=`1^`n*Bb+aG2^Rj7i&TkX3rTB%kSCor_D3tY2Dt zbwPA~Thc`t?ysx%x1_{5s(G1SKdWa-&oy*#{r8hLKH#RTNlwrwbSwugXjJ;K8|q`RgfWcVh#1Idvgw z%{^*6tY9z-JSm&bqDbO4oMXuLcGz-SZHV{LpjXMjxV^uTp4`5&#N2FcF>^lvPB=41 zd)^Lx=4e?gmUh6tS)rhj$`~;NoWZUr2n=##ztL%*OG?J8J zyNqY|6*iRJ&R^a+zh`JnzI4AHbQM93_->)22C!Z2b-pMf9I%T{$`bRiGo+WlnoqV&JH?BVz{~| zAl*9tVi9UnHGP?f{?KFoMZ(BS*=oc^Cct1M<1@++gyMdZ0Z-&tN-d7EzmE{w)gz*; zQK{h3EfZr=*cT8qv^*>z_oI8oUin+}h8nA3S=Q&|A8*~D zDS7TAEL!v;O>{{&4y+XQo3xazK6~J$-c(KH!`b1iH8BE5njz{VoV4RdUt>;{!37o9 zxQB1Bdf!I_E*!K^*<7RFbi1mExxd`Nk52E)BU26|(*2AFiZ4bj2`X(Sv>$Gp;T!4h zE7E(08+_Hb4hh|?0LF~aV+zN?tto< z_-=M}%lH=t`bHP}?zt}@3TBu5LOCUZB@-VHlBx^b<8qiQ_Fstraq!-7O%B(}*H)6? zk56*6z2r(r0CE_u$B(`U238q&z#g+YwNU%i5sc61oWO)&mK)Nw^KdqDat;o@`kB}c zqxpreO{IYGnR{a5HD#`8YTzO{mvDFu99TtpsEh6}v?`+u?9?L4qab=ZHB|y0Z;V5X z=gNCLuHMlgT&3c)uj}G4M|zybNkYJBUPThhe}^A;bFy!h++=_ivhT3 ze-k_6JghPi5fA#-^8KCK>=e|7S6cL=9F^+5aM52yi_<`ERA-y$`=O_*MaGE{a+een zHUx9iNc6Gz^w;-1cLZN9B|mfJt&My|C1H*88N4<-CNu6?L3z$2ZgV``Z`~1a(l(Qi zo|*wH8W+naSs4imL2i!+6sns``Ulvb%B(_V)UCE=_W;uA`1y+BDpzfS708lXgJj#lK4H?2xw8$` zfLZq;SFGsi-`W1o%1-p0XvdFFED<~mX}R*YyzzBSK8fDnwxcCJzfG7I6FELO$c-As zw=TruBJBzv5G+C7>oVU@-wVCPEJ$w6&t?BO&yGhR1%H70V@U=dO~?}jF;G(UOJfw) zl(jC43P>;mxn28MI#`d-x1}sGP6LnoFxiDjM1c2lC&n(QP^3EXM?}j_*exO=SGzBV zcqr`r_Jy?M&%8Y>B_?7Q``}vm7mUzblcpX@4c&dnc=tQRYxlPCDqs$Zb?Te@n>KG( zjE7q2Cjv^0YluTocIgQ$`gtH(tO~Y_M4l4gd>+cWTJuK7Wd{~2PBBS(j$pD~V4b4K zkkQ1Z7x|L|B&bnqKeIhT`AW8JOtKo8LH^_RL?(ZVB*=V1&N=cCZvWR=d#Ni-$Z z59x_3y;7|r!Yr*7KJ}hvkh>LInU{7XfM;XMuu6l}LWDLdM%_-NP;Y`C#092`=(`n+ ze*|JSJ8tgfG{jt+uy6(COSo$i7bnf0`bGqHVIGgB6$f=AQf(D-w-`X~ zJd&sv8afJK>@R-yy5)Ap+byg!E+fzT2y}5O^5!5o(`)F_zJ)jVF^OA184+td1F=MrC>mz>C4_)jn4XuwA7tKdRHfV#M#^K^5bIr|w?iqp@NKTGD;06oh%{QQ6yy zcC@Sdl%t?eU%N#y>Tv=a_8`C;L>PrtWeF>G-U=he7x_Bh?6wGwr+Q8xPaB@KHp)eB zCR@h}?jl2PcO52g5$7_QH(Jl2>hy{-cbBj@FP6X3TxYZK7acnd;>|txVXDOBeO2B- zha5LkmWcz|4&G>h0UQb1&+pLSP&&4nGtnoOyU+r}_@t2X#nLkL1074 zWrS<@RAFz*14ldbH9|LeeeQYK74B%q(=o#Iy~EuZ<7jE3j+^vG_v}h{v`30a=e-E- zJ8*2<#w>zBHWE%zdr5$hqKf^?D71A_j)AK7L?3=P7>(|QO!=2U;I!<-L`1xbJ~&2y zD_iQEx-?Il`v-)ITt)KUz?YqcOw(eIllwx$XbCcIjL_N0CT~9Ak-dMYz)pbH3VKs1 zYG>QG&{Hubj^NQf)5TLLVF%?eAH>9o)-9uAO->4I?$F~pOvgnDQ#w5ENYO>Z=GZAb zX2EjHzc9-APAwt3KoUo97s;n*!SaPrB|YZ5>YqGwZloQgyKLfBY*`(m$6o`8 z_$bel5w-p%w(T5+LBA%e*>#B)aOCS#JZq-I zhMs2ZIaI4CA7PIvbvb0o0fz}hpI08P>JmJ$xvJ*Q&j!B1F+xE{Z}z)K^^oByA2e#tb^hl@`-K1P~ zbdU5fk1UgnLEcGV3uQYCV*O{wv3iq$iEkZcVWGBiSZaeJC^T4hQSNiJaodV8GF<4* z^_8S3)x}vdc8XzFQf?E>A?OoXHfvW`?njUEXIp$gZnf{@>NH7G`#8|7^Zvoij&z#T zVf?GdDmrDUU6(_UHVg*r3Cll-z}wD5!ZN11da&4Bcvo^FDR+Vxhk;bxdcm^*P0MTR z^44@ouA?V--^9A*`93FQ#%M`mV>X%GixB$Pi_)hqp{{OcYtmhFa`dScz@pc$6rwv@ zaObZ*r%Ll>7goNnDRYeCnP0Ra6K$#}W(WZK^AOqT*oQ}5v`INeYNKwOJ_3Rta5ocw zxWGt@oqyahZ?9K0n*oqPOCvZx&s%$O9lOASB+H|gJo7(=_>~|C-xSURro_&kh6ztf z*eB?rk-vHnaysyOzvU)W&amF4^ONq;6>8z~1;PUpJADbVY!@k; zM^BQqjOW(dd15T@86Efy-95#J!y>EaNw}9e9`9hSnruBMCdKA8%@fB?tBs&V?VD4= zydQD9p4dfa7J#~SU&sC+@- znMsWztKcM_C--X@Dg)dx{|$Ghk!y3QI3KKI?e?DQ1wWr(@(55y9ONT4xh6IjdqEdl zTVmN;XqUVFvdea}7c-zD-MXd^$~6hyb!t>fUU3U78Ndoi&V_x2ro%r4p||{;X@MXD zC~$3d#kjP2ZeF9kp84@@BBQj|Abpp%b*+fm#s*5!wKdNtY&(@z_V}#uiP~0byGQNh z%hie}5M%E7M?7>%E>M6;q>$&TWi62Fw;Me&L~!yV4)K_a!>lmL=h}VO>|d461Yck$ zN&qjx%CKsBf6NMO=f`R4)j>4~FCgbtQ++M+aY6Xo<(2z*Ej7AED7uO06L5s_q zrtyrHD5U*kg_E5Y(5L7CY)CCU{RE7;_-t$8tOBl7p|`xIx!zr8MlgdKdnkvXXNPks z)mgDo)s57+bkKI~=+CaIzQLzL0y#up#B$@njQNy0*AmR{-=KsiWsPbb1A}j)J(Z(J*ORz zlQ$uZ6>WuFa?a>c107jQPj`1MkF>ERCwVcNq|j<%$eSXfiW4*`4mb;2_6=D+=%|-6 zR%K8XD|kH{fBU?>>9zgYcMmwClfp+h6Y4eVKz|vRYYQ&&gxp=#w(u??`R==?!DV)j z19dasJz6)TelvGRH4`9 zaO;RC9Y^QuBLx%F&sw1wE^T4j@a@qw9BcVyM^=(%7{l$Am4O${)Th&p*leLsdiX|y zB^-M<&x@*gjw~%B@XBt)zDH`(uqLekS5M|-!NXza36{Uk+m2l}>Llx)bc_h~wP-jQ z?m9n1+mRppc-an}k3glr*w9H#0`Wms)aW&ilSJH;m}IaOj)K}=M^E|_eVQF7M#WAB#~ zv9=A8u!pNP)s$%EWMGluV=>%g8-JNmJ}e(X8IQCp2DQZY!d@Ue_q7FCXo=i%xyDdLZXnkz1E=}o1Te@{b9$&me)Ho_()p_8Fa0E1 zcLg2!7|gZeh9eHtQk|aRRZIc;(mdL$ral^=rQGX!fBEl&r@U9jJABO!^ynGinPtFN zvgYgR@sX@VL(lbeHMw~ckf?Z|R?J*Y%hNKw0LF^244|7kLt<5mxR!QNxp8kj&`X68 zsq3q~y=C*z;V+^gC0&NsC6YFf`0tvjru{59q2`rBNH3Es-3yUP1y&#Uit|?kNgvkA z?Y-ix`AWOFLUPT3^0%5SBixMRPUYc5c&9rS)ah}Pn?EI;b?7lL$4qQRg5l$4ra-G_ zd*erVG|)?N_LYvYm1ZoP#-=MzMKNoE+G~%3Uf;s7B^35&s+OGL?)PlY8!pbgsa3%& z>{vhfZ_0^=HRT_Qv~Ml1^kK_psWabX*@?rC4@)D z1f6L@2OVB+NfOV@m-J;9Y?GA~L#O0plxSVeBSKm%<6l0q<2*7dp6yjcM}U_5)*p#O z(%sp9GSH_!m3}!Ohm+v;6r}jxk}dyR6}LK=`P5yDu75!p$78j*%=5IB@L2#pU0I=HIpHXiCW|Ek&MQ!5no$L6nFymr0Wv^ASIfC*FOAk!MYyvH%b zPyhMqitt<*(RC=29Q@^Fo4&}sRU<61=M>$P@wC|`#fc;$9?oK(v%nMIb>PHRwq?A4 zjR<4l;53Ek4{V+U8)C@UIUYr# z!iD8KCjpNVGRg=@3OG|Gfm^!8`(DlgEHtqlahEuyCM)bC!3NpSwz2>HPF2@SWcrB$ zDn|8LBdjXh24bKxVydO`#O#4fO4scoBFkj%6nDw7OkPbM{4pnSihNwoI~Z}1Q$L@O zMmJ`5}CQ-|~2JhiGpQ8d!_(;=q&~BKigNZrT zZG5-|7H1?x;2b!S>QP?hjcb1pL}zqOj@WN}N^ zg$vVD@tpDrWR)}W(piCCvpx^eNkzOno>ITxrl9qNn%MY>Ol};J@Dn`FNpLxJMk6X{3pLtlPwBC)@mR^`~c58%n(&(e}G>UE@Dxpnm zde^UMoWr6W?5Zw%Lv!1(&37*2N$1CdC{EwV;C>0Rq0Kli-qBL?5?N_qnmyZVL3Xyr zqYr{8>UsorNH{n)RP0}}&DJJ+bR?8MdxkS%QoJy|sE9}F|9bq1-lm7;kCBYplWA5R zKX=RYJW$bUrY`v@_fDJO9q7mP#+?3a(aG~BL^?^3E1m{sA`aSC^Sh3O7A1%GA_IG2R;!1%?*u~iNHq7ACYa|$UP}ae(uL7cy!F^M8mY7 zNrcdbeMQ)~KxlWwHP23i_N03m5;uR}7b@8gC&Z;*l+Q#;)GPNM3DVOE2HY`>axrZz zzzalV^>!)bf5g3;9afoA^2lsUk}`w&udWJrT>@i6cgnxR%1)mAK_-~z2YPfgzchU` z6yJLTM+}_Ig=$qb@diUJz%=%mxC<1Pr6QQ=+#>3n;|!#8~xdnouqsRWV`-ER!YT&0oIL&5E%)EKxU*+{E=ec3SM=jBeUdujB>4x(ypbsY^ zsowi87{f>90^{$Q$M9=&DJ<@Ip7Y-j40}^P%40&BwXTK5g(t?g3%g4|jyv1xHPSa4 zCUo>BOcZ6=2{?F#HlhSy)BA zerPr9{CExwEz>(US?3&X~uh0EraaFTM2n{zQXSb#UL}{A0XMj zWBo9g;q(4~AvG?tubMxub1~`fJrLukE;?L#D)f%UKnvcFKD8MfGNee|qg)CkgkKo< zmNs7QwWevYlY<=_2KV znlMngRZ`OHIHt3csd7H_GHeFr^;DhMw+BdP4O!eKpDRgYui>|JKpu%T$ zmD65U8{uZ`DjPC(4f!lTzPCJu#e2NvExSKf+U%}~edmhnc+--r-MQ^u>2C1Q#sk~B zggP|ZD=VvJTjBG4PgvPwS`hHt#oZMi4m9dXIWv9^YExH|hZviN_mh2~&P85wvsB(J z=uNSk&bZqxqcU4wDDpsbOk=fM(J3 z64$}-?HOOu*ECARzBveb5*7*Fg0&vukqEDA;nW5>uDZCu`*&pla!Kk> z8Wwv(Wd{hC3H-3&Y3@xPrVQ%3L(q5wf+*_x6sapnaV}OqmA(s_aNSt)#-s9mx20{Y zo`OCgm}4x4tZT@*6V3{=8*_fXt*X2}o52cO`O4uH^+}??_ zg<%HXOAy>u#86z);x$a-Vc>IwW7mmW5y-*;Gf;x@TDwOS6$QpFd9_x=zvxr_#Usyp z*wwwS+&$obu4N1)aPrj;#0LCk2C{#K%Pz;hsMXWC>SmBGA?Ft~5iF}BA_QFCimkbt ztVSRRxZW<*Fdh?)m%LhqCL%uX7!#UxyyeqG*+u2UDPS>TbIx#RnZ7Qyh%8t0X9F}} zgL9lf-cs6sOxUap?BRYXM+4U}&mJqXCPYEWeBZid`p~46l7dem(aaz>r?R zo-x`?h=D%s#GBK|Cj~*vy8S+b|De6F$9fM-Mt_s%tV?&A1i-H8*odHHJ)rMmyZ!6b zuJ-cgLCx|RMCH54q#?uXw8R6#oR0Mu3f-xfSdvS-Pq>o8W?L@L16wl?Vt7Fc&}7W0 zAO0kd32acXp8mKJ@fCZUsgZCuVh6y%N?lwM`-pHQC+Gi{mwhfGiO2vLdoqW@@Mu>; QuqZ%FO<%QE`N^~Y0eRr7#{d8T literal 0 HcmV?d00001 diff --git a/src/renderer/manage/pages/bucketPage.vue b/src/renderer/manage/pages/bucketPage.vue index 2826cfb..d4275cd 100644 --- a/src/renderer/manage/pages/bucketPage.vue +++ b/src/renderer/manage/pages/bucketPage.vue @@ -15,7 +15,7 @@ style="flex-grow: 1;margin-left: 16px" > +