前言
用過幾款上傳圖片到圖床的軟體,但是自己常用的圖床,比如青雲物件儲存基本都沒有支援的。
剛好前幾天發現了一款可以自定義外掛的圖片上傳軟體 PicGo,藉此機會正好為其新增青雲物件儲存圖床的支援。
專案地址:picgo-plugin-qingstor-uploader
準備工作
外掛基於 PicGo-Core 開發,參閱開發文件 PicGo-Core-Doc 進行開發。
-
確保已安裝 Node.js 版本 >= 8
-
全域性安裝
yarn global add picgo # 或者 npm install picgo -g 複製程式碼
-
使用外掛模板
picgo init plugin <your-project-name> 複製程式碼
- 所有外掛以
picgo-plugin-xxx
的方式命名 - 按照提示配置你的專案
- 所有外掛以
開發外掛
picgo 是個上傳的流程系統。因此外掛其實就是針對這個流程系統的某個部件或者某些部件的開發。
附一下流程圖:
其中可以供開發的部件總共有5個:
兩個模組:
- Transformer
- Uploader
三個生命週期外掛入口:
- beforeTransformPlugins
- beforeUploadPlugins
- afterUploadPlugins
通常來說如果你只是要實現一個 picgo 預設不支援的圖床的話,你只需要開發一個
Uploader
。
我們這裡只是開發圖床的話就只需要開發 Uploader
即可。
這裡定位到專案的 src/index.ts
或 src/index.js
,
在這裡就是你所要支援圖床的配置的地方了。
圖床配置檔案
新增必須的配置項,新增圖床配置:
import { PluginConfig } from 'picgo/dist/utils/interfaces'
const config = (ctx: picgo): PluginConfig[] => {
let userConfig = ctx.getConfig('picBed.qingstor-uploader')
if (!userConfig) {
userConfig = {}
}
const config = [
{
name: 'accessKeyId',
type: 'input',
default: userConfig.accessKeyId || '',
message: 'AccessKeyId 不能為空',
required: true
},
{
name: 'accessKeySecret',
type: 'password',
default: userConfig.accessKeySecret || '',
message: 'AccessKeySecret 不能為空',
required: true
},
{
name: 'bucket',
type: 'input',
default: userConfig.bucket || '',
message: 'Bucket不能為空',
required: true
},
{
name: 'zone',
type: 'input',
alias: '區域',
default: userConfig.area || '',
message: '區域程式碼不能為空',
required: true
},
{
name: 'path',
type: 'input',
alias: '儲存路徑',
message: 'blog',
default: userConfig.path || '',
required: false
},
{
name: 'customUrl',
type: 'input',
alias: '私有云網址',
message: 'https://qingstor.com',
default: userConfig.customUrl || '',
required: false
}
]
return config
}
複製程式碼
簽名配置
根據青雲物件儲存簽名特點,使用 accessKeyId 和 accessKeySecret 生成上傳時的簽名。
-
首先觀察
strToSign
:strToSign = Verb + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Date + "\n" (+ Canonicalized Headers + "\n") + Canonicalized Resource 複製程式碼
這裡只上傳圖片,
Verb
就是PUT
,Date
使用new Date().toUTCString()
。考慮到簽名的複雜程度,上傳時不傳送 Content-MD5 和 Content-Type 請求頭以降低簽名方法的複雜度。
-
然後就是
Canonicalized Headers
:Canonicalized Headers 代表請求頭中以 x-qs- 開頭的欄位。如果該值為空,不保留空白行
這種自定義的請求頭肯定是沒有的,也可以去掉。
-
Canonicalized Resource 代表請求訪問的資源
預設形式:
/bucketName/path/fileName
考慮到
path
和fileName
可能的中文情況,需要對其 encode 一下。 -
對
strToSign
進行簽名將API金鑰的私鑰 (
accessKeySecret
) 作為 key,使用Hmac sha256
演算法給簽名串生成簽名, 然後將簽名進行 Base64 編碼,最後拼接簽名。
完整程式碼如下:
import crypto from 'crypto'
// generate QingStor signature
const generateSignature = (options: any, fileName: string): string => {
const date = new Date().toUTCString()
const strToSign = `PUT\n\n\n${date}\n/${options.bucket}/${encodeURI(options.path)}/${encodeURI(fileName)}`
const signature = crypto.createHmac('sha256', options.accessKeySecret).update(strToSign).digest('base64')
return `QS ${options.accessKeyId}:${signature}`
}
複製程式碼
protocol 和 host
對於配置了 customUrl
的私有云使用者,需要獲取到 protocol
和 host
。
const getHost = (customUrl: any): any => {
let protocol = 'https'
let host = 'qingstor.com'
if (customUrl) {
if (customUrl.startsWith('http://')) {
protocol = 'http'
host = customUrl.substring(7)
} else if (customUrl.startsWith('https://')) {
host = customUrl.substring(8)
} else {
host = customUrl
}
}
return {
protocol: protocol,
host: host
}
}
複製程式碼
配置 request
const postOptions = (options: any, fileName: string, signature: string, image: Buffer): any => {
const url = getHost(options.customUrl)
return {
method: 'PUT',
url: `${url.protocol}://${options.zone}.${url.host}/${options.bucket}/${encodeURI(options.path)}/${encodeURI(fileName)}`,
headers: {
Host: `${options.zone}.${url.host}`,
Authorization: signature,
Date: new Date().toUTCString()
},
body: image,
resolveWithFullResponse: true
}
}
複製程式碼
配置外掛 Plugin 的 handle
組合上述方法,處理上傳邏輯
const handle = async (ctx: picgo): Promise<picgo> => {
const qingstorOptions = ctx.getConfig('picBed.qingstor-uploader')
if (!qingstorOptions) {
throw new Error('Can\'t find the qingstor config')
}
try {
const imgList = ctx.output
const customUrl = qingstorOptions.customUrl
const path = qingstorOptions.path
for (let i in imgList) {
const signature = generateSignature(qingstorOptions, imgList[i].fileName)
let image = imgList[i].buffer
if (!image && imgList[i].base64Image) {
image = Buffer.from(imgList[i].base64Image, 'base64')
}
const options = postOptions(qingstorOptions, imgList[i].fileName, signature, image)
let body = await ctx.Request.request(options)
if (body.statusCode === 200 || body.statusCode === 201) {
delete imgList[i].base64Image
delete imgList[i].buffer
const url = getHost(customUrl)
imgList[i]['imgUrl'] = `${url.protocol}://${qingstorOptions.zone}.${url.host}/${qingstorOptions.bucket}/${encodeURI(path)}/${imgList[i].fileName}`
} else {
throw new Error('Upload failed')
}
}
return ctx
} catch (err) {
if (err.error === 'Upload failed') {
ctx.emit('notification', {
title: '上傳失敗!',
body: `請檢查你的配置項是否正確`
})
} else {
ctx.emit('notification', {
title: '上傳失敗!',
body: '請檢查你的配置項是否正確'
})
}
throw err
}
}
複製程式碼
註冊外掛
將 uploader 註冊即可:
export = (ctx: picgo) => {
const register = () => {
ctx.helper.uploader.register('qingstor-uploader', {
handle,
name: '青雲 QingStor',
config: config
})
}
return {
uploader: 'qingstor-uploader',
register
}
}
複製程式碼
釋出外掛
-
先登入 npm 賬號
npm login 複製程式碼
-
釋出到 npm 上就可以了
npm publish 複製程式碼