feat: add support for Cloudflare R2 using AWS S3 API (#484)
All checks were successful
Build and Deploy / build-and-deploy (push) Has been skipped

This commit is contained in:
renyang@lu 2024-12-20 13:19:19 +08:00 committed by GitHub
parent 1d6ab54091
commit f10c5e665f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 1775 additions and 10 deletions

View File

@ -43,16 +43,17 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
## 目前支持哪些图床 ## 目前支持哪些图床
| # | 图床 | 使用时是否需要配置 | 备注 | | # | 图床 | 使用时是否需要配置 | 备注 |
| --- | ----------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | --- | ------------------------------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| 1 | 默认 | 否 | - | | 1 | 默认 | 否 | - |
| 2 | [GitHub](https://github.com) | 配置 `Repo`、`Token` 参数 | [如何获取 GitHub token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) | | 2 | [GitHub](https://github.com) | 配置 `Repo`、`Token` 参数 | [如何获取 GitHub token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) |
| 3 | [阿里云](https://www.aliyun.com/product/oss) | 配置 `AccessKey ID`、`AccessKey Secret`、`Bucket`、`Region` 参数 | [如何使用阿里云 OSS](https://help.aliyun.com/document_detail/31883.html) | | 3 | [阿里云](https://www.aliyun.com/product/oss) | 配置 `AccessKey ID`、`AccessKey Secret`、`Bucket`、`Region` 参数 | [如何使用阿里云 OSS](https://help.aliyun.com/document_detail/31883.html) |
| 4 | [腾讯云](https://cloud.tencent.com/act/pro/cos) | 配置 `SecretId`、`SecretKey`、`Bucket`、`Region` 参数 | [如何使用腾讯云 COS](https://cloud.tencent.com/document/product/436/38484) | | 4 | [腾讯云](https://cloud.tencent.com/act/pro/cos) | 配置 `SecretId`、`SecretKey`、`Bucket`、`Region` 参数 | [如何使用腾讯云 COS](https://cloud.tencent.com/document/product/436/38484) |
| 5 | [七牛云](https://www.qiniu.com/products/kodo) | 配置 `AccessKey`、`SecretKey`、`Bucket`、`Domain`、`Region` 参数 | [如何使用七牛云 Kodo](https://developer.qiniu.com/kodo) | | 5 | [七牛云](https://www.qiniu.com/products/kodo) | 配置 `AccessKey`、`SecretKey`、`Bucket`、`Domain`、`Region` 参数 | [如何使用七牛云 Kodo](https://developer.qiniu.com/kodo) |
| 6 | [MinIO](https://min.io/) | 配置 `Endpoint`、`Port`、`UseSSL`、`Bucket`、`AccessKey`、`SecretKey` 参数 | [如何使用 MinIO](http://docs.minio.org.cn/docs/master/) | | 6 | [MinIO](https://min.io/) | 配置 `Endpoint`、`Port`、`UseSSL`、`Bucket`、`AccessKey`、`SecretKey` 参数 | [如何使用 MinIO](http://docs.minio.org.cn/docs/master/) |
| 7 | [公众号](https://mp.weixin.qq.com/) | 配置 `appID`、`appsecret`、`代理域名` 参数 | [如何获取公众号开发者 ID 密码?](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html) | | 7 | [公众号](https://mp.weixin.qq.com/) | 配置 `appID`、`appsecret`、`代理域名` 参数 | [如何获取公众号开发者 ID 密码?](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html) |
| 8 | 自定义上传 | 是 | [如何自定义上传?](#自定义上传逻辑) | | 8 | [Cloudflare R2](https://developers.cloudflare.com/r2/) | 配置 `AccountId`、`AccessKey`、`SecretKey`、`Bucket`、`Domain` 参数 | [如何使用 S3 API 操作 R2](https://developers.cloudflare.com/r2/api/s3/api/) |
| 9 | 自定义上传 | 是 | [如何自定义上传?](#自定义上传逻辑) |
![demo1](https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/demo1.gif) ![demo1](https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/demo1.gif)

1645
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
"postinstall": "simple-git-hooks && wxt prepare" "postinstall": "simple-git-hooks && wxt prepare"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.716.0",
"@vueuse/core": "^12.0.0", "@vueuse/core": "^12.0.0",
"axios": "^1.7.8", "axios": "^1.7.8",
"buffer-from": "^1.1.2", "buffer-from": "^1.1.2",

View File

@ -56,6 +56,15 @@ const minioOSS = ref({
secretKey: ``, secretKey: ``,
}) })
const formR2 = ref({
accountId: ``,
accessKey: ``,
secretKey: ``,
bucket: ``,
domain: ``,
path: ``,
})
const formMp = ref({ const formMp = ref({
proxyOrigin: ``, proxyOrigin: ``,
appID: ``, appID: ``,
@ -96,6 +105,10 @@ const options = [
value: `mp`, value: `mp`,
label: `公众号图床`, label: `公众号图床`,
}, },
{
value: `r2`,
label: `Cloudflare R2`,
},
{ {
value: `formCustom`, value: `formCustom`,
label: `自定义代码`, label: `自定义代码`,
@ -125,6 +138,9 @@ onBeforeMount(() => {
if (localStorage.getItem(`minioConfig`)) { if (localStorage.getItem(`minioConfig`)) {
minioOSS.value = JSON.parse(localStorage.getItem(`minioConfig`)!) minioOSS.value = JSON.parse(localStorage.getItem(`minioConfig`)!)
} }
if (localStorage.getItem(`r2Config`)) {
formR2.value = JSON.parse(localStorage.getItem(`r2Config`)!)
}
if (localStorage.getItem(`imgHost`)) { if (localStorage.getItem(`imgHost`)) {
imgHost.value = localStorage.getItem(`imgHost`)! imgHost.value = localStorage.getItem(`imgHost`)!
} }
@ -220,6 +236,23 @@ function saveQiniuConfiguration() {
toast.success(`保存成功`) toast.success(`保存成功`)
} }
function saveR2Configuration() {
if (
!(
formR2.value.accountId
&& formR2.value.accessKey
&& formR2.value.secretKey
&& formR2.value.bucket
&& formR2.value.domain
)
) {
toast.error(`Cloudflare R2参数配置不全`)
return
}
localStorage.setItem(`r2Config`, JSON.stringify(formR2.value))
toast.success(`保存成功`)
}
function saveMpConfiguration() { function saveMpConfiguration() {
if ( if (
!( !(
@ -315,6 +348,9 @@ function onDrop(e: DragEvent) {
<TabsTrigger value="mp"> <TabsTrigger value="mp">
公众号图床 公众号图床
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="r2">
Cloudflare R2
</TabsTrigger>
<TabsTrigger value="formCustom"> <TabsTrigger value="formCustom">
自定义代码 自定义代码
</TabsTrigger> </TabsTrigger>
@ -689,6 +725,58 @@ function onDrop(e: DragEvent) {
</FormItem> </FormItem>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="r2">
<div class="space-y-4">
<FormItem label="AccountId" required>
<Input v-model.trim="formR2.accountId" placeholder="如: 0030f123e55a57546f4c281c564e560" class="min-w-[350px]" />
</FormItem>
<FormItem label="AccessKey" required>
<Input v-model.trim="formR2.accessKey" placeholder="如: 358090b3a12824a6b0787gae7ad0fc72" />
</FormItem>
<FormItem label="SecretKey" required>
<Input
v-model.trim="formR2.secretKey" type="password"
placeholder="如: c1c4dbcb0b6b785ac6633422a06dff3dac055fe74fe40xj1b5c5fcf1bf128010"
/>
</FormItem>
<FormItem label="Bucket" required>
<Input v-model.trim="formR2.bucket" placeholder="如md" />
</FormItem>
<FormItem label="域名" required>
<Input v-model.trim="formR2.domain" placeholder="如https://oss.example.com" />
</FormItem>
<FormItem label="存储路径">
<Input v-model.trim="formR2.path" placeholder="如img可不填默认为根目录" />
</FormItem>
<FormItem>
<div class="flex flex-col items-start">
<Button
variant="link"
class="p-0"
as="a"
href="https://developers.cloudflare.com/r2/api/s3/api/"
target="_blank"
>
如何使用 S3 API 操作 Cloudflare R2
</Button>
<Button
variant="link"
class="p-0"
as="a"
href="https://developers.cloudflare.com/r2/buckets/cors/"
target="_blank"
>
如何设置跨域(CORS)
</Button>
</div>
</FormItem>
<FormItem>
<Button @click="saveR2Configuration">
保存配置
</Button>
</FormItem>
</div>
</TabsContent>
<TabsContent value="formCustom"> <TabsContent value="formCustom">
<CustomUploadForm /> <CustomUploadForm />
</TabsContent> </TabsContent>

View File

@ -3,6 +3,7 @@ import fetch from '@/utils/fetch'
import * as tokenTools from '@/utils/tokenTools' import * as tokenTools from '@/utils/tokenTools'
import { base64encode, safe64, utf16to8 } from '@/utils/tokenTools' import { base64encode, safe64, utf16to8 } from '@/utils/tokenTools'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import Buffer from 'buffer-from' import Buffer from 'buffer-from'
import COS from 'cos-js-sdk-v5' import COS from 'cos-js-sdk-v5'
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
@ -376,6 +377,33 @@ async function mpFileUpload(file: File) {
}) })
} }
// -----------------------------------------------------------------------
// Cloudflare R2 File Upload
// -----------------------------------------------------------------------
async function r2Upload(file: File) {
const { accountId, accessKey, secretKey, bucket, path, domain } = JSON.parse(
localStorage.getItem(`r2Config`)!,
)
const dir = path ? `${path}/` : ``
const filename = dir + getDateFilename(file.name)
const client = new S3Client({ region: `auto`, endpoint: `https://${accountId}.r2.cloudflarestorage.com`, credentials: { accessKeyId: accessKey, secretAccessKey: secretKey } })
return new Promise<string>((resolve, reject) => {
const putObjectCommand = new PutObjectCommand({
Bucket: bucket,
Key: filename,
ContentType: file.type,
Body: file,
})
client.send(putObjectCommand).then(() => {
resolve(`${domain}/${filename}`)
}).catch((err) => {
reject(err)
})
})
}
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// formCustom File Upload // formCustom File Upload
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -433,6 +461,8 @@ function fileUpload(content: string, file: File) {
return ghFileUpload(content, file.name) return ghFileUpload(content, file.name)
case `mp`: case `mp`:
return mpFileUpload(file) return mpFileUpload(file)
case `r2`:
return r2Upload(file)
case `formCustom`: case `formCustom`:
return formCustomUpload(content, file) return formCustomUpload(content, file)
default: default: