/
picgoPostApi.ts
280 lines (251 loc) · 10.3 KB
/
picgoPostApi.ts
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
/*
* Copyright (c) 2023, Terwer . All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Terwer designates this
* particular file as subject to the "Classpath" exception as provided
* by Terwer in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com
* or visit www.terwer.space if you need additional information or have any
* questions.
*/
import { ImageParser } from "~/src/utils/parser/imageParser.ts"
import { SiyuanConfig, SiyuanKernelApi } from "zhi-siyuan-api"
import { createAppLogger } from "~/common/appLogger.ts"
import { ImageItem } from "~/src/models/imageItem.ts"
import { ParsedImage } from "~/src/models/parsedImage.ts"
import { PicgoPostResult } from "~/src/models/picgoPostResult.ts"
import { appConstants } from "~/src/appConstants.ts"
import { JsonUtil, StrUtil } from "zhi-common"
import { SiyuanDevice } from "zhi-device"
import { PicgoApi } from "~/src/service/picgoApi.js"
import { useExternalPicgoSettingStore } from "~/src/stores/useExternalPicgoSettingStore.ts"
import { useSiyuanDevice } from "~/src/composables/useSiyuanDevice.ts"
import { useSiyuanApi } from "~/src/composables/useSiyuanApi.ts"
/**
* Picgo与文章交互的通用方法
*/
export class PicgoPostApi {
private readonly logger = createAppLogger("picgo-post-api")
private readonly imageParser: ImageParser
private readonly siyuanApi: SiyuanKernelApi
private readonly siyuanConfig: SiyuanConfig
private readonly isSiyuanOrSiyuanNewWin: boolean
private readonly picgoApi: PicgoApi
/**
* 初始化思源笔记图床插件
*
* @param kApi - 可选,若不传递,使用默认的
*/
constructor(kApi?: SiyuanKernelApi) {
this.imageParser = new ImageParser()
this.picgoApi = new PicgoApi()
const { isInSiyuanOrSiyuanNewWin } = useSiyuanDevice()
this.isSiyuanOrSiyuanNewWin = isInSiyuanOrSiyuanNewWin()
const { siyuanConfig, kernelApi } = useSiyuanApi()
this.siyuanConfig = siyuanConfig
this.siyuanApi = kApi ?? kernelApi
}
/**
* 将字符串数组格式的图片信息转换成图片对象数组
*
* @param attrs 文章属性
* @param retImgs 字符串数组格式的图片信息
* @param imageBaseUrl - 本地图片前缀,一般是思源的地址
*/
public async doConvertImagesToImagesItemArray(
attrs: any,
retImgs: ParsedImage[],
imageBaseUrl?: string
): Promise<ImageItem[]> {
const ret = [] as ImageItem[]
for (let i = 0; i < retImgs.length; i++) {
const retImg = retImgs[i]
const originUrl = retImg.url
let imgUrl = retImg.url
// 获取属性存储的映射数据
let fileMap = {}
this.logger.debug("attrs=>", attrs)
if (!StrUtil.isEmptyString(attrs[appConstants.PICGO_FILE_MAP_KEY])) {
fileMap = JsonUtil.safeParse(attrs[appConstants.PICGO_FILE_MAP_KEY], {})
this.logger.debug("fileMap=>", fileMap)
}
// 处理思源本地图片预览
// 这个是从思源查出来解析的是否是本地
if (retImg.isLocal) {
let baseUrl = imageBaseUrl ?? this.siyuanConfig.apiUrl ?? ""
imgUrl = StrUtil.pathJoin(baseUrl, "/" + imgUrl)
}
const imageItem = new ImageItem(originUrl, imgUrl, retImg.isLocal, retImg.alt, retImg.title)
// fileMap 查出来的是是否上传,上传了,isLocal就false
if (fileMap[imageItem.hash]) {
const newImageItem = fileMap[imageItem.hash]
this.logger.debug("newImageItem=>", newImageItem)
if (!newImageItem.isLocal) {
imageItem.isLocal = false
imageItem.url = newImageItem.url
}
}
// imageItem.originUrl = decodeURIComponent(imageItem.originUrl)
imageItem.url = decodeURIComponent(imageItem.url)
this.logger.debug("imageItem=>", imageItem)
ret.push(imageItem)
}
this.logger.debug("ret=>", ret)
return ret
}
/**
* 上传当前文章图片到图床(提供给外部调用)
*
* @param pageId 文章ID
* @param attrs 文章属性
* @param mdContent 文章的Markdown文本
*/
public async uploadPostImagesToBed(pageId: string, attrs: any, mdContent: string): Promise<PicgoPostResult> {
const ret = new PicgoPostResult()
const localImages = this.imageParser.parseLocalImagesToArray(mdContent)
const uniqueLocalImages = [...new Set([...localImages])]
this.logger.debug("uniqueLocalImages=>", uniqueLocalImages)
if (uniqueLocalImages.length === 0) {
ret.flag = false
ret.hasImages = false
ret.mdContent = mdContent
ret.errmsg = "文章中没有图片"
return ret
}
// 开始上传
try {
ret.hasImages = true
const imageItemArray = await this.doConvertImagesToImagesItemArray(attrs, uniqueLocalImages)
const replaceMap = {}
let hasLocalImages = false
for (let i = 0; i < imageItemArray.length; i++) {
const imageItem = imageItemArray[i]
if (imageItem.originUrl.includes("assets")) {
replaceMap[imageItem.hash] = imageItem
}
if (!imageItem.isLocal) {
this.logger.debug("已经上传过图床,请勿重复上传=>", imageItem.originUrl)
continue
}
hasLocalImages = true
let newattrs: any
let isLocal: boolean
let newImageItem: ImageItem
try {
// 实际上传逻辑
await this.uploadSingleImageToBed(pageId, attrs, imageItem)
// 上传完成,需要获取最新链接
newattrs = await this.siyuanApi.getBlockAttrs(pageId)
isLocal = false
const newfileMap = JsonUtil.safeParse(newattrs[appConstants.PICGO_FILE_MAP_KEY], {})
newImageItem = newfileMap[imageItem.hash]
} catch (e) {
newattrs = attrs
isLocal = true
newImageItem = imageItem
this.logger.warn("单个图片上传异常", { pageId, attrs, imageItem })
this.logger.warn("单个图片上传失败,错误信息如下", e)
}
// 无论成功失败都要保存元数据,失败了当做本地图片
replaceMap[imageItem.hash] = new ImageItem(
newImageItem.originUrl,
newImageItem.url,
isLocal,
newImageItem.alt,
newImageItem.title
)
}
if (!hasLocalImages) {
// ElMessage.info("未发现本地图片,不上传!若之前上传过,将做链接替换")
this.logger.warn("未发现本地图片,不上传!若之前上传过,将做链接替换")
}
// 处理链接替换
this.logger.debug("准备替换正文图片,replaceMap=>", JSON.stringify(replaceMap))
this.logger.debug("开始替换正文,原文=>", JSON.stringify({ mdContent }))
ret.mdContent = this.imageParser.replaceImagesWithImageItemArray(mdContent, replaceMap)
this.logger.debug("图片链接替换完成,新正文=>", JSON.stringify({ newmdContent: ret.mdContent }))
ret.flag = true
this.logger.debug("正文替换完成,最终结果=>", ret)
} catch (e) {
ret.flag = false
ret.errmsg = e
this.logger.error("文章图片上传失败=>", e)
}
return ret
}
/**
* 上传单张图片到图床
*
* @param pageId 文章ID
* @param attrs 文章属性
* @param imageItem 图片信息
* @param forceUpload 强制上传
*/
public async uploadSingleImageToBed(
pageId: string,
attrs: any,
imageItem: ImageItem,
forceUpload?: boolean
): Promise<void> {
const mapInfoStr = attrs[appConstants.PICGO_FILE_MAP_KEY] ?? "{}"
const fileMap = JsonUtil.safeParse(mapInfoStr, {})
this.logger.warn("fileMap=>", fileMap)
// 处理上传
const filePaths = []
if (!forceUpload && !imageItem.isLocal) {
this.logger.warn("非本地图片,忽略=>", imageItem.url)
return
}
let imageFullPath: string
if (this.isSiyuanOrSiyuanNewWin) {
const win = SiyuanDevice.siyuanWindow()
const dataDir: string = win.siyuan.config.system.dataDir
imageFullPath = `${dataDir}/assets/${imageItem.name}`
this.logger.info(`Will upload picture from ${imageFullPath}, imageItem =>`, imageItem)
const fs = win.require("fs")
if (!fs.existsSync(imageFullPath)) {
imageFullPath = imageItem.url
}
} else {
imageFullPath = imageItem.url
}
this.logger.warn("isSiyuanOrSiyuanNewWin=>" + this.isSiyuanOrSiyuanNewWin + ", imageFullPath=>", imageFullPath)
filePaths.push(imageFullPath)
// 批量上传
const imageJson: any = await this.picgoApi.uploadByPicGO(filePaths)
this.logger.warn("图片上传完成,imageJson=>", imageJson)
const imageJsonObj = JsonUtil.safeParse(imageJson, []) as any
// 处理后续
if (imageJsonObj && imageJsonObj.length > 0) {
const img = imageJsonObj[0]
if (!img?.imgUrl || StrUtil.isEmptyString(img.imgUrl)) {
throw new Error(
"图片上传失败,可能原因:PicGO配置错误或者该平台不支持图片覆盖,请检查配置或者尝试上传新图片。请打开picgo.log查看更多信息"
)
}
const newImageItem = new ImageItem(imageItem.originUrl, img.imgUrl, false, imageItem.alt, imageItem.title)
fileMap[newImageItem.hash] = newImageItem
} else {
throw new Error("图片上传失败,可能原因:PicGO配置错误,请检查配置。请打开picgo.log查看更多信息")
}
this.logger.warn("newFileMap=>", fileMap)
const newFileMapStr = JSON.stringify(fileMap)
await this.siyuanApi.setBlockAttrs(pageId, {
[appConstants.PICGO_FILE_MAP_KEY]: newFileMapStr,
})
}
}