Feature: s3-compatible storage is supported now

This commit is contained in:
萌萌哒赫萝 2023-02-20 10:25:59 +08:00
parent 7f7f400ce9
commit 176bdac993
23 changed files with 2060 additions and 155 deletions

View File

@ -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)
## 应用截图

View File

@ -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",

View File

@ -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}`

View File

@ -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
}

View File

@ -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
}
/**

View File

@ -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<any> {
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<any> {
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: <any>[],
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<any> {
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: <any>[],
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<boolean> {
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<boolean> {
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<boolean> {
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<string> {
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<boolean> {
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<boolean> {
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<boolean> {
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

View File

@ -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
}

View File

@ -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
}
/**

View File

@ -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
}
/**

View File

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

View File

@ -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')
})

View File

@ -4,11 +4,11 @@ export default class AwsS3Api {
static async delete (configMap: IStringKeyMap): Promise<boolean> {
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
})
}
})

View File

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

View File

@ -15,7 +15,7 @@ export default class SmmsApi {
hash,
format: 'json'
},
timeout: 10000
timeout: 30000
})
return res.status === 200
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -15,7 +15,7 @@
style="flex-grow: 1;margin-left: 16px"
>
<el-select
v-if="showCustomUrlSelectList && customUrlList.length > 1"
v-if="showCustomUrlSelectList && customUrlList.length > 1 && isAutoCustomUrl"
v-model="currentCustomUrl"
placeholder="请选择自定义域名"
style="width: 200px;"
@ -28,6 +28,13 @@
:value="item.value"
/>
</el-select>
<el-input
v-else-if="['aliyun', 'qiniu', 'tcyun', 's3plist'].includes(currentPicBedName)"
v-model="currentCustomUrl"
placeholder="请输入自定义域名"
style="width: 200px;"
@blur="handelChangeCustomUrl"
/>
<el-link
v-else
:underline="false"
@ -140,7 +147,7 @@
<Link />
</el-icon>
<template #dropdown>
<template v-if="['tcyun', 'qiniu', 'aliyun', 'github'].includes(currentPicBedName)">
<template v-if="showPresignedUrl">
<el-dropdown-item
v-for="i in [...linkArray, { key: '预签名链接', value: 'preSignedUrl' }]"
:key="i.key"
@ -790,6 +797,7 @@ import { v4 as uuidv4 } from 'uuid'
import path from 'path'
import { IUploadTask, IDownloadTask } from '~/main/manage/datastore/upDownTaskQueue'
import fs from 'fs-extra'
import { getConfig, saveConfig } from '../utils/dataSender'
/*
configMap:{
@ -866,9 +874,11 @@ const currentCustomUrl = ref('')
const showCustomUrlSelectList = computed(() => ['tcyun', 'aliyun', 'qiniu', 'github'].includes(currentPicBedName.value))
const showCreateNewFolder = computed(() => ['tcyun', 'aliyun', 'qiniu', 'upyun', 'github'].includes(currentPicBedName.value))
const showCreateNewFolder = computed(() => ['tcyun', 'aliyun', 'qiniu', 'upyun', 'github', 's3plist'].includes(currentPicBedName.value))
const showRenameFileIcon = computed(() => ['tcyun', 'aliyun', 'qiniu', 'upyun'].includes(currentPicBedName.value))
const showRenameFileIcon = computed(() => ['tcyun', 'aliyun', 'qiniu', 'upyun', 's3plist'].includes(currentPicBedName.value))
const showPresignedUrl = computed(() => ['tcyun', 'aliyun', 'qiniu', 'github', 's3plist'].includes(currentPicBedName.value))
const uploadingTaskList = computed(() => uploadTaskList.value.filter(item => ['uploading', 'queuing', 'paused'].includes(item.status)))
@ -878,6 +888,8 @@ const downloadingTaskList = computed(() => downloadTaskList.value.filter(item =>
const downloadedTaskList = computed(() => downloadTaskList.value.filter(item => ['downloaded', 'failed', 'canceled'].includes(item.status)))
const isAutoCustomUrl = computed(() => manageStore.config.picBed[configMap.alias].isAutoCustomUrl === undefined ? true : manageStore.config.picBed[configMap.alias].isAutoCustomUrl)
function startRefreshUploadTask () {
refreshUploadTaskId.value = setInterval(() => {
ipcRenderer.invoke('getUploadTaskList').then((res: any) => {
@ -1096,7 +1108,8 @@ function uploadFiles () {
filePath: item.path,
fileSize: item.size,
fileName: item.rawName,
githubBranch: currentCustomUrl.value
githubBranch: currentCustomUrl.value,
aclForUpload: manageStore.config.picBed[configMap.alias].aclForUpload
})
})
ipcRenderer.send('uploadBucketFile', configMap.alias, param)
@ -1193,49 +1206,97 @@ async function handelChangeCustomUrl () {
showLoadingPage.value = true
await resetParam(true)
showLoadingPage.value = false
} else if (['aliyun', 'tcyun', 'qiniu', 's3plist'].includes(currentPicBedName.value)) {
const currentConfigs = await getConfig<any>('picBed')
const currentConfig = currentConfigs[configMap.alias]
const currentTransformedConfig = JSON.parse(currentConfig.transformedConfig ?? '{}')
if (currentTransformedConfig[configMap.bucketName]) {
currentTransformedConfig[configMap.bucketName].customUrl = currentCustomUrl.value
} else {
currentTransformedConfig[configMap.bucketName] = {
customUrl: currentCustomUrl.value
}
}
currentConfig.transformedConfig = JSON.stringify(currentTransformedConfig)
saveConfig(`picBed.${configMap.alias}`, currentConfig)
await manageStore.refreshConfig()
}
}
// when the current picBed is github, the customUrlList is used to store the github repo branches
async function initCustomUrlList () {
const param = {
bucketName: configMap.bucketName,
region: configMap.bucketConfig.Location
}
let defaultUrl = ''
if (currentPicBedName.value === 'tcyun') {
defaultUrl = `https://${configMap.bucketName}.cos.${configMap.bucketConfig.Location}.myqcloud.com`
} else if (currentPicBedName.value === 'aliyun') {
defaultUrl = `https://${configMap.bucketName}.${configMap.bucketConfig.Location}.aliyuncs.com`
} else if (currentPicBedName.value === 'github') {
defaultUrl = 'main'
}
const res = await ipcRenderer.invoke('getBucketDomain', configMap.alias, param)
if (res.length > 0) {
customUrlList.value.length = 0
res.forEach((item: any) => {
if (!item.startsWith('http://') && !item.startsWith('https://') && currentPicBedName.value !== 'github') {
item = manageStore.config.settings.isForceCustomUrlHttps ? `https://${item}` : `http://${item}`
}
customUrlList.value.push({
label: item,
value: item
if ((['aliyun', 'tcyun', 'qiniu'].includes(currentPicBedName.value) &&
(manageStore.config.picBed[configMap.alias].isAutoCustomUrl === undefined || manageStore.config.picBed[configMap.alias].isAutoCustomUrl === true)) ||
['github', 'smms', 'upyun', 'imgur'].includes(currentPicBedName.value)) {
const param = {
bucketName: configMap.bucketName,
region: configMap.bucketConfig.Location
}
let defaultUrl = ''
if (currentPicBedName.value === 'tcyun') {
defaultUrl = `https://${configMap.bucketName}.cos.${configMap.bucketConfig.Location}.myqcloud.com`
} else if (currentPicBedName.value === 'aliyun') {
defaultUrl = `https://${configMap.bucketName}.${configMap.bucketConfig.Location}.aliyuncs.com`
} else if (currentPicBedName.value === 'github') {
defaultUrl = 'main'
}
const res = await ipcRenderer.invoke('getBucketDomain', configMap.alias, param)
if (res.length > 0) {
customUrlList.value.length = 0
res.forEach((item: any) => {
if (!/^https?:\/\//.test(item) && currentPicBedName.value !== 'github') {
item = manageStore.config.settings.isForceCustomUrlHttps ? `https://${item}` : `http://${item}`
}
customUrlList.value.push({
label: item,
value: item
})
})
})
defaultUrl !== '' && currentPicBedName.value !== 'github' && customUrlList.value.push({
label: defaultUrl,
value: defaultUrl
})
currentCustomUrl.value = customUrlList.value[0].value
} else {
customUrlList.value.length = 0
customUrlList.value = [
{
defaultUrl !== '' && currentPicBedName.value !== 'github' && customUrlList.value.push({
label: defaultUrl,
value: defaultUrl
})
currentCustomUrl.value = customUrlList.value[0].value
} else {
customUrlList.value.length = 0
customUrlList.value = [
{
label: defaultUrl,
value: defaultUrl
}
]
currentCustomUrl.value = defaultUrl
}
} else if (['aliyun', 'tcyun', 'qiniu'].includes(currentPicBedName.value)) {
const currentConfigs = await getConfig<any>('picBed')
const currentConfig = currentConfigs[configMap.alias]
const currentTransformedConfig = JSON.parse(currentConfig.transformedConfig ?? '{}')
if (currentTransformedConfig[configMap.bucketName]) {
currentCustomUrl.value = currentTransformedConfig[configMap.bucketName].customUrl ?? ''
} else {
currentCustomUrl.value = ''
}
} else if (currentPicBedName.value === 's3plist') {
const currentConfigs = await getConfig<any>('picBed')
const currentConfig = currentConfigs[configMap.alias]
const currentTransformedConfig = JSON.parse(currentConfig.transformedConfig ?? '{}')
if (currentTransformedConfig[configMap.bucketName]) {
currentCustomUrl.value = currentTransformedConfig[configMap.bucketName].customUrl ?? ''
} else {
if (manageStore.config.picBed[configMap.alias].endpoint) {
const endpoint = manageStore.config.picBed[configMap.alias].endpoint
let url
if (/^https?:\/\//.test(endpoint)) {
url = new URL(endpoint)
} else {
url = new URL(manageStore.config.picBed[configMap.alias].sslEnabled ? 'https://' + endpoint : 'http://' + endpoint)
}
currentCustomUrl.value = `${url.protocol}//${configMap.bucketName}.${url.hostname}`
} else {
currentCustomUrl.value = `https://${configMap.bucketName}.s3.amazonaws.com`
}
]
currentCustomUrl.value = defaultUrl
}
handelChangeCustomUrl()
}
}
@ -2484,7 +2545,7 @@ const columns: Column<any>[] = [
>
自定义
</ElDropdownItem>
{['tcyun', 'aliyun', 'qiniu', 'github'].includes(currentPicBedName.value)
{ showPresignedUrl.value
? <ElDropdownItem
onClick={async () => {
const res = await getPreSignedUrl(item)

View File

@ -133,22 +133,50 @@
v-for="option in supportedPicBedList[item.icon].options"
:key="option"
:prop="item.icon + '.' + option"
:label="supportedPicBedList[item.icon].configOptions[option].description"
>
<template #label>
{{ supportedPicBedList[item.icon].configOptions[option].description }}
<el-tooltip
v-if="!!supportedPicBedList[item.icon].configOptions[option].tooltip"
effect="dark"
:content="supportedPicBedList[item.icon].configOptions[option].tooltip"
placement="right"
>
<el-icon
color="#409EFF"
>
<InfoFilled />
</el-icon>
</el-tooltip>
</template>
<el-input
v-if="option !== 'paging' && option !== 'itemsPerPage'"
v-if="supportedPicBedList[item.icon].configOptions[option].type === 'string'"
v-model.trim="configResult[item.icon + '.' + option]"
:placeholder="supportedPicBedList[item.icon].configOptions[option].placeholder"
/>
<el-switch
v-else-if="option === 'paging'"
v-else-if="supportedPicBedList[item.icon].configOptions[option].type === 'boolean'"
v-model="configResult[item.icon + '.' + option]"
active-color="#13ce66"
inactive-color="#ff4949"
/>
<el-input
v-else
v-else-if="supportedPicBedList[item.icon].configOptions[option].type === 'number'"
v-model.number="configResult[item.icon + '.' + option]"
:placeholder="supportedPicBedList[item.icon].configOptions[option].placeholder"
/>
<el-select
v-else-if="supportedPicBedList[item.icon].configOptions[option].type === 'select'"
v-model="configResult[item.icon + '.' + option]"
placeholder="请选择"
>
<el-option
v-for="i in Object.entries(supportedPicBedList[item.icon].configOptions[option].selectOptions)"
:key="i[0]"
:label="i[1] as string"
:value="i[0]"
/>
</el-select>
</el-form-item>
</el-form>
<div style="margin: 0 auto;position: relative;left: 10%;right: 50%;">
@ -220,7 +248,7 @@
<script lang="ts" setup>
import { reactive, ref, onBeforeMount, computed } from 'vue'
import { supportedPicBedList } from '../utils/constants'
import { Delete, Edit, Pointer } from '@element-plus/icons-vue'
import { Delete, Edit, Pointer, InfoFilled } from '@element-plus/icons-vue'
import { ElMessage, ElNotification } from 'element-plus'
import { getConfig, saveConfig, removeConfig } from '../utils/dataSender'
import { shell } from 'electron'
@ -314,7 +342,7 @@ const handleConfigChange = async (name: string) => {
for (const key of allKeys) {
const resultKey = name + '.' + key
if (supportedPicBedList[name].configOptions[key].required) {
if (key !== 'paging' && !configResult[resultKey]) {
if (supportedPicBedList[name].configOptions[key].type !== 'boolean' && !configResult[resultKey]) {
ElMessage.error(`请填写 ${supportedPicBedList[name].configOptions[key].description}`)
return
}
@ -329,7 +357,7 @@ const handleConfigChange = async (name: string) => {
}
if ((key === 'customUrl') && configResult[resultKey] !== undefined && configResult[resultKey] !== '') {
if (name !== 'upyun') {
if (!configResult[resultKey].startsWith('http://') && !configResult[resultKey].startsWith('https://')) {
if (!/^https?:\/\//.test(configResult[resultKey])) {
ElMessage.error('自定义域名必须以http://或https://开头')
return
}
@ -363,7 +391,7 @@ const handleConfigChange = async (name: string) => {
[bucketName[i]]: {
baseDir: baseDir && baseDir[i] ? baseDir[i] : '/',
area: area && area[i] ? area[i] : '',
customUrl: customUrl && customUrl[i] ? customUrl[i].startsWith('http') || customUrl[i].startsWith('https') ? customUrl[i] : 'http://' + customUrl[i] : '',
customUrl: customUrl && customUrl[i] ? /^https?:\/\//.test(customUrl[i]) ? customUrl[i] : 'http://' + customUrl[i] : '',
operator: operator && operator[i] ? operator[i] : '',
password: password && password[i] ? password[i] : ''
}
@ -489,10 +517,12 @@ function handleConfigImport (alias: string) {
const selectedConfig = existingConfiguration[alias]
if (selectedConfig) {
supportedPicBedList[selectedConfig.picBedName].options.forEach((option: any) => {
if (selectedConfig[option]) {
if (selectedConfig[option] !== undefined) {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
}
if (typeof selectedConfig[option] === 'boolean') {
configResult[selectedConfig.picBedName + '.' + option] = selectedConfig[option]
}
configResult[selectedConfig.picBedName + '.paging'] = selectedConfig.paging
})
}
}

View File

@ -315,7 +315,8 @@ const urlMap : IStringKeyMap = {
aliyun: 'https://oss.console.aliyun.com',
qiniu: 'https://portal.qiniu.com',
tcyun: 'https://console.cloud.tencent.com/cos',
upyun: 'https://console.upyun.com'
upyun: 'https://console.upyun.com',
s3plist: 'https://aws.amazon.com/cn/s3/'
}
const openPicBedUrl = () => shell.openExternal(urlMap[currentPagePicBedConfig.picBedName])
@ -438,6 +439,7 @@ const menuTitleMap:IStringKeyMap = {
qiniu: '存储桶',
tcyun: '存储桶',
upyun: '存储桶',
s3plist: '存储桶',
smms: '相册',
imgur: '相册',
github: '仓库'

View File

@ -89,13 +89,10 @@ export function formatFileName (fileName: string) {
return name.length > 20 ? `${name.slice(0, 20)}...${ext}` : fileName
}
export function getExtension (fileName: string) {
return path.extname(fileName).slice(1)
}
export const getExtension = (fileName: string) => path.extname(fileName).slice(1)
export function isImage (fileName: string) {
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'].includes(getExtension(fileName))
}
export const isImage = (fileName: string) =>
['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'].includes(getExtension(fileName))
export function formObjToTableData (obj: any) {
const exclude = [undefined, null, '', 'transformedConfig']
@ -126,21 +123,23 @@ export interface IHTTPProxy {
export const formatHttpProxy = (proxy: string | undefined, type: 'object' | 'string'): IHTTPProxy | undefined | string => {
if (proxy === undefined || proxy === '') return undefined
if (proxy.startsWith('http://') || proxy.startsWith('https://')) {
if (/^https?:\/\//.test(proxy)) {
const { protocol, hostname, port } = new URL(proxy)
if (type === 'string') return `${protocol}//${hostname}:${port}`
return {
host: hostname,
port: Number(port),
protocol: protocol.slice(0, -1)
}
return type === 'string'
? `${protocol}//${hostname}:${port}`
: {
host: hostname,
port: Number(port),
protocol: protocol.slice(0, -1)
}
} else {
const [host, port] = proxy.split(':')
if (type === 'string') return `http://${host}:${port}`
return {
host,
port: port ? Number(port) : 80,
protocol: 'http'
}
return type === 'string'
? `http://${host}:${port}`
: {
host,
port: port ? Number(port) : 80,
protocol: 'http'
}
}
}

View File

@ -51,6 +51,13 @@ const aliasRule = [
}
]
const aliasTooltip = '配置别名只能包含中文、英文、数字和下划线且不能超过15个字符'
const itemsPerPageTooltip = '每页显示数量必须在20-1000之间'
const pagingTooltip = '关闭分页时,目录列表将使用数据库缓存以优化性能'
const bucketNameTooltip = '英文逗号分隔bucket1,bucket2,bucket3和起始目录顺序一一对应'
const baseDirTooltip = '英文逗号分隔,如:/dir1,/dir2,/dir3和存储桶顺序一一对应'
const isAutoCustomUrlTooltip = '开启时,将自动获取存储桶绑定的域名,关闭时可手动填写域名'
export const supportedPicBedList: IStringKeyMap = {
smms: {
name: 'SM.MS',
@ -62,7 +69,8 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'smms-A'
default: 'smms-A',
tooltip: aliasTooltip
},
token: {
required: true,
@ -75,7 +83,8 @@ export const supportedPicBedList: IStringKeyMap = {
required: true,
description: '是否分页',
default: true,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
}
},
explain: '大陆地区请访问备用域名https://smms.app, 请勿大批量上传图片否则API接口会被限制',
@ -93,7 +102,8 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'qiniu-A'
default: 'qiniu-A',
tooltip: aliasTooltip
},
accessKey: {
required: true,
@ -113,31 +123,42 @@ export const supportedPicBedList: IStringKeyMap = {
required: false,
description: '空间名-可选',
placeholder: '英文逗号分隔例如bucket1,bucket2',
type: 'string'
type: 'string',
tooltip: bucketNameTooltip
},
baseDir: {
required: false,
description: '起始目录-可选',
placeholder: '英文逗号分隔,例如:/test1,/test2',
default: '/',
type: 'string'
type: 'string',
tooltip: baseDirTooltip
},
isAutoCustomUrl: {
required: true,
description: '是否自动获取绑定域名',
default: true,
type: 'boolean',
tooltip: isAutoCustomUrlTooltip
},
paging: {
required: true,
description: '是否分页',
default: true,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
},
itemsPerPage: {
required: true,
description: '每页显示数量',
default: 50,
type: 'number',
rule: itemsPerPageRule
rule: itemsPerPageRule,
tooltip: itemsPerPageTooltip
}
},
explain: '空间名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值',
options: ['alias', 'accessKey', 'secretKey', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'],
options: ['alias', 'accessKey', 'secretKey', 'bucketName', 'baseDir', 'isAutoCustomUrl', 'paging', 'itemsPerPage'],
refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-3',
referenceText: '配置教程请参考:'
},
@ -151,14 +172,16 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'github-A'
default: 'github-A',
tooltip: aliasTooltip
},
token: {
required: true,
description: 'token-必需',
placeholder: '请输入token',
type: 'string',
rule: defaultBaseRule('token')
rule: defaultBaseRule('token'),
tooltip: '请提供具有完整repo权限的token否则部分功能可能无法使用'
},
githubUsername: {
required: true,
@ -171,19 +194,22 @@ export const supportedPicBedList: IStringKeyMap = {
required: false,
description: '代理-可选',
placeholder: '例如http://127.0.0.1:1080',
type: 'string'
type: 'string',
tooltip: '如果您的网络环境需要使用代理才能访问GitHub请在此处填写代理地址'
},
paging: {
required: true,
description: '是否分页',
default: false,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
},
customUrl: {
required: false,
description: 'CDN加速域名-可选;例如: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}',
description: 'CDN加速域名-可选',
placeholder: '支持使用{username}、{repo}、{branch}和{path}作为替换占位符,用于适配不同仓库和分支',
type: 'string',
tooltip: '例如: https://cdn.staticaly.com/gh/{username}/{repo}@{branch}/{path}',
rule: [
{
validator: (_rule: any, value: any, callback: any) => {
@ -251,7 +277,8 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'aliyun-A'
default: 'aliyun-A',
tooltip: aliasTooltip
},
accessKeyId: {
required: true,
@ -271,31 +298,42 @@ export const supportedPicBedList: IStringKeyMap = {
required: false,
description: '存储桶名-可选',
placeholder: '英文逗号分隔例如bucket1,bucket2',
type: 'string'
type: 'string',
tooltip: bucketNameTooltip
},
baseDir: {
required: false,
description: '起始目录-可选',
placeholder: '英文逗号分隔,例如:/test1,/test2',
type: 'string',
default: '/'
default: '/',
tooltip: baseDirTooltip
},
isAutoCustomUrl: {
required: true,
description: '是否自动获取绑定域名',
default: true,
type: 'boolean',
tooltip: isAutoCustomUrlTooltip
},
paging: {
required: true,
description: '是否分页',
default: true,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
},
itemsPerPage: {
required: true,
description: '每页显示数量',
default: 50,
type: 'number',
rule: itemsPerPageRule
rule: itemsPerPageRule,
tooltip: itemsPerPageTooltip
}
},
explain: '存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值',
options: ['alias', 'accessKeyId', 'accessKeySecret', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'],
options: ['alias', 'accessKeyId', 'accessKeySecret', 'bucketName', 'baseDir', 'isAutoCustomUrl', 'paging', 'itemsPerPage'],
refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-1',
referenceText: '配置教程请参考:'
},
@ -309,7 +347,8 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'tcyun-A'
default: 'tcyun-A',
tooltip: aliasTooltip
},
secretId: {
required: true,
@ -330,37 +369,49 @@ export const supportedPicBedList: IStringKeyMap = {
description: 'appId-必需',
placeholder: '请输入appId',
type: 'string',
rule: defaultBaseRule('appId')
rule: defaultBaseRule('appId'),
tooltip: '例如1250000000'
},
bucketName: {
required: false,
description: '存储桶名-可选(注意包含AppId)',
placeholder: '英文逗号分隔例如bucket1-1250000000,bucket2-1250000000',
type: 'string'
type: 'string',
tooltip: bucketNameTooltip
},
baseDir: {
required: false,
description: '起始目录-可选',
placeholder: '英文逗号分隔,例如:/test1,/test2',
type: 'string',
default: '/'
default: '/',
tooltip: baseDirTooltip
},
isAutoCustomUrl: {
required: true,
description: '是否自动获取绑定域名',
default: true,
type: 'boolean',
tooltip: isAutoCustomUrlTooltip
},
paging: {
required: true,
description: '是否分页',
default: true,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
},
itemsPerPage: {
required: true,
description: '每页显示数量',
default: 50,
type: 'number',
rule: itemsPerPageRule
rule: itemsPerPageRule,
tooltip: itemsPerPageTooltip
}
},
explain: '存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值',
options: ['alias', 'secretId', 'secretKey', 'appId', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'],
options: ['alias', 'secretId', 'secretKey', 'appId', 'bucketName', 'baseDir', 'isAutoCustomUrl', 'paging', 'itemsPerPage'],
refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=%e5%8f%82%e6%95%b0%e8%af%b4%e6%98%8e-2',
referenceText: '配置教程请参考:'
},
@ -374,7 +425,8 @@ export const supportedPicBedList: IStringKeyMap = {
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 'upyun-A'
default: 'upyun-A',
tooltip: aliasTooltip
},
bucketName: {
required: true,
@ -445,14 +497,16 @@ export const supportedPicBedList: IStringKeyMap = {
required: true,
description: '是否分页',
default: true,
type: 'boolean'
type: 'boolean',
tooltip: pagingTooltip
},
itemsPerPage: {
required: true,
description: '每页显示数量',
default: 50,
type: 'number',
rule: itemsPerPageRule
rule: itemsPerPageRule,
tooltip: itemsPerPageTooltip
}
},
explain: '又拍云图床务必填写加速域名,否则无法正常使用',
@ -481,21 +535,131 @@ export const supportedPicBedList: IStringKeyMap = {
},
accessToken: {
required: true,
description: 'accessToken-必需(不是clientID,请参考配置教程)',
description: 'accessToken-必需',
placeholder: '请输入accessToken',
type: 'string',
rule: defaultBaseRule('accessToken')
rule: defaultBaseRule('accessToken'),
tooltip: '不是clientID,请参考配置教程'
},
proxy: {
required: false,
description: '代理-可选',
placeholder: '例如http://127.0.0.1:1080',
type: 'string'
type: 'string',
tooltip: '大陆地区请使用代理,否则无法正常使用'
}
},
explain: '大陆地区请使用代理API调用存在限制请注意使用频率',
options: ['alias', 'imgurUserName', 'accessToken', 'proxy'],
refLink: 'https://pichoro.horosama.com/#/PicHoroDocs/configure?id=imgur%e5%9b%be%e5%ba%8a-1',
referenceText: '配置教程请参考:'
},
s3plist: {
name: 'S3兼容云',
icon: 's3plist',
configOptions: {
alias: {
required: true,
description: '配置别名-必需',
placeholder: '该配置的唯一标识',
type: 'string',
rule: aliasRule,
default: 's3plist-A',
tooltip: aliasTooltip
},
accessKeyId: {
required: true,
description: 'accessKeyId-必需',
placeholder: '请输入accessKeyId',
type: 'string',
rule: defaultBaseRule('accessKeyId')
},
secretAccessKey: {
required: true,
description: 'secretAccessKey-必需',
placeholder: '请输入secretAccessKey',
type: 'string',
rule: defaultBaseRule('secretAccessKey')
},
endpoint: {
required: false,
description: 'endpoint-可选',
placeholder: '例如s3.us-east-1.amazonaws.com',
type: 'string',
tooltip: '如果不填写,默认访问 AWS S3请提供根API endpoint'
},
sslEnabled: {
required: true,
description: '使用HTTPS连接',
default: true,
type: 'boolean',
tooltip: '大部分平台都支持HTTPS连接如果您的平台不支持请关闭该选项'
},
s3ForcePathStyle: {
required: true,
description: '启用 S3 Path style',
default: false,
type: 'boolean',
tooltip: '例如使用 minio 时需要启用'
},
proxy: {
required: false,
description: '代理-可选',
placeholder: '例如http://127.0.0.1:1080',
type: 'string',
tooltip: '如果部分平台大陆地区无法访问,请使用代理'
},
aclForUpload: {
required: true,
description: '上传文件的权限',
rule: defaultBaseRule('aclForUpload'),
default: 'public-read',
type: 'select',
selectOptions: {
private: '私有',
'public-read': '公共读',
'public-read-write': '公共读写',
'authenticated-read': '授权读',
'bucket-owner-read': '桶所有者读',
'bucket-owner-full-control': '桶所有者完全控制',
'aws-exec-read': 'aws执行读'
},
tooltip: '上传文件的权限可选值private、public-read、public-read-write、authenticated-read、bucket-owner-read、bucket-owner-full-control、aws-exec-read'
},
bucketName: {
required: false,
description: '存储桶名-可选',
placeholder: '英文逗号分隔例如bucket1,bucket2',
type: 'string',
tooltip: bucketNameTooltip
},
baseDir: {
required: false,
description: '起始目录-可选',
placeholder: '英文逗号分隔,例如:/test1,/test2',
type: 'string',
default: '/',
tooltip: baseDirTooltip
},
paging: {
required: true,
description: '是否分页',
default: true,
type: 'boolean',
tooltip: pagingTooltip
},
itemsPerPage: {
required: true,
description: '每页显示数量',
default: 50,
type: 'number',
rule: itemsPerPageRule,
tooltip: itemsPerPageTooltip
}
},
explain: '存储桶名和起始目录配置时可通过英文逗号分隔不同存储桶的设置,顺序必须一致,逗号间留空或缺失项使用默认值',
options: ['alias', 'accessKeyId', 'secretAccessKey', 'endpoint', 'sslEnabled', 's3ForcePathStyle', 'proxy', 'aclForUpload', 'bucketName', 'baseDir', 'paging', 'itemsPerPage'],
refLink: 'https://github.com/wayjam/picgo-plugin-s3',
referenceText: '配置教程请参考:'
}
}

View File

@ -446,7 +446,6 @@ function remove (item: ImgInfo) {
}, 0)
}
}
console.log(file)
sendToMain('removeFiles', [file])
const obj = {
title: $T('OPERATION_SUCCEED'),

View File

@ -1,4 +1,4 @@
export const isUrl = (url: string): boolean => (url.startsWith('http://') || url.startsWith('https://'))
export const isUrl = (url: string): boolean => (/^https?:\/\//.test(url))
export const isUrlEncode = (url: string): boolean => {
url = url || ''
try {

1039
yarn.lock

File diff suppressed because it is too large Load Diff