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 0000000..7f036af Binary files /dev/null and b/src/renderer/manage/pages/assets/s3plist.png differ 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" > +