From 746360b486b8aba4533a6042e1d439a81e3e3ec1 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, 21 Aug 2023 19:48:18 -0700 Subject: [PATCH] :sparkles: Feature: add remote delete support for huawei obs and doge cloud --- src/main/events/ipcList.ts | 12 +- src/main/manage/utils/dogeAPI.ts | 2 +- src/main/utils/deleteFunc.ts | 137 ++++++++++++++++++++++- src/renderer/apis/allApi.ts | 6 +- src/renderer/apis/dogecloud.ts | 18 +++ src/renderer/apis/huaweiyun.ts | 18 +++ src/renderer/manage/pages/manageMain.vue | 1 - src/universal/utils/static.ts | 2 +- token.json | 1 + 9 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 src/renderer/apis/dogecloud.ts create mode 100644 src/renderer/apis/huaweiyun.ts create mode 100644 token.json diff --git a/src/main/events/ipcList.ts b/src/main/events/ipcList.ts index b3054cb..0ebc968 100644 --- a/src/main/events/ipcList.ts +++ b/src/main/events/ipcList.ts @@ -88,7 +88,7 @@ import SSHClient from '../utils/sshClient' // Sftp 配置类型声明 import { ISftpPlistConfig } from 'piclist' -import { removeFileFromS3InMain } from '~/main/utils/deleteFunc' +import { removeFileFromS3InMain, removeFileFromDogeInMain, removeFileFromHuaweiInMain } from '~/main/utils/deleteFunc' const STORE_PATH = app.getPath('userData') @@ -188,6 +188,16 @@ export default { return result }) + ipcMain.handle('delete-doge-file', async (_evt: IpcMainInvokeEvent, configMap: IStringKeyMap) => { + const result = await removeFileFromDogeInMain(configMap) + return result + }) + + ipcMain.handle('delete-huaweicloud-file', async (_evt: IpcMainInvokeEvent, configMap: IStringKeyMap) => { + const result = await removeFileFromHuaweiInMain(configMap) + return result + }) + // migrate from PicGo ipcMain.handle('migrateFromPicGo', async () => { diff --git a/src/main/manage/utils/dogeAPI.ts b/src/main/manage/utils/dogeAPI.ts index 716981a..464b99b 100644 --- a/src/main/manage/utils/dogeAPI.ts +++ b/src/main/manage/utils/dogeAPI.ts @@ -54,7 +54,7 @@ export async function getTempToken (accessKey: string, secretKey: string): Promi Credentials: { 'doge-token': { token, - expires: Date.now() + 7200000 + expires: data.ExpiredAt * 1000 } } }) diff --git a/src/main/utils/deleteFunc.ts b/src/main/utils/deleteFunc.ts index 80c42c8..34e2e55 100644 --- a/src/main/utils/deleteFunc.ts +++ b/src/main/utils/deleteFunc.ts @@ -3,8 +3,75 @@ import { NodeHttpHandler } from '@smithy/node-http-handler' import http, { AgentOptions } from 'http' import https from 'https' import { getAgent } from '../manage/utils/common' +import axios from 'axios' +import crypto from 'crypto' +import querystring from 'querystring' -export async function removeFileFromS3InMain (configMap: IStringKeyMap) { +interface DogecloudTokenFull { + Credentials: { + accessKeyId: string + secretAccessKey: string + sessionToken: string + }, + ExpiredAt: number, + Buckets: { + name: string + s3Bucket: string + s3Endpoint: string + }[] +} + +const dogeRegionMap: IStringKeyMap = { + 'ap-shanghai': '0', + 'ap-beijing': '1', + 'ap-guangzhou': '2', + 'ap-chengdu': '3' +} + +async function dogecloudApi ( + apiPath: string, + data = {}, + jsonMode: boolean = false, + accessKey: string, + secretKey: string +) { + const body = jsonMode ? JSON.stringify(data) : querystring.encode(data) + const sign = crypto.createHmac('sha1', secretKey).update(Buffer.from(apiPath + '\n' + body, 'utf8')).digest('hex') + const authorization = `TOKEN ${accessKey}:${sign}` + try { + const res = await axios.request({ + url: `https://api.dogecloud.com${apiPath}`, + method: 'POST', + data: body, + responseType: 'json', + headers: { + 'Content-Type': jsonMode ? 'application/json' : 'application/x-www-form-urlencoded', + Authorization: authorization + } + }) + if (res.data.code !== 200) { + throw new Error('API Error') + } + return res.data.data + } catch (err: any) { + throw new Error('API Error') + } +} + +async function getDogeToken (accessKey: string, secretKey: string): Promise<{} | DogecloudTokenFull> { + try { + const data = await dogecloudApi('/auth/tmp_token.json', { + channel: 'OSS_FULL', + scopes: ['*'] + }, true, accessKey, secretKey) + return data + } catch (err: any) { + console.log(err) + return {} + } +} + +export async function removeFileFromS3InMain (configMap: IStringKeyMap, dogeMode: boolean = false) { try { const { imgUrl, config: { accessKeyID, secretAccessKey, bucketName, region, endpoint, pathStyleAccess, rejectUnauthorized, proxy } } = configMap const url = new URL(!/^https?:\/\//.test(imgUrl) ? `http://${imgUrl}` : imgUrl) @@ -50,6 +117,13 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap) { region, requestHandler: handler } + if (dogeMode) { + s3Options.credentials = { + accessKeyId: configMap.config.accessKeyID, + secretAccessKey: configMap.config.secretAccessKey, + sessionToken: configMap.config.sessionToken + } + } const client = new S3Client(s3Options) const command = new DeleteObjectCommand({ Bucket: bucketName, @@ -62,3 +136,64 @@ export async function removeFileFromS3InMain (configMap: IStringKeyMap) { return false } } + +export async function removeFileFromDogeInMain (configMap: IStringKeyMap) { + try { + const { config: { bucketName, AccessKey, SecretKey } } = configMap + const token = await getDogeToken(AccessKey, SecretKey) as DogecloudTokenFull + const bucket = token.Buckets?.find(item => item.name === bucketName || item.s3Bucket === bucketName) + const newConfigMap = Object.assign({}, configMap) + newConfigMap.config = { + ...newConfigMap.config, + accessKeyID: token.Credentials?.accessKeyId, + secretAccessKey: token.Credentials?.secretAccessKey, + sessionToken: token.Credentials?.sessionToken, + endpoint: bucket?.s3Endpoint, + region: dogeRegionMap[bucket?.s3Endpoint?.split('.')[1] || 'ap-shanghai'], + bucketName: bucket?.s3Bucket + } + return await removeFileFromS3InMain(newConfigMap, true) + } catch (err: any) { + console.log(err) + return false + } +} + +function createHuaweiAuthorization ( + bucketName: string, + path: string, + fileName: string, + accessKey: string, + secretKey: string, + date: string = new Date().toUTCString() +) { + const strToSign = `DELETE\n\n\n${date}\n/${bucketName}${path}/${fileName}` + const singature = crypto.createHmac('sha1', secretKey).update(strToSign).digest('base64') + return `OBS ${accessKey}:${singature}` +} + +export async function removeFileFromHuaweiInMain (configMap: IStringKeyMap) { + const { fileName, config } = configMap + const { accessKeyId, accessKeySecret, bucketName, endpoint } = config + let path = config.path || '/' + path = `/${path.replace(/^\/+|\/+$/, '')}` + path = path === '/' ? '' : path + const date = new Date().toUTCString() + const authorization = createHuaweiAuthorization(bucketName, path, fileName, accessKeyId, accessKeySecret, date) + try { + const res = await axios.request({ + url: `https://${bucketName}.${endpoint}${encodeURI(path)}/${encodeURIComponent(fileName)}`, + method: 'DELETE', + responseType: 'json', + headers: { + Host: `${bucketName}.${endpoint}`, + Date: date, + Authorization: authorization + } + }) + return res.status === 204 + } catch (error) { + console.log(error) + return false + } +} diff --git a/src/renderer/apis/allApi.ts b/src/renderer/apis/allApi.ts index d854783..db1bd88 100644 --- a/src/renderer/apis/allApi.ts +++ b/src/renderer/apis/allApi.ts @@ -9,6 +9,8 @@ import SmmsApi from './smms' import TcyunApi from './tcyun' import UpyunApi from './upyun' import WebdavApi from './webdav' +import DogeCloudApi from './dogecloud' +import HuaweicloudApi from './huaweiyun' const apiMap: IStringKeyMap = { aliyun: AliyunApi, @@ -21,7 +23,9 @@ const apiMap: IStringKeyMap = { smms: SmmsApi, tcyun: TcyunApi, upyun: UpyunApi, - webdavplist: WebdavApi + webdavplist: WebdavApi, + dogecloud: DogeCloudApi, + 'huaweicloud-uploader': HuaweicloudApi } export default class ALLApi { diff --git a/src/renderer/apis/dogecloud.ts b/src/renderer/apis/dogecloud.ts new file mode 100644 index 0000000..c66efd5 --- /dev/null +++ b/src/renderer/apis/dogecloud.ts @@ -0,0 +1,18 @@ +import { ipcRenderer } from 'electron' +import { getRawData } from '~/renderer/utils/common' +import { removeFileFromDogeInMain } from '~/main/utils/deleteFunc' + +export default class AwsS3Api { + static async delete (configMap: IStringKeyMap): Promise { + try { + return ipcRenderer + ? await ipcRenderer.invoke('delete-doge-file', + getRawData(configMap) + ) + : await removeFileFromDogeInMain(getRawData(configMap)) + } catch (error) { + console.log(error) + return false + } + } +} diff --git a/src/renderer/apis/huaweiyun.ts b/src/renderer/apis/huaweiyun.ts new file mode 100644 index 0000000..c1ef65f --- /dev/null +++ b/src/renderer/apis/huaweiyun.ts @@ -0,0 +1,18 @@ +import { ipcRenderer } from 'electron' +import { getRawData } from '~/renderer/utils/common' +import { removeFileFromHuaweiInMain } from '~/main/utils/deleteFunc' + +export default class HuaweicloudApi { + static async delete (configMap: IStringKeyMap): Promise { + try { + return ipcRenderer + ? await ipcRenderer.invoke('delete-huaweicloud-file', + getRawData(configMap) + ) + : await removeFileFromHuaweiInMain(getRawData(configMap)) + } catch (error) { + console.log(error) + return false + } + } +} diff --git a/src/renderer/manage/pages/manageMain.vue b/src/renderer/manage/pages/manageMain.vue index 6b008ed..e575a00 100644 --- a/src/renderer/manage/pages/manageMain.vue +++ b/src/renderer/manage/pages/manageMain.vue @@ -476,7 +476,6 @@ function handleSelectMenu (bucketName: string) { webPath: currentPicBedConfig.webPath || '' } currentSelectedBucket.value = bucketName - console.log(configMap) router.push({ path: '/main-page/manage-main-page/manage-bucket-page', query: { diff --git a/src/universal/utils/static.ts b/src/universal/utils/static.ts index 57cbe6c..7dd0cf4 100644 --- a/src/universal/utils/static.ts +++ b/src/universal/utils/static.ts @@ -6,4 +6,4 @@ export const RELEASE_URL_BACKUP = 'https://release.piclist.cn' export const STABLE_RELEASE_URL = 'https://github.com/Kuingsmile/PicList/releases/latest' export const C1 = Buffer.from(C1N, 'base64').toString() -export const picBedsCanbeDeleted = ['aliyun', 'aws-s3', 'github', 'imgur', 'local', 'sftpplist', 'smms', 'qiniu', 'tcyun', 'upyun', 'webdavplist'] +export const picBedsCanbeDeleted = ['aliyun', 'aws-s3', 'github', 'imgur', 'local', 'sftpplist', 'smms', 'qiniu', 'tcyun', 'upyun', 'webdavplist', 'dogecloud', 'huaweicloud-uploader'] diff --git a/token.json b/token.json new file mode 100644 index 0000000..8640fa5 --- /dev/null +++ b/token.json @@ -0,0 +1 @@ +{"credentials":{"accessKeyId":"AKIDnom3VMJULIYRTDSAbfViNIwBgO8WT9z8O5tvIUTG4jffEZhnAes5vx9AOYJrIilA","secretAccessKey":"OTl1srVfPKoheYjbjmF+LoOTnL66fZlfmEzPhCllBtw=","sessionToken":"fbliLFsUatpu7Nc77ob0jtfwDB38j56a928f93e916c50c385c0848816f6779acuXXhUtSZWuvvNDn4um8ZU0pMkOnax0HQZQ-dAtVnEWTgA2eBU_oRr7O1l5cypeTh1HeEBL4hwydggwJ6Tuy5kvYVCvgDcCfUgpNwr8rw-mUNXW_rvgFS0CdldipPY4ta2QrQA4IC8SDvC25ruTygBzA3nMqf-uyHs5VmlAM5e8Mab3kKtP6Y5Q1PSySSDYFq7LMZ3KbfGtvGtdp6RHSQI_24_3GLUgyIBeRYvN-viZP86oWfWiEVz0DzZa8T2LR5Z0rR01BZfb3k0CMdctojaAFRgeIbRa5aBIvv-r1DahAF80dtsUk-V8KmzpP2l_N6NT4nk2b6MLTzLAY2i0KPyQEOc8t8pdZUvVDwtTVelmHw2Vg6-pHRz8rraW2QiqApZcGKvgQwhAfnYSgbLCT68csZUkgKCME7gOzk87of50_TOP39ppQ14Twvma3vkn55C72x0BG83ExnUn9IeG02UHWabGzgZyjG7dvukfcIP_zzGzW_G-4hD0_w2tBVwkKfog1bVH48QN3asBnpiFXprqoeDuuRjvvIgF3Fp8tG4aHLcSzgwVyBqrQgo93EkkKWb-VafpquG0KGfed29rG63M78XgYnaAqHSFuo0e22UMMyupLwOc-cmwsMJYiEU_0j"},"s3Bucket":"s-gz-7032-piclist2-1258813047","s3Endpoint":"https://cos.ap-guangzhou.myqcloud.com"} \ No newline at end of file