从零开始为 PicGo 开发一个新图床

前言

用过几款上传图片到图床的软件,但是自己常用的图床,比如青云对象存储基本都没有支持的。

刚好前几天发现了一款可以自定义插件的图片上传软件 PicGo,借此机会正好为其新增青云对象存储图床的支持。

picgo-qingstor-uploader-configuration.png

项目地址:picgo-plugin-qingstor-uploader

准备工作

插件基于 PicGo-Core 开发,参阅开发文档 PicGo-Core-Doc 进行开发。

  1. 确保已安装 Node.js 版本 >= 8

  2. 全局安装

    yarn global add picgo # 或者 npm install picgo -g
    
  3. 使用插件模板

    picgo init plugin <your-project-name>
    
    • 所有插件以 picgo-plugin-xxx 的方式命名
    • 按照提示配置你的项目

开发插件

picgo 是个上传的流程系统。因此插件其实就是针对这个流程系统的某个部件或者某些部件的开发。

附一下流程图:

picgo-core-fix.jpg

其中可以供开发的部件总共有5个:

两个模块:

  1. Transformer
  2. Uploader

三个生命周期插件入口:

  1. beforeTransformPlugins
  2. beforeUploadPlugins
  3. afterUploadPlugins

通常来说如果你只是要实现一个 picgo 默认不支持的图床的话,你只需要开发一个 Uploader

我们这里只是开发图床的话就只需要开发 Uploader 即可。


这里定位到项目的 src/index.tssrc/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 生成上传时的签名。

  1. 首先观察 strToSign :

    strToSign = Verb + "\n"
                  + Content-MD5 + "\n"
                  + Content-Type + "\n"
                  + Date + "\n"
                  (+ Canonicalized Headers + "\n")
                  + Canonicalized Resource
    

    这里只上传图片,Verb 就是 PUTDate 使用 new Date().toUTCString()

    考虑到签名的复杂程度,上传时不发送 Content-MD5 和 Content-Type 请求头以降低签名方法的复杂度。

  2. 然后就是 Canonicalized Headers :

    Canonicalized Headers 代表请求头中以 x-qs- 开头的字段。如果该值为空,不保留空白行

    这种自定义的请求头肯定是没有的,也可以去掉。

  3. Canonicalized Resource 代表请求访问的资源

    默认形式:/bucketName/path/fileName

    考虑到 pathfileName 可能的中文情况,需要对其 encode 一下。

  4. 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 的私有云用户,需要获取到 protocolhost

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

发布插件

  1. 先登录 npm 账号

    npm login
    
  2. 发布到 npm 上就可以了

    npm publish
    

文章作者: chengww
文章链接: https://chengww.com/archives/picgo-plugin-uploader-development.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 chengww's blog

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容