PicGo图床插件

PicGo 是一个用于快速上传图片并获取图片预览链接的工具,可以作为 Typora 在编辑 MarkDown 文档时插件处理图片保存的问题。

官方文档地址:https://picgo.github.io/PicGo-Core-Doc/zh/dev-guide/cli.html

PicGo 上传流程

core-lifecycle

可供开发的部分包括:两个模块(Transformer、Uploader)、三个生命周期函数(beforeTransformPlugins、beforeUploadPlugins、afterUploadPlugins)

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = (ctx) => {
const register = () => {
// 注册钩子函数
ctx.helper.uploader.register('wlyd', { handle: handleUploader, config })
ctx.helper.beforeTransformPlugins.register('wlyd', handleBeforeTransform)
}

return {
register, // 注册函数
uploader: 'wlyd' // 声明注册的uploader的id
}
}

注册模块(Transformer、Uploader):handle 执行函数;config 界面配置

1
ctx.helper.uploader.register('wlyd', { handle, config })

执行函数

1
2
3
const handle = (ctx) => {
// todo...
}

ctx 常用属性如下

  • output:Array 上传文件列表
  • getConfig、setConfig、saveConfig、unsetConfig、removeConfig 操作配置文件
  • Request.request 发起请求(v1.5.0后废弃,可以使用 request 方法)

界面配置

1
2
3
4
const config = (ctx) => {
// todo...
return [...]
}

配置对象

1
2
3
4
5
6
7
8
9
10
{
name: 'upload_guid', // 字段名称,读取时使用
type: 'input', // 展示类型
default: '', // 默认值
required: true, // 是否必须
alias: '上传GUID' // 字段别名,界面展示使用
choices: [ // 下拉框选项
{ name: '', value: '' }
]
}

type值

  • input:输入框

  • password

  • list:下拉框,必须包含 choices 属性

  • checkbox:多选下拉框,必须包含 choices 属性

  • confirm:开关

注意事项

上传完成后必要操作:

1
2
3
4
delete ctx.output[index].base64Image
delete ctx.output[index].buffer
// 更新图片链接,否则无法正常预览
ctx.output[index].imgUrl = link

完整demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const fs = require('fs')
const path = require('path')

/** 获取上传后图片的预览链接 */
const getPreviewLink = () => {
return ''
}

/** 获取上传链接 */
const getUploadLink = async () => {
return ''
}
/** 获取上传配置 */
const getUploadConfig = (uploadLink, fileName, file) => {
return {
method: 'POST',
url: uploadLink,
headers: {
contentType: 'multipart/form-data'
},
formData: {/** 上传参数 */}
}
}

const getImageBuffer = (image) => {
let buffer = image.buffer
if (!buffer && image.base64Image) {
buffer = Buffer.from(image.base64Image, 'base64')
}

return new Uint8Array(buffer)
}

const getFilePath = (fileName) => {
return path.join(__dirname, fileName)
}

/** 获取上传文件:先保存本地目录,然后再读取,最后删除本地文件 */
const getFile = async (image) => {
const filePath = getFilePath(image.fileName)
await fs.writeFileSync(filePath, getImageBuffer(image))
const stream = fs.createReadStream(filePath)
setTimeout(() => fs.unlink(getFilePath(image.fileName), () => {}), 100)

return stream
}

/** 判断是否上传成功 */
const isUploadSuccess = (data) => {
return true
}

/** 上传文件 */
const uploadFile = (ctx, config) => {
return async image => {
try {
const uploadLink = getUploadLink()
const file = await getFile(image)

const uploadConfig = getUploadConfig(uploadLink, image.fileName, file)
return await ctx.Request.request(uploadConfig)
} catch (error) {
ctx.emit('notification', { title: '上传失败', error })
throw new Error(JSON.stringify(error))
}
}
}

/** 图片重命名方式 */
const UPDATE_FILE_NAME_ACTION = {
time: (file) => {
const date = new Date()
const hour = `${date.getHours()}`.padStart(2, '0')
const minute = `${date.getMinutes()}`.padStart(2, '0')
const second = `${date.getSeconds()}`.padStart(2, '0')

return `${hour}${minute}${second}${date.getMilliseconds()}${file.extname}`
},
guid: (file) => {
let d = new Date().getTime()
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
}) + file.extname
}
}

/** 图片重命名 */
const updateFileName = (type, file) => {
return type in UPDATE_FILE_NAME_ACTION
? UPDATE_FILE_NAME_ACTION[type](file)
: file.fileName
}

/** 主函数 */
const handle = async (ctx) => {
// 获取 data.json 配置
const config = ctx.getConfig('picBed.wlyd')
const upload = uploadFile(ctx, config)

for (const key in ctx.output) {
const image = ctx.output[key]
image.fileName = updateFileName(config.rename_type, image)

const body = await upload(image)

if (isUploadSuccess(body)) {
delete ctx.output[key].base64Image
delete ctx.output[key].buffer
// 更新图片链接,否则 typora 无法正常预览
ctx.output[key].imgUrl = getPreviewLink(image.fileName, config.preview_guid)
}
}
}

/** 界面配置 */
const config = (ctx) => {
return [
{
name: 'upload_guid',
type: 'input',
default: '',
required: true,
alias: '上传GUID'
},
{
name: 'rename_type',
type: 'list',
default: 'time',
alias: '文件重命名',
choices: [
{ name: '关闭', value: 'close' },
{ name: '时间戳', value: 'time' },
{ name: 'GUID', value: 'guid' }
]
}
]
}

module.exports = (ctx) => {
const register = () => {
ctx.helper.uploader.register('wlyd', { handle, config })
}

return { uploader: 'wlyd', register }
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信