diff --git a/README.md b/README.md index d0576aa..4714c59 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![](./preview.png) -Your favorite PicGo image bed and PicGo plugins are still available in siyuan-notes, wuhu~ +Your favorite PicGo image bed is still available in siyuan-notes, wuhu~ > Important Note: > diff --git a/README_zh_CN.md b/README_zh_CN.md index 0116739..30b167b 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -4,7 +4,7 @@ ![](./preview.png) -您喜爱的 PicGo 图床与 PicGo 插件,在思源笔记依然可用,没想到吧~ +您喜爱的 PicGo 图床在思源笔记依然可用,没想到吧~ > 重要提示: > diff --git a/icon.png b/icon.png index 4ec61a0..e755af0 100644 Binary files a/icon.png and b/icon.png differ diff --git a/icon.psd b/icon.psd new file mode 100644 index 0000000..8537186 Binary files /dev/null and b/icon.psd differ diff --git a/libs/Universal-PicGo-Core/custom.d.ts b/libs/Universal-PicGo-Core/custom.d.ts index 1a74c95..ca22c44 100644 --- a/libs/Universal-PicGo-Core/custom.d.ts +++ b/libs/Universal-PicGo-Core/custom.d.ts @@ -1,4 +1,8 @@ declare module '*.json' { const value: { [key: string]: any } export default value -} \ No newline at end of file +} + +declare module "ali-oss" +declare module "arraybuffer-to-buffer" +declare module "upyun" \ No newline at end of file diff --git a/libs/Universal-PicGo-Core/package.json b/libs/Universal-PicGo-Core/package.json index 8e59a15..3589c5e 100644 --- a/libs/Universal-PicGo-Core/package.json +++ b/libs/Universal-PicGo-Core/package.json @@ -27,13 +27,16 @@ "devDependencies": { "@terwer/eslint-config-custom": "^1.3.6", "@terwer/vite-config-custom": "^0.7.6", + "@types/mime-types": "^2.1.4", "vite-plugin-node-polyfills": "^0.21.0" }, "dependencies": { "@picgo/i18n": "^1.0.0", + "ali-oss": "^6.20.0", "axios": "^1.6.8", "dayjs": "^1.11.10", "js-yaml": "^4.1.0", + "mime-types": "^2.1.35", "queue": "^7.0.0", "universal-picgo-store": "workspace:*", "zhi-lib-base": "^0.8.0" diff --git a/libs/Universal-PicGo-Core/src/core/ExternalPicgo.ts b/libs/Universal-PicGo-Core/src/core/ExternalPicgo.ts new file mode 100644 index 0000000..30f1a03 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/core/ExternalPicgo.ts @@ -0,0 +1,122 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ILogger, simpleLogger } from "zhi-lib-base" +import ExternalPicgoConfigDb from "../db/externalPicGo" +import { IImgInfo, IPicGo } from "../types" +import { PicgoTypeEnum } from "../utils/enums" +import { browserPathJoin } from "../utils/browserUtils" +import { isFileOrBlob } from "../utils/common" + +/** + *外部的PicGO 上传 Api + * + * @since 0.6.0 + * @version 1.6.0 + * @author terwer + */ +class ExternalPicgo { + private logger: ILogger + private requestUrl = "http://127.0.0.1:36677" + private readonly endpointUrl = "/upload" + public db: ExternalPicgoConfigDb + + constructor(ctx: IPicGo, isDev?: boolean) { + this.logger = simpleLogger("external-picgo", "external-picgo", isDev) + this.db = new ExternalPicgoConfigDb(ctx) + } + + /** + * 上传图片到PicGO + * + * @param input 路径数组,可为空,为空上传剪贴板 + */ + public async upload(input?: any[]): Promise { + const useBundledPicgo = this.db.get("useBundledPicgo") + const picgoType = this.db.get("picgoType") + if (useBundledPicgo) { + throw new Error("bundled picgo cannot use extenal picgo api") + } + if (picgoType !== PicgoTypeEnum.App) { + throw new Error(`picgoType ${picgoType} is not supported via external picgo api`) + } + + // check blob + let hasBlob = false + if (input) { + for (const inputItem of input) { + if (isFileOrBlob(inputItem) || typeof inputItem !== "string") { + hasBlob = true + break + } + } + } + if (hasBlob) { + throw new Error("blob is not supported via external picgo api") + } + + this.requestUrl = this.db.get("extPicgoApiUrl") ?? this.requestUrl + let ret: IImgInfo[] = [] + + const fetchOptions = { + method: "POST", + } + + let data + // 传递了路径,上传具体图片,否则上传剪贴板 + if (input) { + data = { list: input } + } + + // 数据不为空才传递 + if (data) { + Object.assign(fetchOptions, { + body: JSON.stringify(data), + }) + } + + Object.assign(fetchOptions, { + headers: { + "Content-Type": "application/json", + "User-Agent": "Terwer/0.1.0", + }, + }) + + // 发送请求 + const apiUrl = browserPathJoin(this.requestUrl, this.endpointUrl) + this.logger.debug("调用HTTP请求上传图片到PicGO,apiUrl=>", apiUrl) + this.logger.debug("调用HTTP请求上传图片到PicGO,fetchOps=>", fetchOptions) + + // 使用兼容的fetch调用并返回统一的JSON数据 + const response = await fetch(apiUrl, fetchOptions) + const resJson = await response.json() + this.logger.debug("调用HTTP请求上传图片到PicGO,resJson=>", resJson) + + if (resJson.success) { + const rtnArray: IImgInfo[] = [] + if (resJson.result && resJson.result.length > 0) { + resJson.result.forEach((img: string) => { + const rtnItem = { + fileName: img.substring(img.lastIndexOf("/") + 1), + imgUrl: img, + } + rtnArray.push(rtnItem) + }) + } + + ret = rtnArray + } else { + throw new Error("调用HTTP上传到PicGO失败,请检查配置=>" + resJson.message) + } + + return Promise.resolve(ret) + } +} + +export { ExternalPicgo } diff --git a/libs/Universal-PicGo-Core/src/core/Lifecycle.ts b/libs/Universal-PicGo-Core/src/core/Lifecycle.ts index 47cccdf..c28e497 100644 --- a/libs/Universal-PicGo-Core/src/core/Lifecycle.ts +++ b/libs/Universal-PicGo-Core/src/core/Lifecycle.ts @@ -12,7 +12,6 @@ import { ILifecyclePlugins, IPicGo, IPlugin, Undefinable } from "../types" import { ILogger } from "zhi-lib-base" import { createContext } from "../utils/createContext" import { IBuildInEvent } from "../utils/enums" -import { handleUrlEncode } from "../utils/common" export class Lifecycle extends EventEmitter { private readonly ctx: IPicGo @@ -114,11 +113,9 @@ export class Lifecycle extends EventEmitter { await this.handlePlugins(ctx.helper.afterUploadPlugins, ctx) let msg = "" const length = ctx.output.length - // notice, now picgo builtin uploader will encodeOutputURL by default - const isEncodeOutputURL = ctx.getConfig>("settings.encodeOutputURL") === true for (let i = 0; i < length; i++) { if (typeof ctx.output[i].imgUrl !== "undefined") { - msg += isEncodeOutputURL ? handleUrlEncode(ctx.output[i].imgUrl!) : ctx.output[i].imgUrl! + msg += ctx.output[i].imgUrl! if (i !== length - 1) { msg += "\n" } @@ -129,6 +126,7 @@ export class Lifecycle extends EventEmitter { ctx.emit(IBuildInEvent.FINISHED, ctx) if (msg === "") { ctx.log.warn("[after-upload] image upload occured an error, please read log for details") + throw new Error("image upload occured an error, please read log for details") } else { ctx.log.info(`[after-upload] upload finishied => \n${msg}`) } diff --git a/libs/Universal-PicGo-Core/src/core/UniversalPicGo.ts b/libs/Universal-PicGo-Core/src/core/UniversalPicGo.ts index 7ff8504..7ac7858 100644 --- a/libs/Universal-PicGo-Core/src/core/UniversalPicGo.ts +++ b/libs/Universal-PicGo-Core/src/core/UniversalPicGo.ts @@ -17,7 +17,6 @@ import { IPicGo, IPicGoPlugin, IPicGoPluginInterface, - IPicGoRequest, IPluginLoader, IStringKeyMap, } from "../types" @@ -32,7 +31,7 @@ import getClipboardImage from "../utils/getClipboardImage" import { IBuildInEvent, IBusEvent } from "../utils/enums" import ConfigDb from "../db/config" import { hasNodeEnv, win } from "universal-picgo-store" -import { ensureFileSync, pathExistsSync } from "../utils/nodeUtils" +import { ensureFileSync, ensureFolderSync, pathExistsSync } from "../utils/nodeUtils" import { I18nManager } from "../i18n" import { browserPathJoin, getBrowserDirectoryPath } from "../utils/browserUtils" import { isConfigKeyInBlackList, isInputConfigValid } from "../utils/common" @@ -52,9 +51,9 @@ class UniversalPicGo extends EventEmitter implements IPicGo { private _pluginLoader!: PluginLoader configPath: string baseDir!: string + pluginBaseDir!: string helper!: IHelper log: ILogger - // cmd: Commander output: IImgInfo[] input: any[] pluginHandler: PluginHandler @@ -63,8 +62,6 @@ class UniversalPicGo extends EventEmitter implements IPicGo { VERSION: string = process.env.PICGO_VERSION ?? "unknown" private readonly isDev: boolean - // GUI_VERSION?: string - get pluginLoader(): IPluginLoader { return this._pluginLoader } @@ -77,11 +74,12 @@ class UniversalPicGo extends EventEmitter implements IPicGo { return this.requestWrapper.PicGoRequest.bind(this.requestWrapper) } - constructor(configPath = "", isDev?: boolean) { + constructor(configPath?: string, pluginBaseDir?: string, isDev?: boolean) { super() this.isDev = isDev ?? false this.log = this.getLogger() - this.configPath = configPath + this.configPath = configPath ?? "" + this.pluginBaseDir = pluginBaseDir ?? "" this.output = [] this.input = [] this.helper = { @@ -92,10 +90,9 @@ class UniversalPicGo extends EventEmitter implements IPicGo { afterUploadPlugins: new LifecyclePlugins("afterUploadPlugins"), } this.initConfigPath() - // this.cmd = new Commander(this) + this.initConfig() this.pluginHandler = new PluginHandler(this) this.requestWrapper = new PicGoRequestWrapper(this) - this.initConfig() this.init() this.log.info("UniversalPicGo inited") @@ -116,18 +113,11 @@ class UniversalPicGo extends EventEmitter implements IPicGo { } } - // registerCommands(): void { - // if (this.configPath !== "") { - // this.cmd.init() - // this.cmd.loadCommands() - // } - // } - - getConfig(name?: string): T { + getConfig(name?: string, defaultValue?: any): T { if (!name) { return this._config as unknown as T } else { - return _.get(this._config, name) + return _.get(this._config, name, defaultValue) } } @@ -227,35 +217,56 @@ class UniversalPicGo extends EventEmitter implements IPicGo { // =================================================================================================================== - private initConfigPath(): void { - this.log.debug("win =>", win) - this.log.info(`hasNodeEnv => ${hasNodeEnv}`) + private getDefautBaseDir(): string { if (hasNodeEnv) { const os = win.require("os") const fs = win.fs const path = win.require("path") const { homedir } = os - if (this.configPath === "") { - this.configPath = homedir() + "/.universal-picgo/config.json" - } - if (path.extname(this.configPath).toUpperCase() !== ".JSON") { - this.configPath = "" - throw Error("The configuration file only supports JSON format.") - } - this.baseDir = path.dirname(this.configPath) - const exist = pathExistsSync(fs, path, this.configPath) - if (!exist) { - ensureFileSync(fs, path, `${this.configPath}`) + const dir = path.join(homedir(), ".universal-picgo") + ensureFolderSync(fs, dir) + return dir + } else { + return "universal-picgo" + } + } + + private initConfigPath(): void { + if (this.configPath === "") { + this.baseDir = this.getDefautBaseDir() + if (hasNodeEnv) { + const path = win.require("path") + this.configPath = path.join(this.baseDir, "picgo.cfg.json") + } else { + this.configPath = browserPathJoin(this.baseDir, "picgo.cfg.json") } } else { - if (this.configPath === "") { - this.baseDir = "universal-picgo" - this.configPath = browserPathJoin(this.baseDir, "config.json") + if (hasNodeEnv) { + const fs = win.fs + const path = win.require("path") + + if (path.extname(this.configPath).toUpperCase() !== ".JSON") { + this.configPath = "" + throw Error("The configuration file only supports JSON format.") + } + this.baseDir = path.dirname(this.configPath) + const exist = pathExistsSync(fs, path, this.configPath) + if (!exist) { + ensureFileSync(fs, path, `${this.configPath}`) + } } else { - // 模拟 path.dirname 的功能,获取路径的目录部分赋值给 baseDir this.baseDir = getBrowserDirectoryPath(this.configPath) } } + + if (this.pluginBaseDir === "") { + this.pluginBaseDir = this.getDefautBaseDir() + } + + this.log.debug("win =>", win) + this.log.info(`hasNodeEnv => ${hasNodeEnv}`) + this.log.info(`this.baseDir => ${this.baseDir}`) + this.log.info(`this.pluginBaseDir => ${this.pluginBaseDir}`) } private initConfig(): void { diff --git a/libs/Universal-PicGo-Core/src/db/config/index.ts b/libs/Universal-PicGo-Core/src/db/config/index.ts index f7eb527..25afb7d 100644 --- a/libs/Universal-PicGo-Core/src/db/config/index.ts +++ b/libs/Universal-PicGo-Core/src/db/config/index.ts @@ -7,29 +7,35 @@ * of this license document, but changing it is not allowed. */ -import { IConfig, IPicGo } from "../../types" +import { IConfig, IPicGo, IPicgoDb } from "../../types" import { IJSON, JSONStore } from "universal-picgo-store" -class ConfigDb { +class ConfigDb implements IPicgoDb { private readonly ctx: IPicGo private readonly db: JSONStore + public readonly key: string + public readonly initialValue = { + picBed: { + uploader: "smms", + current: "smms", + }, + picgoPlugins: {}, + } constructor(ctx: IPicGo) { this.ctx = ctx - this.db = new JSONStore(this.ctx.configPath) + this.key = this.ctx.configPath + this.db = new JSONStore(this.key) - this.safeSet("picBed", { - uploader: "smms", - current: "smms", - }) - this.safeSet("picgoPlugins", {}) + this.safeSet("picBed", this.initialValue.picBed) + this.safeSet("picgoPlugins", this.initialValue.picgoPlugins) } read(flush?: boolean): IJSON { return this.db.read(flush) } - get(key: ""): any { + get(key: string): any { this.read(true) return this.db.get(key) } diff --git a/libs/Universal-PicGo-Core/src/db/externalPicGo/index.ts b/libs/Universal-PicGo-Core/src/db/externalPicGo/index.ts new file mode 100644 index 0000000..e1f0951 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/db/externalPicGo/index.ts @@ -0,0 +1,91 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { IExternalPicgoConfig, IPicGo, IPicgoDb } from "../../types" +import { hasNodeEnv, IJSON, JSONStore, win } from "universal-picgo-store" +import { browserPathJoin } from "../../utils/browserUtils" +import { PicgoTypeEnum } from "../../utils/enums" + +class ExternalPicgoConfigDb implements IPicgoDb { + private readonly ctx: IPicGo + private readonly db: JSONStore + public readonly key: string + public readonly initialValue = { + useBundledPicgo: true, + picgoType: PicgoTypeEnum.Bundled, + extPicgoApiUrl: "http://127.0.0.1:36677", + } + + constructor(ctx: IPicGo) { + this.ctx = ctx + + if (hasNodeEnv) { + const path = win.require("path") + this.key = path.join(this.ctx.pluginBaseDir, "external-picgo-cfg.json") + } else { + this.key = browserPathJoin(this.ctx.pluginBaseDir, "external-picgo-cfg.json") + } + + this.db = new JSONStore(this.key) + + this.safeSet("useBundledPicgo", this.initialValue.useBundledPicgo) + this.safeSet("picgoType", this.initialValue.picgoType) + this.safeSet("extPicgoApiUrl", this.initialValue.extPicgoApiUrl) + } + + read(flush?: boolean): IJSON { + return this.db.read(flush) + } + + get(key: string): any { + this.read(true) + return this.db.get(key) + } + + set(key: string, value: any): void { + this.read(true) + return this.db.set(key, value) + } + + has(key: string): boolean { + this.read(true) + return this.db.has(key) + } + + unset(key: string, value: any): boolean { + this.read(true) + return this.db.unset(key, value) + } + + saveConfig(config: Partial): void { + Object.keys(config).forEach((name: string) => { + this.set(name, config[name]) + }) + } + + removeConfig(config: IExternalPicgoConfig): void { + Object.keys(config).forEach((name: string) => { + this.unset(name, config[name]) + }) + } + + // =================================================================================================================== + safeSet(key: string, value: any) { + if (!this.db.has(key)) { + try { + this.db.set(key, value) + } catch (e: any) { + this.ctx.log.error(e) + throw e + } + } + } +} + +export default ExternalPicgoConfigDb diff --git a/libs/Universal-PicGo-Core/src/db/pluginLoder/index.ts b/libs/Universal-PicGo-Core/src/db/pluginLoder/index.ts index 524ef5f..d0ce092 100644 --- a/libs/Universal-PicGo-Core/src/db/pluginLoder/index.ts +++ b/libs/Universal-PicGo-Core/src/db/pluginLoder/index.ts @@ -7,44 +7,42 @@ * of this license document, but changing it is not allowed. */ -import { IConfig, IPicGo } from "../../types" +import { IPicGo, IPicgoDb } from "../../types" import { hasNodeEnv, IJSON, JSONStore, win } from "universal-picgo-store" import { browserPathJoin } from "../../utils/browserUtils" -class PluginLoaderDb { +class PluginLoaderDb implements IPicgoDb { private readonly ctx: IPicGo private readonly db: JSONStore + public readonly key: string + public readonly initialValue = { + name: "picgo-plugins", + description: "picgo-plugins", + repository: "https://github.com/terwer/siyuan-plugin-picgo/tree/main/libs/Universal-PicGo-Core", + license: "MIT", + } constructor(ctx: IPicGo) { this.ctx = ctx - let packagePath: string if (hasNodeEnv) { const path = win.require("path") - packagePath = path.join(this.ctx.baseDir, "package.json") + this.key = path.join(this.ctx.pluginBaseDir, "package.json") } else { - packagePath = browserPathJoin(this.ctx.baseDir, "package.json") + this.key = browserPathJoin(this.ctx.pluginBaseDir, "package.json") } - this.db = new JSONStore(packagePath) + this.db = new JSONStore(this.key) - // const pkg = { - // name: "picgo-plugins", - // description: "picgo-plugins", - // repository: "https://github.com/PicGo/PicGo-Core", - // license: "MIT", - // } - this.safeSet("name", "picgo-plugins") - this.safeSet("description", "picgo-plugins") - this.safeSet("repository", "https://github.com/terwer/siyuan-plugin-picgo/tree/main/libs/Universal-PicGo-Core") - this.safeSet("license", "MIT") + // 初始化 + this.saveConfig(this.initialValue) } read(flush?: boolean): IJSON { return this.db.read(flush) } - get(key: ""): any { + get(key: string): any { this.read(true) return this.db.get(key) } @@ -64,13 +62,13 @@ class PluginLoaderDb { return this.db.unset(key, value) } - saveConfig(config: Partial): void { + saveConfig(config: Partial): void { Object.keys(config).forEach((name: string) => { this.set(name, config[name]) }) } - removeConfig(config: IConfig): void { + removeConfig(config: any): void { Object.keys(config).forEach((name: string) => { this.unset(name, config[name]) }) diff --git a/libs/Universal-PicGo-Core/src/i18n/browserI18nDb.ts b/libs/Universal-PicGo-Core/src/i18n/browserI18nDb.ts index 9ee1427..5316eba 100644 --- a/libs/Universal-PicGo-Core/src/i18n/browserI18nDb.ts +++ b/libs/Universal-PicGo-Core/src/i18n/browserI18nDb.ts @@ -36,7 +36,7 @@ class BrowserI18nDb { return this.db.unset(this.i18nKey, []) } - get(key: ""): any { + get(key: string): any { throw new Error("get is not supported by BrowserI18nDb") } diff --git a/libs/Universal-PicGo-Core/src/i18n/en.ts b/libs/Universal-PicGo-Core/src/i18n/en.ts index 11d16d0..0de074e 100644 --- a/libs/Universal-PicGo-Core/src/i18n/en.ts +++ b/libs/Universal-PicGo-Core/src/i18n/en.ts @@ -65,6 +65,20 @@ export const EN: ILocales = { PICBED_GITHUB_MESSAGE_PATH: "Ex. test/", PICBED_GITHUB_MESSAGE_CUSTOMURL: "Ex. https://test.com", + // Gitlab + PICBED_GITLAB: "Gitlab", + PICBED_GITLAB_URL: "Set Url", + PICBED_GITLAB_TOKEN: "Set Token", + PICBED_GITLAB_REPO: "Set Repo Name", + PICBED_GITLAB_BRANCH: "Set Branch", + PICBED_GITLAB_PATH: "Set Path", + PICBED_GITLAB_AUTHOR_MAIL: "Set Path", + PICBED_GITLAB_AUTHOR_NAME: "Set Path", + PICBED_GITLAB_COMMIT_MESSAGE: "Set Path", + PICBED_GITLAB_MESSAGE_URL: "Ex. http://localhost:8002", + PICBED_GITLAB_MESSAGE_REPO: "Ex. username/repo", + PICBED_GITLAB_MESSAGE_BRANCH: "Ex. main", + // qiniu PICBED_QINIU: "Qiniu", PICBED_QINIU_ACCESSKEY: "Set AccessKey", @@ -115,4 +129,9 @@ export const EN: ILocales = { PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "Plugin update failed", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "Plugin update failed, error code is ${code}, error log is \n ${data}", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "Plugin update failed, please enter a valid plugin name", + + // CORS + CORS_ANYWHERE_REQUIRED: + "You must config cors proxy before using bundled picgo in docker or browser, moredetail please contact youweics@163.com", + CORS_ANYWHERE_REQUIRED_LOCALHOST: "Local url must use local proxy in docker or browser environment", } diff --git a/libs/Universal-PicGo-Core/src/i18n/zh-CN.ts b/libs/Universal-PicGo-Core/src/i18n/zh-CN.ts index de17fe5..bc277d0 100644 --- a/libs/Universal-PicGo-Core/src/i18n/zh-CN.ts +++ b/libs/Universal-PicGo-Core/src/i18n/zh-CN.ts @@ -63,6 +63,20 @@ export const ZH_CN = { PICBED_GITHUB_MESSAGE_PATH: "例如:test/", PICBED_GITHUB_MESSAGE_CUSTOMURL: "例如:https://test.com", + // Gitlab + PICBED_GITLAB: "Gitlab", + PICBED_GITLAB_URL: "设定Url", + PICBED_GITLAB_TOKEN: "设定Token", + PICBED_GITLAB_REPO: "设定仓库名", + PICBED_GITLAB_BRANCH: "设定分支名", + PICBED_GITLAB_PATH: "设定存储路径", + PICBED_GITLAB_AUTHOR_MAIL: "设定提交人邮箱", + PICBED_GITLAB_AUTHOR_NAME: "设定提交人姓名", + PICBED_GITLAB_COMMIT_MESSAGE: "设定提交信息", + PICBED_GITLAB_MESSAGE_URL: "例如: http://localhost:8002", + PICBED_GITLAB_MESSAGE_REPO: "例如: username/repo", + PICBED_GITLAB_MESSAGE_BRANCH: "例如: main", + // qiniu PICBED_QINIU: "七牛云", PICBED_QINIU_ACCESSKEY: "设定AccessKey", @@ -110,6 +124,11 @@ export const ZH_CN = { PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "插件更新失败", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "插件更新失败,失败码为${code},错误日志为 \n ${data}", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "插件更新失败,请输入合法插件名", + + // CORS + CORS_ANYWHERE_REQUIRED: + "在Docker或浏览器中使用内置的PicGo之前,您必须配置CORS代理,如需获取更多详情,请联系youweics@163.com。", + CORS_ANYWHERE_REQUIRED_LOCALHOST: "在Docker或浏览器中本地地址必须使用本地代理", } export type ILocalesKey = keyof typeof ZH_CN diff --git a/libs/Universal-PicGo-Core/src/i18n/zh-TW.ts b/libs/Universal-PicGo-Core/src/i18n/zh-TW.ts index ff7f667..033e10e 100644 --- a/libs/Universal-PicGo-Core/src/i18n/zh-TW.ts +++ b/libs/Universal-PicGo-Core/src/i18n/zh-TW.ts @@ -65,6 +65,20 @@ export const ZH_TW: ILocales = { PICBED_GITHUB_MESSAGE_PATH: "例如:test/", PICBED_GITHUB_MESSAGE_CUSTOMURL: "例如:https://test.com", + // Gitlab + PICBED_GITLAB: "Gitlab", + PICBED_GITLAB_URL: "設定Url", + PICBED_GITLAB_TOKEN: "設定Token", + PICBED_GITLAB_REPO: "設定倉庫名", + PICBED_GITLAB_BRANCH: "設定分支名", + PICBED_GITLAB_PATH: "设定存储路径", + PICBED_GITLAB_AUTHOR_MAIL: "设定提交人邮箱", + PICBED_GITLAB_AUTHOR_NAME: "设定提交人姓名", + PICBED_GITLAB_COMMIT_MESSAGE: "设定提交信息", + PICBED_GITLAB_MESSAGE_URL: "例如: http://localhost:8002", + PICBED_GITLAB_MESSAGE_REPO: "例如: username/repo", + PICBED_GITLAB_MESSAGE_BRANCH: "例如: main", + // qiniu PICBED_QINIU: "七牛云", PICBED_QINIU_ACCESSKEY: "設定AccessKey", @@ -112,4 +126,9 @@ export const ZH_TW: ILocales = { PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "插件更新失敗", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "插件更新失敗,失敗碼為${code},錯誤紀錄為 \n ${data}", PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "插件更新失敗,請輸入正確的插件名稱", + + // CORS + CORS_ANYWHERE_REQUIRED: + "在Docker或瀏覽器中使用內建的PicGo之前,您必須配置CORS代理,如需獲取更多詳情,請聯繫youweics@163.com。", + CORS_ANYWHERE_REQUIRED_LOCALHOST: "在Docker或浏览器中本地地址必须使用本地代理", } diff --git a/libs/Universal-PicGo-Core/src/index.ts b/libs/Universal-PicGo-Core/src/index.ts index 79d9188..2d9de13 100644 --- a/libs/Universal-PicGo-Core/src/index.ts +++ b/libs/Universal-PicGo-Core/src/index.ts @@ -1,5 +1,37 @@ import { UniversalPicGo } from "./core/UniversalPicGo" -import { win, hasNodeEnv } from "universal-picgo-store" +import { ExternalPicgo } from "./core/ExternalPicgo" +import ConfigDb from "./db/config" +import PluginLoaderDb from "./db/pluginLoder" +import ExternalPicgoConfigDb from "./db/externalPicGo" +import { eventBus } from "./utils/eventBus" +import { currentWin, hasNodeEnv, parentWin, win } from "universal-picgo-store" +import { PicgoTypeEnum, IBusEvent } from "./utils/enums" +import { + IPicGo, + IImgInfo, + IPicgoDb, + IConfig, + IExternalPicgoConfig, + IPicBedType, + IUploaderConfigItem, + IUploaderConfigListItem, + IPluginConfig, +} from "./types" +import { isFileOrBlob } from "./utils/common" -export { UniversalPicGo } -export { win, hasNodeEnv } +export { UniversalPicGo, ExternalPicgo, eventBus } +export { ConfigDb, PluginLoaderDb, ExternalPicgoConfigDb } +export { PicgoTypeEnum, IBusEvent } +export { isFileOrBlob } +export { win, currentWin, parentWin, hasNodeEnv } +export { + type IPicGo, + type IImgInfo, + type IPicgoDb, + type IConfig, + type IExternalPicgoConfig, + type IPicBedType, + type IUploaderConfigItem, + type IUploaderConfigListItem, + type IPluginConfig, +} diff --git a/libs/Universal-PicGo-Core/src/lib/PicGoRequest.ts b/libs/Universal-PicGo-Core/src/lib/PicGoRequest.ts index 8ca9c18..d70cc3d 100644 --- a/libs/Universal-PicGo-Core/src/lib/PicGoRequest.ts +++ b/libs/Universal-PicGo-Core/src/lib/PicGoRequest.ts @@ -13,6 +13,9 @@ import { IConfig, IConfigChangePayload, IFullResponse, IPicGo, IResponse, Undefi import { ILogger } from "zhi-lib-base" import { eventBus } from "../utils/eventBus" import { IBusEvent } from "../utils/enums" +import { browserPathJoin } from "../utils/browserUtils" +import { hasNodeEnv } from "universal-picgo-store" +import { ILocalesKey } from "../i18n/zh-CN" // legacy request adaptor start // thanks for https://github.dev/request/request/blob/master/index.js @@ -35,37 +38,6 @@ function requestInterceptor(options: any | AxiosRequestConfig): AxiosRequestConf url: (options.url as string) || "", headers: options.headers || {}, } - // user request config proxy - if (options.proxy) { - let proxyOptions = options.proxy - if (typeof proxyOptions === "string") { - try { - proxyOptions = new URL(options.proxy) - } catch (e) { - proxyOptions = false - opt.proxy = false - console.error(e) - } - __isOldOptions = true - } - // if (proxyOptions) { - // if (options.url?.startsWith("https://")) { - // opt.proxy = false - // opt.httpsAgent = tunnel.httpsOverHttp({ - // proxy: { - // host: proxyOptions?.hostname, - // port: parseInt(proxyOptions?.port, 10), - // }, - // }) - // } else { - // opt.proxy = { - // host: proxyOptions.hostname, - // port: parseInt(proxyOptions.port, 10), - // protocol: "http", - // } - // } - // } - } if ("formData" in options) { const form = new FormData() as any for (const key in options.formData) { @@ -181,21 +153,6 @@ class PicGoRequestWrapper { this.options.headers = userOptions.headers || {} this.options.maxBodyLength = Infinity this.options.maxContentLength = Infinity - // const httpsAgent = new https.Agent({ - // maxVersion: 'TLSv1.2', - // minVersion: 'TLSv1.2' - // }) - // if (this.options.proxy && userOptions.url?.startsWith("https://")) { - // this.options.httpsAgent = tunnel.httpsOverHttp({ - // proxy: { - // host: this.options.proxy.host, - // port: this.options.proxy.port, - // }, - // }) - // this.options.proxy = false - // } else { - // this.options.httpsAgent = httpsAgent - // } this.logger.debug("PicGoRequest start request, options", this.options) const instance = axios.create(this.options) @@ -203,6 +160,21 @@ class PicGoRequestWrapper { // compatible with old request options to new options const opt = requestInterceptor(userOptions) + if (!hasNodeEnv && userOptions.proxy !== false) { + if (!this.proxy || this.proxy.trim() === "") { + throw new Error(this.ctx.i18n.translate("CORS_ANYWHERE_REQUIRED")) + } + if (opt.url?.includes("127.0.0.1") || opt.url?.includes("localhost")) { + // 本地地址需要配置本地代理才启用 + if (this.proxy?.includes("127.0.0.1") || this.proxy?.includes("localhost")) { + opt.url = browserPathJoin(this.proxy, opt.url ?? "") + } else { + throw new Error(this.ctx.i18n.translate("CORS_ANYWHERE_REQUIRED_LOCALHOST")) + } + } else { + opt.url = browserPathJoin(this.proxy, opt.url ?? "") + } + } const that = this instance.interceptors.request.use(function (obj) { @@ -231,21 +203,29 @@ class PicGoRequestWrapper { return resp } else { return instance.request(opt).then((res) => { + let customResp: any + // use old request option format - let oldResp: any if (opt.__isOldOptions) { if ("json" in userOptions) { if (userOptions.json) { - oldResp = res.data + customResp = res.data } } else { - oldResp = JSON.stringify(res.data) + customResp = JSON.stringify(res.data) } } else { - oldResp = res.data + // new resp + if (userOptions.responseType === "json") { + customResp = res.data + } else if (userOptions.responseType === "text") { + customResp = JSON.stringify(res.data) + } else { + customResp = res.data + } } - that.logger.debug("PicGoRequest request interceptor oldRequest, oldResp", oldResp) - return oldResp + that.logger.debug("PicGoRequest request interceptor oldRequest, oldResp", customResp) + return customResp }) as Promise> } } diff --git a/libs/Universal-PicGo-Core/src/lib/PluginLoader.ts b/libs/Universal-PicGo-Core/src/lib/PluginLoader.ts index 7e44243..d5de69a 100644 --- a/libs/Universal-PicGo-Core/src/lib/PluginLoader.ts +++ b/libs/Universal-PicGo-Core/src/lib/PluginLoader.ts @@ -38,8 +38,8 @@ export class PluginLoader implements IPluginLoader { if (hasNodeEnv) { const fs = win.fs const path = win.require("path") - const packagePath = path.join(this.ctx.baseDir, "package.json") - const pluginDir = path.join(this.ctx.baseDir, "node_modules/") + const packagePath = path.join(this.ctx.pluginBaseDir, "package.json") + const pluginDir = path.join(this.ctx.pluginBaseDir, "node_modules/") // Thanks to hexo -> https://github.com/hexojs/hexo/blob/master/lib/hexo/load_plugins.js if (!fs.existsSync(pluginDir)) { return false @@ -57,6 +57,14 @@ export class PluginLoader implements IPluginLoader { } return true } else { + const json = this.db.read(true) + const deps = Object.keys(json.dependencies || {}) + const devDeps = Object.keys(json.devDependencies || {}) + const modules = deps.concat(devDeps).filter((name: string) => { + if (!/^picgo-plugin-|^@[^/]+\/picgo-plugin-/.test(name)) return false + const path = this.resolvePlugin(this.ctx, name) + return false + }) this.logger.warn("load is not supported in browser") return false } @@ -130,7 +138,7 @@ export class PluginLoader implements IPluginLoader { } const path = win.require("path") - const pluginDir = path.join(this.ctx.baseDir, "node_modules/") + const pluginDir = path.join(this.ctx.pluginBaseDir, "node_modules/") // eslint-disable-next-line @typescript-eslint/no-var-requires const plugin = require(pluginDir + name)(this.ctx) this.pluginMap.set(name, plugin) @@ -162,7 +170,7 @@ export class PluginLoader implements IPluginLoader { private resolvePlugin(ctx: IPicGo, name: string): string { if (hasNodeEnv) { const path = win.require("path") - return path.join(ctx.baseDir, "node_modules", name) + return path.join(ctx.pluginBaseDir, "node_modules", name) } else { throw new Error("resolvePlugin is not supported in browser") } diff --git a/libs/Universal-PicGo-Core/src/plugins/transformer/path.ts b/libs/Universal-PicGo-Core/src/plugins/transformer/path.ts index 6fbe2d0..3242249 100644 --- a/libs/Universal-PicGo-Core/src/plugins/transformer/path.ts +++ b/libs/Universal-PicGo-Core/src/plugins/transformer/path.ts @@ -10,14 +10,29 @@ import dayjs from "dayjs" import { IImgInfo, IImgSize, IPathTransformedImgInfo, IPicGo } from "../../types" import { win } from "universal-picgo-store" -import { getFSFile, getImageSize, getURLFile, isUrl } from "../../utils/common" +import { Buffer } from "../../utils/nodePolyfill" +import { + getBase64File, + getBlobFile, + getFSFile, + getImageSize, + getURLFile, + isBase64, + isBuffer, + isFileOrBlob, + isUrl, +} from "../../utils/common" const handle = async (ctx: IPicGo): Promise => { const results: IImgInfo[] = ctx.output await Promise.all( - ctx.input.map(async (item: string | typeof win.Buffer, index: number) => { + ctx.input.map(async (item: string | Buffer | typeof win.Buffer, index: number) => { let info: IPathTransformedImgInfo - if (win.Buffer.isBuffer(item)) { + if (isFileOrBlob(item)) { + ctx.log.debug("using File or Blob in path transform") + info = await getBlobFile(item) + } else if (isBuffer(item)) { + ctx.log.debug("using buffer in path transform") info = { success: true, buffer: item, @@ -25,8 +40,13 @@ const handle = async (ctx: IPicGo): Promise => { extname: "", // will use getImageSize result } } else if (isUrl(item)) { + ctx.log.debug("using image url in path transform") info = await getURLFile(item, ctx) + } else if (isBase64(item)) { + ctx.log.debug("using image base64 in path transform") + info = await getBase64File(item) } else { + ctx.log.debug("using fs in path transform") info = await getFSFile(item) } if (info.success && info.buffer) { @@ -34,13 +54,14 @@ const handle = async (ctx: IPicGo): Promise => { const extname = info.extname || imgSize.extname || ".png" results[index] = { buffer: info.buffer, - fileName: info.fileName || `${dayjs().format("YYYYMMDDHHmmss")}${extname}}`, + fileName: info.fileName || `${dayjs().format("YYYYMMDDHHmmss")}${extname}`, width: imgSize.width, height: imgSize.height, extname, } } else { ctx.log.error(info.reason) + throw new Error(info.reason) } }) ) @@ -49,7 +70,11 @@ const handle = async (ctx: IPicGo): Promise => { return ctx } -const getImgSize = (ctx: IPicGo, file: typeof win.Buffer, path: string | typeof win.Buffer): IImgSize => { +const getImgSize = ( + ctx: IPicGo, + file: Buffer | typeof win.Buffer, + path: string | Buffer | typeof win.Buffer +): IImgSize => { const imageSize = getImageSize(file) if (!imageSize.real) { if (typeof path === "string") { diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/index.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/index.ts new file mode 100644 index 0000000..a42488d --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/index.ts @@ -0,0 +1,125 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { IAliyunConfig, IPicGo, IPluginConfig } from "../../../types" +import { ILocalesKey } from "../../../i18n/zh-CN" +import { hasNodeEnv } from "universal-picgo-store" +import { handleNode } from "./node" +import { handleWeb } from "./web" + +const handle = async (ctx: IPicGo): Promise => { + if (hasNodeEnv) { + return handleNode(ctx) + } + return handleWeb(ctx) +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.aliyun") || {} + const config: IPluginConfig[] = [ + { + name: "accessKeyId", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_ACCESSKEYID") + }, + default: userConfig.accessKeyId || "", + required: true, + }, + { + name: "accessKeySecret", + type: "password", + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_ACCESSKEYSECRET") + }, + default: userConfig.accessKeySecret || "", + required: true, + }, + { + name: "bucket", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_BUCKET") + }, + default: userConfig.bucket || "", + required: true, + }, + { + name: "area", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_ALICLOUD_AREA") + }, + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_AREA") + }, + default: userConfig.area || "", + get message() { + return ctx.i18n.translate("PICBED_ALICLOUD_MESSAGE_AREA") + }, + required: true, + }, + { + name: "path", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_ALICLOUD_PATH") + }, + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_PATH") + }, + get message() { + return ctx.i18n.translate("PICBED_ALICLOUD_MESSAGE_PATH") + }, + default: userConfig.path || "", + required: false, + }, + { + name: "customUrl", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_ALICLOUD_CUSTOMURL") + }, + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_CUSTOMURL") + }, + get message() { + return ctx.i18n.translate("PICBED_ALICLOUD_MESSAGE_CUSTOMURL") + }, + default: userConfig.customUrl || "", + required: false, + }, + { + name: "options", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_ALICLOUD_OPTIONS") + }, + get alias() { + return ctx.i18n.translate("PICBED_ALICLOUD_OPTIONS") + }, + get message() { + return ctx.i18n.translate("PICBED_ALICLOUD_MESSAGE_OPTIONS") + }, + default: userConfig.options || "", + required: false, + }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("aliyun", { + get name() { + return ctx.i18n.translate("PICBED_ALICLOUD") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/node.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/node.ts new file mode 100644 index 0000000..71cb1ea --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/node.ts @@ -0,0 +1,82 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +// noinspection ES6PreferShortImport + +import { IAliyunConfig, IPicGo } from "../../../types" +import { IBuildInEvent } from "../../../utils/enums" +import { ILocalesKey } from "../../../i18n/zh-CN" +import { base64ToBuffer } from "../../../utils/common" +import OSS from "ali-oss" + +const handleNode = async (ctx: IPicGo): Promise => { + const aliYunOptions = ctx.getConfig("picBed.aliyun") + if (!aliYunOptions) { + throw new Error("Can't find aliYun OSS config") + } + + const store = new OSS({ + region: aliYunOptions.area, + accessKeyId: aliYunOptions.accessKeyId, + accessKeySecret: aliYunOptions.accessKeySecret, + bucket: aliYunOptions.bucket, + }) + + const imgList = ctx.output + const customUrl = aliYunOptions.customUrl + const path = aliYunOptions.path + + for (const img of imgList) { + if (img.fileName) { + let image = img.buffer + if (!image && img.base64Image) { + image = base64ToBuffer(img.base64Image) + } + if (!image) { + ctx.log.error("Can not find image buffer") + throw new Error("Can not find image buffer") + } + try { + const optionUrl = aliYunOptions.options || "" + const remotePath = `${path}${img.fileName}${optionUrl}` + + const result = await store.put(remotePath, new Blob([image])) + console.log("Using aliyun SDK for upload, result=>", result) + + if (result?.res?.status && result.res.status === 200) { + delete img.base64Image + delete img.buffer + if (customUrl) { + img.imgUrl = `${customUrl}/${path}${img.fileName}${optionUrl}` + } else { + img.imgUrl = `https://${aliYunOptions.bucket}.${aliYunOptions.area}.aliyuncs.com/${path}${img.fileName}${optionUrl}` + } + } else { + throw new Error("Upload failed") + } + } catch (e: any) { + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + return ctx +} + +export { handleNode } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/web.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/web.ts new file mode 100644 index 0000000..965c687 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/aliyun/web.ts @@ -0,0 +1,109 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { IAliyunConfig, IPicGo } from "../../../types" +import crypto from "crypto" +import mime from "mime-types" +import { IBuildInEvent } from "../../../utils/enums" +import { ILocalesKey } from "../../../i18n/zh-CN" +import { AxiosRequestConfig } from "axios" +import { base64ToBuffer, safeParse } from "../../../utils/common" + +// generate OSS signature +const generateSignature = (options: IAliyunConfig, fileName: string): string => { + const date = new Date().toUTCString() + const mimeType = mime.lookup(fileName) + if (!mimeType) throw Error(`No mime type found for file ${fileName}`) + + const signString = `PUT\n\n${mimeType}\n${date}\n/${options.bucket}/${options.path}${fileName}` + + const signature = crypto.createHmac("sha1", options.accessKeySecret).update(signString).digest("base64") + return `OSS ${options.accessKeyId}:${signature}` +} + +const postOptions = ( + options: IAliyunConfig, + fileName: string, + signature: string, + image: Buffer +): AxiosRequestConfig => { + const xCorsHeaders = { + Host: `${options.bucket}.${options.area}.aliyuncs.com`, + Date: new Date().toUTCString(), + } + + return { + method: "PUT", + url: `https://${options.bucket}.${options.area}.aliyuncs.com/${encodeURI(options.path)}${encodeURI(fileName)}`, + headers: { + Authorization: signature, + "Content-Type": mime.lookup(fileName), + "x-cors-headers": JSON.stringify(xCorsHeaders), + }, + data: image, + resolveWithFullResponse: true, + } as AxiosRequestConfig +} + +const handleWeb = async (ctx: IPicGo): Promise => { + const aliYunOptions = ctx.getConfig("picBed.aliyun") + if (!aliYunOptions) { + throw new Error("Can't find aliYun OSS config") + } + + const imgList = ctx.output + const customUrl = aliYunOptions.customUrl + const path = aliYunOptions.path + for (const img of imgList) { + if (img.fileName) { + let image = img.buffer + if (!image && img.base64Image) { + image = base64ToBuffer(img.base64Image) + } + if (!image) { + ctx.log.error("Can not find image buffer") + throw new Error("Can not find image buffer") + } + try { + const signature = generateSignature(aliYunOptions, img.fileName) + const options = postOptions(aliYunOptions, img.fileName, signature, image) + const res: any = await ctx.request(options) + const body = safeParse(res) + if (body.statusCode === 200) { + delete img.base64Image + delete img.buffer + const optionUrl = aliYunOptions.options || "" + if (customUrl) { + img.imgUrl = `${customUrl}/${path}${img.fileName}${optionUrl}` + } else { + img.imgUrl = `https://${aliYunOptions.bucket}.${aliYunOptions.area}.aliyuncs.com/${path}${img.fileName}${optionUrl}` + } + } else { + throw new Error("Upload failed") + } + } catch (e: any) { + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + return ctx +} + +export { handleWeb } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/github.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/github.ts new file mode 100644 index 0000000..4ff7023 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/github.ts @@ -0,0 +1,203 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { AxiosRequestConfig } from "axios" +import { IGithubConfig, IPicGo, IPluginConfig } from "../../types" +import mime from "mime-types" +import { ILocalesKey } from "../../i18n/zh-CN" +import { bufferToBase64 } from "../../utils/common" +import { IBuildInEvent } from "../../utils/enums" + +const postOptions = (fileName: string, options: IGithubConfig, data: any): AxiosRequestConfig => { + const path = options.path || "" + const { token, repo } = options + return { + method: "PUT", + url: `https://api.github.com/repos/${repo}/contents/${encodeURI(path)}${encodeURIComponent(fileName)}`, + headers: { + Authorization: `token ${token}`, + // "User-Agent": "PicGo", + "Content-Type": mime.lookup(fileName), + }, + data: data, + // proxy=false 表示浏览器换无需代理也可以直接使用 + // 默认情况下浏览器需要设置代理 + proxy: false, + } as const +} + +const handle = async (ctx: IPicGo): Promise => { + const githubOptions = ctx.getConfig("picBed.github") + if (!githubOptions) { + throw new Error("Can't find github config") + } + try { + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName) { + let base64Image = img.base64Image + if (!base64Image && img.buffer) { + base64Image = bufferToBase64(img.buffer) + } + if (!base64Image) { + ctx.log.error("Can not find image base64") + throw new Error("Can not find image base64") + } + const data = { + message: "Upload by PicGo via siyuan-note", + branch: githubOptions.branch, + content: base64Image, + path: githubOptions.path + encodeURI(img.fileName), + } + const postConfig = postOptions(img.fileName, githubOptions, data) + try { + const body: { + content: { + download_url: string + } + } = await ctx.request(postConfig) + if (body) { + delete img.base64Image + delete img.buffer + if (githubOptions.customUrl) { + img.imgUrl = `${githubOptions.customUrl}/${encodeURI(githubOptions.path)}${encodeURIComponent( + img.fileName + )}` + } else { + img.imgUrl = body.content.download_url + } + } else { + throw new Error("Server error, please try again") + } + } catch (e: any) { + // handle duplicate images + if (e.statusCode === 422) { + delete img.base64Image + delete img.buffer + if (githubOptions.customUrl) { + img.imgUrl = `${githubOptions.customUrl}/${encodeURI(githubOptions.path)}${encodeURIComponent( + img.fileName + )}` + } else { + img.imgUrl = `https://raw.githubusercontent.com/${githubOptions.repo}/${githubOptions.branch}/${encodeURI( + githubOptions.path + )}${encodeURIComponent(img.fileName)}` + } + } else { + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body?.error ?? e.response?.body?.message ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + } + return ctx + } catch (err) { + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS_AND_NETWORK"), + }) + throw err + } +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.github") || {} + const config: IPluginConfig[] = [ + { + name: "repo", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITHUB_REPO") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITHUB_REPO") + }, + get message() { + return ctx.i18n.translate("PICBED_GITHUB_MESSAGE_REPO") + }, + default: userConfig.repo || "", + required: true, + }, + { + name: "branch", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITHUB_BRANCH") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITHUB_BRANCH") + }, + get message() { + return ctx.i18n.translate("PICBED_GITHUB_MESSAGE_BRANCH") + }, + default: userConfig.branch || "main", + required: true, + }, + { + name: "token", + type: "password", + get alias() { + return ctx.i18n.translate("PICBED_GITHUB_TOKEN") + }, + default: userConfig.token || "", + required: true, + }, + { + name: "path", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITHUB_PATH") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITHUB_PATH") + }, + get message() { + return ctx.i18n.translate("PICBED_GITHUB_MESSAGE_PATH") + }, + default: userConfig.path || "", + required: false, + }, + { + name: "customUrl", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITHUB_CUSTOMURL") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITHUB_CUSTOMURL") + }, + get message() { + return ctx.i18n.translate("PICBED_GITHUB_MESSAGE_CUSTOMURL") + }, + default: userConfig.customUrl || "", + required: false, + }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("github", { + get name() { + return ctx.i18n.translate("PICBED_GITHUB") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/gitlab.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/gitlab.ts new file mode 100644 index 0000000..e3aab58 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/gitlab.ts @@ -0,0 +1,213 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { IGitlabConfig, IPicGo } from "../../types" +import { ILocalesKey } from "../../i18n/zh-CN" +import { bufferToBase64, safeParse } from "../../utils/common" +import { AxiosRequestConfig } from "axios" +import { IBuildInEvent } from "../../utils/enums" + +const postOptions = (userConfig: IGitlabConfig, base64Image: string, fileName: string): AxiosRequestConfig => { + const body = { + branch: userConfig.branch, + author_email: userConfig.authorMail, + author_name: userConfig.authorName, + encoding: "base64", + commit_message: userConfig.commitMessage, + content: base64Image, + } + + const repo = encodeURIComponent(userConfig.repo) + const filepath = encodeURIComponent(userConfig.path + fileName) + return { + method: "POST", + url: `${userConfig.url}/api/v4/projects/${repo}/repository/files/${filepath}`, + headers: { + "Content-Type": "application/json", + // "User-Agent": "PicGo", + "PRIVATE-TOKEN": userConfig.token, + }, + data: body, + responseType: "json", + // proxy=false 表示浏览器换无需代理也可以直接使用 + // 默认情况下浏览器需要设置代理 + proxy: false, + } +} + +const handle = async (ctx: IPicGo): Promise => { + const userConfig = ctx.getConfig("picBed.gitlab") + if (!userConfig) { + throw new Error("Can not find gitlab config!") + } + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName) { + try { + let base64Image = img.base64Image + if (!base64Image && img.buffer) { + base64Image = bufferToBase64(img.buffer) + } + if (!base64Image) { + ctx.log.error("Can not find image base64") + throw new Error("Can not find image base64") + } + + const postConfig = postOptions(userConfig, base64Image, img.fileName) + const res: any = await ctx.request(postConfig) + const body = safeParse(res) + + delete img.base64Image + delete img.buffer + + // http://localhost:8002/api/v4/projects/terwer%2Fgitlab-upload/repository/files/img%2Fimage-20240321213215-uzrob4t.png + // 需要转换成 + // http://localhost:8002/api/v4/projects/terwer%2Fgitlab-upload/repository/files/img%2Fimage-20240321213215-uzrob4t.png/raw?private_token=glpat-xxxxxxxxxxxxxxxx + const repo = encodeURIComponent(userConfig.repo) + const filepath = encodeURIComponent(body.file_path) + // for private projects + // get token + // Preferences -> Access tokens + // img.imgUrl = `${userConfig.url}/api/v4/projects/${repo}/repository/files/${filepath}/raw?private_token=${userConfig.token}` + // + // change to public => Settings -> General + // Visibility, project features, permissions + img.imgUrl = `${userConfig.url}/api/v4/projects/${repo}/repository/files/${filepath}/raw` + } catch (e: any) { + let errMsg: any + // 处理重复图片 + if (e?.statusCode === 400 && e.response?.body?.message.indexOf("exists") > -1) { + delete img.base64Image + delete img.buffer + const originalUrl = e.url + img.imgUrl = originalUrl + } else { + if (e?.statusCode) { + errMsg = e.response?.body?.error ?? e.response?.body?.message ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + } + return ctx +} + +const config = (ctx: IPicGo) => { + const userConfig = ctx.getConfig("picBed.gitlab") || {} + return [ + { + name: "url", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITLAB_URL") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_URL") + }, + get message() { + return ctx.i18n.translate("PICBED_GITLAB_MESSAGE_URL") + }, + default: userConfig.url || "", + required: true, + }, + { + name: "repo", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITLAB_REPO") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_REPO") + }, + get message() { + return ctx.i18n.translate("PICBED_GITLAB_MESSAGE_REPO") + }, + default: userConfig.repo || "", + required: true, + }, + { + name: "branch", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_GITLAB_BRANCH") + }, + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_BRANCH") + }, + get message() { + return ctx.i18n.translate("PICBED_GITLAB_MESSAGE_BRANCH") + }, + default: userConfig.branch || "main", + required: true, + }, + { + name: "token", + type: "password", + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_TOKEN") + }, + default: userConfig.token || "", + required: true, + }, + { + name: "path", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_PATH") + }, + default: userConfig.path || "img/", + required: false, + }, + { + name: "authorMail", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_AUTHOR_MAIL") + }, + default: userConfig.authorMail || "youweics@163.com", + required: false, + }, + { + name: "authorName", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_AUTHOR_NAME") + }, + default: userConfig.authorName || "terwer", + required: false, + }, + { + name: "commitMessage", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_GITLAB_COMMIT_MESSAGE") + }, + default: userConfig.commitMessage || "upload by PicGo via siyuan-plugin-picgo", + required: false, + }, + ] +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("gitlab", { + get name() { + return ctx.i18n.translate("PICBED_GITLAB") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/imgur.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/imgur.ts new file mode 100644 index 0000000..0ba9319 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/imgur.ts @@ -0,0 +1,126 @@ +// noinspection ExceptionCaughtLocallyJS,SuspiciousTypeOfGuard + +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { IImgurConfig, IPicGo, IPluginConfig } from "../../types" +import { IBuildInEvent } from "../../utils/enums" +import { ILocalesKey } from "../../i18n/zh-CN" +import { AxiosRequestConfig } from "axios" +import { bufferToBase64 } from "../../utils/common" + +const postOptions = (options: IImgurConfig, fileName: string, imgBase64: string): AxiosRequestConfig => { + const clientId = options.clientId + + const formData = new FormData() + formData.append("image", imgBase64) + formData.append("type", "base64") + + return { + method: "POST", + url: "https://api.imgur.com/3/image", + headers: { + Authorization: `Client-ID ${clientId}`, + "content-type": "multipart/form-data", + // Host: "api.imgur.com", + // "User-Agent": "PicGo", + }, + data: formData, + } +} + +const handle = async (ctx: IPicGo): Promise => { + const imgurOptions = ctx.getConfig("picBed.imgur") + if (!imgurOptions) { + throw new Error("Can't find imgur config") + } + + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName) { + let base64Image = img.base64Image + if (!base64Image && img.buffer) { + base64Image = bufferToBase64(img.buffer) + } + if (!base64Image) { + ctx.log.error("Can not find image base64") + throw new Error("Can not find image base64") + } + const options = postOptions(imgurOptions, img.fileName, base64Image) + try { + const res: string = await ctx.request(options) + const body = typeof res === "string" ? JSON.parse(res) : res + if (body.success) { + delete img.base64Image + delete img.buffer + img.imgUrl = body.data.link + } else { + throw new Error("Server error, please try again") + } + } catch (e: any) { + let errMsg: any + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS_AND_NETWORK"), + text: "http://docs.imgur.com/api/errno/", + }) + if (e?.statusCode) { + errMsg = e.response?.body?.data.error ?? e.response?.body?.data ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + throw errMsg + } + } + } + return ctx +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.imgur") || {} + const config: IPluginConfig[] = [ + { + name: "clientId", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_IMGUR_CLIENTID") + }, + default: userConfig.clientId || "", + required: true, + }, + // use universal proxy instead + // { + // name: "proxy", + // type: "input", + // get prefix() { + // return ctx.i18n.translate("PICBED_IMGUR_PROXY") + // }, + // get alias() { + // return ctx.i18n.translate("PICBED_IMGUR_PROXY") + // }, + // get message() { + // return ctx.i18n.translate("PICBED_IMGUR_MESSAGE_PROXY") + // }, + // default: userConfig.proxy || "", + // required: false, + // }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("imgur", { + get name() { + return ctx.i18n.translate("PICBED_IMGUR") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/index.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/index.ts index f9dbc59..1eb46be 100644 --- a/libs/Universal-PicGo-Core/src/plugins/uploader/index.ts +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/index.ts @@ -8,11 +8,26 @@ */ import { IPicGo, IPicGoPlugin } from "../../types" +import githubUploader from "./github" +import gitlabUploader from "./gitlab" +import aliYunUploader from "./aliyun" +import tcYunUploader from "./tcyun" +import qiniuUploader from "./qiniu" +import upyunUploader from "./upyun" import SMMSUploader from "./smms" +import imgurUploader from "./imgur" + const buildInUploaders: IPicGoPlugin = () => { return { register(ctx: IPicGo) { + githubUploader(ctx) + gitlabUploader(ctx) + aliYunUploader(ctx) + tcYunUploader(ctx) + qiniuUploader(ctx) + upyunUploader(ctx) SMMSUploader(ctx) + imgurUploader(ctx) }, } } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/digest.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/digest.ts new file mode 100644 index 0000000..58353a0 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/digest.ts @@ -0,0 +1,22 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +class Mac { + accessKey: string + secretKey: string + options: any + + constructor(accessKey: string, secretKey: string, options?: Partial) { + this.accessKey = accessKey + this.secretKey = secretKey + this.options = { ...options } + } +} + +export { Mac } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/index.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/index.ts new file mode 100644 index 0000000..c459ae6 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/index.ts @@ -0,0 +1,212 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ILocalesKey } from "../../../i18n/zh-CN" +import { IPicGo, IPluginConfig, IQiniuConfig } from "../../../types" +import { IBuildInEvent } from "../../../utils/enums" +import { bufferToBase64, safeParse } from "../../../utils/common" +import mime from "mime-types" +import { AxiosRequestConfig } from "axios" +import { Mac } from "./digest" +import { PutPolicy } from "./rs" + +function postOptions(options: IQiniuConfig, fileName: string, token: string, imgBase64: string): AxiosRequestConfig { + const area = selectArea(options.area || "z0") + const path = options.path || "" + const base64FileName = Buffer.from(path + fileName, "utf-8") + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + return { + method: "POST", + url: `http://upload${area}.qiniup.com/putb64/-1/key/${base64FileName}`, + headers: { + Authorization: `UpToken ${token}`, + "Content-Type": mime.lookup(fileName) || "application/octet-stream", + }, + data: imgBase64, + // proxy=false 表示浏览器换无需代理也可以直接使用 + // 默认情况下浏览器需要设置代理 + proxy: false, + } +} + +function selectArea(area: string): string { + return area === "z0" ? "" : "-" + area +} + +function getToken(qiniuOptions: any): string { + const accessKey = qiniuOptions.accessKey + const secretKey = qiniuOptions.secretKey + const mac = new Mac(accessKey, secretKey) + const options = { + scope: qiniuOptions.bucket, + } + const putPolicy = new PutPolicy(options) + return putPolicy.uploadToken(mac) +} + +const handle = async (ctx: IPicGo): Promise => { + const qiniuOptions = ctx.getConfig("picBed.qiniu") + if (!qiniuOptions) { + throw new Error("Can't find qiniu config") + } + + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName && img.buffer) { + try { + let base64Image = img.base64Image + if (!base64Image && img.buffer) { + base64Image = bufferToBase64(img.buffer) + } + if (!base64Image) { + ctx.log.error("Can not find image base64") + throw new Error("Can not find image base64") + } + const options = postOptions(qiniuOptions, img.fileName, getToken(qiniuOptions), base64Image) + const res: any = await ctx.request(options) + const body = safeParse(res) + + if (body?.key) { + delete img.base64Image + delete img.buffer + const baseUrl = qiniuOptions.url + const options = qiniuOptions.options + img.imgUrl = `${baseUrl}/${body.key as string}${options}` + } else { + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: body.msg, + }) + ctx.log.error("qiniu error", body) + throw new Error("Upload failed") + } + } catch (e: any) { + if (e.message !== "Upload failed") { + // err.response maybe undefined + if (e.error) { + const errMsg = e.error + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: errMsg, + }) + throw errMsg + } + } + throw e + } + } + } + return ctx +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.qiniu") || {} + const config: IPluginConfig[] = [ + { + name: "accessKey", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_QINIU_ACCESSKEY") + }, + default: userConfig.accessKey || "", + required: true, + }, + { + name: "secretKey", + type: "password", + get alias() { + return ctx.i18n.translate("PICBED_QINIU_SECRETKEY") + }, + default: userConfig.secretKey || "", + required: true, + }, + { + name: "bucket", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_QINIU_BUCKET") + }, + default: userConfig.bucket || "", + required: true, + }, + { + name: "url", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_QINIU_URL") + }, + get alias() { + return ctx.i18n.translate("PICBED_QINIU_URL") + }, + get message() { + return ctx.i18n.translate("PICBED_QINIU_MESSAGE_URL") + }, + default: userConfig.url || "", + required: true, + }, + { + name: "area", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_QINIU_AREA") + }, + get alias() { + return ctx.i18n.translate("PICBED_QINIU_AREA") + }, + get message() { + return ctx.i18n.translate("PICBED_QINIU_MESSAGE_AREA") + }, + default: userConfig.area || "", + required: true, + }, + { + name: "options", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_QINIU_OPTIONS") + }, + get alias() { + return ctx.i18n.translate("PICBED_QINIU_OPTIONS") + }, + get message() { + return ctx.i18n.translate("PICBED_QINIU_MESSAGE_OPTIONS") + }, + default: userConfig.options || "", + required: false, + }, + { + name: "path", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_QINIU_PATH") + }, + get alias() { + return ctx.i18n.translate("PICBED_QINIU_PATH") + }, + get message() { + return ctx.i18n.translate("PICBED_QINIU_MESSAGE_PATH") + }, + default: userConfig.path || "", + required: false, + }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("qiniu", { + get name() { + return ctx.i18n.translate("PICBED_QINIU") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/rs.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/rs.ts new file mode 100644 index 0000000..7ba2149 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/rs.ts @@ -0,0 +1,94 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { Mac } from "./digest" +import { util } from "./util" + +// 用于与旧 SDK 版本兼容 +function _putPolicyBuildInKeys(): string[] { + return [ + "scope", + "isPrefixalScope", + "insertOnly", + "saveKey", + "forceSaveKey", + "endUser", + "returnUrl", + "returnBody", + "callbackUrl", + "callbackHost", + "callbackBody", + "callbackBodyType", + "callbackFetchKey", + "persistentOps", + "persistentNotifyUrl", + "persistentPipeline", + "fsizeLimit", + "fsizeMin", + "detectMime", + "mimeLimit", + "deleteAfterDays", + "fileType", + ] +} + +/** + * 上传策略 + * @link https://developer.qiniu.com/kodo/manual/1206/put-policy + */ +class PutPolicy { + private readonly expires: number + + constructor(options: any) { + if (typeof options !== "object") { + throw new Error("invalid putpolicy options") + } + + const that = this as any + Object.keys(options).forEach((k) => { + if (k === "expires") { + return + } + that[k] = options[k] + }) + + this.expires = options.expires || 3600 + _putPolicyBuildInKeys().forEach((k) => { + if ((this as any)[k] === undefined) { + that[k] = that[k] || null + } + }) + } + + getFlags(): any { + const that = this as any + const flags: any = {} + + Object.keys(this).forEach((k) => { + if (k === "expires" || that[k] === null) { + return + } + flags[k] = that[k] + }) + + flags.deadline = this.expires + Math.floor(Date.now() / 1000) + + return flags + } + + uploadToken(mac: Mac): string { + const flags = this.getFlags() + const encodedFlags = util.urlsafeBase64Encode(JSON.stringify(flags)) + const encoded = util.hmacSha1(encodedFlags, mac.secretKey) + const encodedSign = util.base64ToUrlSafe(encoded) + return [mac.accessKey, encodedSign, encodedFlags].join(":") + } +} + +export { PutPolicy } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/util.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/util.ts new file mode 100644 index 0000000..5e9904b --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/qiniu/util.ts @@ -0,0 +1,42 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { Buffer } from "../../../utils/nodePolyfill" +import crypto from "crypto" + +const base64ToUrlSafe = function (v: string) { + return v.replace(/\//g, "_").replace(/\+/g, "-") +} + +const urlSafeToBase64 = function (v: string) { + return v.replace(/_/g, "/").replace(/-/g, "+") +} + +// UrlSafe Base64 Decode +const urlsafeBase64Encode = function (jsonFlags: string) { + const encoded = Buffer.from(jsonFlags).toString("base64") + return base64ToUrlSafe(encoded) +} + +// UrlSafe Base64 Decode +const urlSafeBase64Decode = function (fromStr: string) { + return Buffer.from(urlSafeToBase64(fromStr), "base64").toString() +} + +// Hmac-sha1 Crypt +const hmacSha1 = (encodedFlags: string, secretKey: string) => { + // return value already encoded with base64 + const hmac = crypto.createHmac("sha1", secretKey) + hmac.update(encodedFlags) + return hmac.digest("base64") +} + +const util = { urlsafeBase64Encode, urlSafeBase64Decode, base64ToUrlSafe, urlSafeToBase64, hmacSha1 } + +export { util } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/smms.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/smms.ts index 96651f2..40de01b 100644 --- a/libs/Universal-PicGo-Core/src/plugins/uploader/smms.ts +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/smms.ts @@ -11,7 +11,7 @@ import { ILocalesKey } from "../../i18n/zh-CN" import { IPicGo, IPluginConfig, ISmmsConfig } from "../../types" import { IBuildInEvent } from "../../utils/enums" import { AxiosRequestConfig } from "axios" -import { safeParse } from "../../utils/common" +import { base64ToBuffer, safeParse } from "../../utils/common" const postOptions = (fileName: string, image: Buffer, apiToken: string, backupDomain = ""): AxiosRequestConfig => { const domain = backupDomain || "sm.ms" @@ -34,20 +34,24 @@ const postOptions = (fileName: string, image: Buffer, apiToken: string, backupDo } const handle = async (ctx: IPicGo): Promise => { - const smmsConfig = ctx.getConfig("picBed.smms") - if (!smmsConfig) { + const userConfig = ctx.getConfig("picBed.smms") + if (!userConfig) { throw new Error("Can not find smms config!") } const imgList = ctx.output for (const img of imgList) { - if (img.fileName && img.buffer) { + if (img.fileName) { let image = img.buffer if (!image && img.base64Image) { - image = Buffer.from(img.base64Image, "base64") + image = base64ToBuffer(img.base64Image) } - const postConfig = postOptions(img.fileName, image, smmsConfig?.token, smmsConfig?.backupDomain) + if (!image) { + ctx.log.error("Can not find image buffer") + throw new Error("Can not find image buffer") + } + const postConfig = postOptions(img.fileName, image, userConfig?.token, userConfig?.backupDomain) try { - const res: string = await ctx.request(postConfig) + const res: any = await ctx.request(postConfig) const body = safeParse(res) if (body.code === "success") { delete img.base64Image @@ -66,8 +70,14 @@ const handle = async (ctx: IPicGo): Promise => { throw new Error(body.message) } } catch (e: any) { - ctx.log.error(e) - throw e + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body?.error ?? e.response?.body?.message ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + throw errMsg } } } diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/tcyun.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/tcyun.ts new file mode 100644 index 0000000..1078133 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/tcyun.ts @@ -0,0 +1,330 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import crypto from "crypto" +import { ILocalesKey } from "../../i18n/zh-CN" +import { IPicGo, IPluginConfig, ITcyunConfig } from "../../types" +import mime from "mime-types" +import { Buffer } from "../../utils/nodePolyfill" +import { AxiosRequestConfig } from "axios" +import { base64ToBuffer, safeParse } from "../../utils/common" +import { IBuildInEvent } from "../../utils/enums" + +// generate COS signature string + +export interface ISignature { + signature: string + appId: string + bucket: string + signTime: string +} + +const generateSignature = (options: ITcyunConfig, fileName: string): ISignature => { + const secretId = options.secretId + const secretKey = options.secretKey + const appId = options.appId + const bucket = options.bucket + let signature + let signTime = "" + if (!options.version || options.version === "v4") { + const random = Math.floor(Math.random() * 10000000000) + const current = Math.floor(new Date().getTime() / 1000) - 1 + const expired = current + 3600 + + const multiSignature = `a=${appId}&b=${bucket}&k=${secretId}&e=${expired}&t=${current}&r=${random}&f=` + + const signHexKey = crypto.createHmac("sha1", secretKey).update(multiSignature).digest() + const tempString = Buffer.concat([signHexKey, Buffer.from(multiSignature)]) + signature = Buffer.from(tempString).toString("base64") + } else { + // https://cloud.tencent.com/document/product/436/7778#signature + const today = Math.floor(new Date().getTime() / 1000) + const tomorrow = today + 86400 + signTime = `${today};${tomorrow}` + const signKey = crypto.createHmac("sha1", secretKey).update(signTime).digest("hex") + const httpString = `put\n/${options.path}${fileName}\n\nhost=${options.bucket}.cos.${options.area}.myqcloud.com\n` + const sha1edHttpString = crypto.createHash("sha1").update(httpString).digest("hex") + const stringToSign = `sha1\n${signTime}\n${sha1edHttpString}\n` + signature = crypto.createHmac("sha1", signKey).update(stringToSign).digest("hex") + } + return { + signature, + appId, + bucket, + signTime, + } +} + +const postOptions = ( + options: ITcyunConfig, + fileName: string, + signature: ISignature, + image: Buffer, + version: string +): AxiosRequestConfig => { + const area = options.area + const path = options.path + if (!options.version || options.version === "v4") { + return { + method: "POST", + url: `http://${area}.file.myqcloud.com/files/v2/${signature.appId}/${signature.bucket}/${encodeURI( + path + )}${fileName}`, + headers: { + // Host: `${area}.file.myqcloud.com`, + Authorization: signature.signature, + contentType: "multipart/form-data", + userAgent: `PicGo;${version};null;null`, + }, + data: { + op: "upload", + filecontent: image, + }, + resolveWithFullResponse: true, + } as AxiosRequestConfig + } else { + return { + method: "PUT", + url: `http://${options.bucket}.cos.${options.area}.myqcloud.com/${encodeURIComponent(path)}${encodeURIComponent( + fileName + )}`, + headers: { + // Host: `${options.bucket}.cos.${options.area}.myqcloud.com`, + Authorization: `q-sign-algorithm=sha1&q-ak=${options.secretId}&q-sign-time=${signature.signTime}&q-key-time=${signature.signTime}&q-header-list=host&q-url-param-list=&q-signature=${signature.signature}`, + contentType: mime.lookup(fileName), + userAgent: `PicGo;${version};null;null`, + }, + data: image, + resolveWithFullResponse: true, + } as AxiosRequestConfig + } +} + +const handle = async (ctx: IPicGo): Promise => { + const tcYunOptions = ctx.getConfig("picBed.tcyun") + if (!tcYunOptions) { + throw new Error("Can't find tencent COS config") + } + + const imgList = ctx.output + const customUrl = tcYunOptions.customUrl + const path = tcYunOptions.path + const useV4 = !tcYunOptions.version || tcYunOptions.version === "v4" + + for (const img of imgList) { + if (img.fileName) { + const signature = generateSignature(tcYunOptions, img.fileName) + if (!signature) { + return false + } + + let image = img.buffer + if (!image && img.base64Image) { + image = base64ToBuffer(img.base64Image) + } + if (!image) { + ctx.log.error("Can not find image buffer") + throw new Error("Can not find image buffer") + } + + const options = postOptions(tcYunOptions, img.fileName, signature, image, ctx.VERSION) + try { + const res: any = await ctx.request(options) + + let body + if (useV4 && typeof res === "string") { + body = safeParse(res) + } else { + body = res + } + if (body.statusCode === 400) { + if (body?.body?.err) { + throw body.body.err + } else { + throw new Error(body?.body?.msg || body?.body?.message) + } + } + const optionUrl = tcYunOptions.options || "" + if (useV4 && body.message === "SUCCESS") { + delete img.base64Image + delete img.buffer + if (customUrl) { + img.imgUrl = `${customUrl}/${path}${img.fileName}` + } else { + img.imgUrl = `${body.data.source_url as string}${optionUrl}` + } + } else if (!useV4 && body && body.statusCode === 200) { + delete img.base64Image + delete img.buffer + if (customUrl) { + img.imgUrl = `${customUrl}/${encodeURI(path)}${encodeURI(img.fileName)}${optionUrl}` + } else { + img.imgUrl = `https://${tcYunOptions.bucket}.cos.${tcYunOptions.area}.myqcloud.com/${encodeURI( + path + )}${encodeURI(img.fileName)}${optionUrl}` + } + } else { + throw new Error(res.body.msg) + } + } catch (e: any) { + if (!tcYunOptions.version || tcYunOptions.version === "v4") { + const errObj = safeParse(e.error) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("UPLOAD_FAILED_REASON", { + code: errObj.code as string, + }), + text: "https://cloud.tencent.com/document/product/436/8432", + }) + } else { + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + } + return ctx +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.tcyun") || {} + const config: IPluginConfig[] = [ + { + name: "version", + type: "list", + alias: ctx.i18n.translate("PICBED_TENCENTCLOUD_VERSION"), + choices: ["v4", "v5"], + default: "v5", + required: false, + }, + { + name: "secretId", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_SECRETID") + }, + default: userConfig.secretId || "", + required: true, + }, + { + name: "secretKey", + type: "password", + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_SECRETKEY") + }, + default: userConfig.secretKey || "", + required: true, + }, + { + name: "bucket", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_BUCKET") + }, + default: userConfig.bucket || "", + required: true, + }, + { + name: "appId", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_APPID") + }, + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_APPID") + }, + default: userConfig.appId || "", + get message() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_MESSAGE_APPID") + }, + required: true, + }, + { + name: "area", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_AREA") + }, + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_AREA") + }, + default: userConfig.area || "", + get message() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_MESSAGE_AREA") + }, + required: true, + }, + { + name: "path", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_PATH") + }, + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_PATH") + }, + default: userConfig.path || "", + get message() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_MESSAGE_PATH") + }, + required: false, + }, + { + name: "customUrl", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_CUSTOMURL") + }, + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_CUSTOMURL") + }, + default: userConfig.customUrl || "", + get message() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_MESSAGE_CUSTOMURL") + }, + required: false, + }, + { + name: "options", + type: "input", + default: userConfig.options || "", + get prefix() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_OPTIONS") + }, + get alias() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_OPTIONS") + }, + get message() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD_MESSAGE_OPTIONS") + }, + required: false, + }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("tcyun", { + get name() { + return ctx.i18n.translate("PICBED_TENCENTCLOUD") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/plugins/uploader/upyun/index.ts b/libs/Universal-PicGo-Core/src/plugins/uploader/upyun/index.ts new file mode 100644 index 0000000..a675086 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/plugins/uploader/upyun/index.ts @@ -0,0 +1,248 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import crypto from "crypto" +import { ILocalesKey } from "../../../i18n/zh-CN" +import { IPicGo, IPluginConfig, IUpyunConfig } from "../../../types" +import { base64ToBuffer } from "../../../utils/common" +import { IBuildInEvent } from "../../../utils/enums" +import { Buffer } from "../../../utils/nodePolyfill" +import { AxiosRequestConfig } from "axios" + +// 封装计算MD5哈希的方法 +function calculateMD5(input: string) { + return crypto.createHash("md5").update(input).digest("hex") +} + +function hmacsha1(secret: string, value: string) { + return crypto.createHmac("sha1", secret).update(value, "utf8").digest().toString("base64") +} + +/** + * @param signKey + * @param method 当前请求的 API 使用的方法 + * @param uri 当前请求的资源路径 + * @param date + * @param policy + * @param contentMD5 内容的 md5 值 + */ +const getSignature = ( + signKey: string, + method: string, + uri: string, + date: string, + policy: string, + contentMD5 = null +) => { + // https://help.upyun.com/knowledge-base/audit_authorization/#e6b3a8e6848fe4ba8be9a1b9 + // https://docs.upyun.com/api/form_api/ + // https://docs.upyun.com/api/authorization/#http-header + // 签名计算方法 + // + // Authorization: UPYUN : + // Password = MD5(password) + // + // Signature = Base64 (HMAC-SHA1 (, + // & + // & + // & + // & + // + // )) + // 计算当前 api 请求签名信息 + let stringToSign = `${method}&${uri}&${date}&${policy}` + if (contentMD5) { + stringToSign += `&${contentMD5}` + } + + return hmacsha1(signKey, stringToSign) +} + +const postOptions = (options: IUpyunConfig, fileName: string, saveKey: string, image: Buffer): AxiosRequestConfig => { + // 计算当前时间的时间戳(单位:秒) + const currentTimeStamp = Math.floor(Date.now() / 1000) + // 设置过期时间为30分钟后 + const expirationTime = currentTimeStamp + 30 * 60 + + const date = new Date().toUTCString() + + const uploadArgs: any = { + bucket: options.bucket, + "save-key": saveKey, + expiration: expirationTime.toString(), + date: date, + } + const policy = Buffer.from(JSON.stringify(uploadArgs)).toString("base64") + const password = calculateMD5(options.password) + const signature = getSignature(password, "POST", `/${options.bucket}`, date, policy) + + const formData = new FormData() + formData.append("authorization", `UPYUN ${options.operator}:${signature}`) + formData.append("file", new Blob([image], { type: "image/png" }), fileName) + formData.append("policy", policy) + + return { + method: "POST", + url: `http://v0.api.upyun.com/${options.bucket}`, + headers: { + "Content-Type": "multipart/form-data", + }, + data: formData, + resolveWithFullResponse: true, + } as AxiosRequestConfig +} + +const handle = async (ctx: IPicGo): Promise => { + const upyunOptions = ctx.getConfig("picBed.upyun") + if (!upyunOptions) { + throw new Error("Can't find upYun config") + } + + const imgList = ctx.output + for (const img of imgList) { + if (img.fileName) { + let image = img.buffer + if (!image && img.base64Image) { + image = base64ToBuffer(img.base64Image) + } + if (!image) { + ctx.log.error("Can not find image buffer") + throw new Error("Can not find image buffer") + } + try { + const path = upyunOptions.path || "" + const saveKey = `${path}${img.fileName}${upyunOptions.options}` + + const options = postOptions(upyunOptions, img.fileName, saveKey, image) + const res: any = await ctx.request(options) + console.log("Using upyun SDK for upload, res=>", res) + + if (res) { + delete img.base64Image + delete img.buffer + img.imgUrl = `${upyunOptions.url}/${saveKey}` + } else { + throw new Error("Upload failed") + } + } catch (e: any) { + let errMsg: any + if (e?.statusCode) { + errMsg = e.response?.body?.message ?? e.stack ?? "unknown error" + } else { + errMsg = e.toString() + } + ctx.log.error(errMsg) + ctx.emit(IBuildInEvent.NOTIFICATION, { + title: ctx.i18n.translate("UPLOAD_FAILED"), + body: ctx.i18n.translate("CHECK_SETTINGS"), + }) + throw errMsg + } + } + } + return ctx +} + +const config = (ctx: IPicGo): IPluginConfig[] => { + const userConfig = ctx.getConfig("picBed.upyun") || {} + const config: IPluginConfig[] = [ + { + name: "bucket", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_BUCKET") + }, + default: userConfig.bucket || "", + required: true, + }, + { + name: "operator", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_OPERATOR") + }, + get prefix() { + return ctx.i18n.translate("PICBED_UPYUN_OPERATOR") + }, + get message() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_OPERATOR") + }, + default: userConfig.operator || "", + required: true, + }, + { + name: "password", + type: "password", + get prefix() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_PASSWORD") + }, + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_PASSWORD") + }, + get message() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_PASSWORD") + }, + default: userConfig.password || "", + required: true, + }, + { + name: "url", + type: "input", + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_URL") + }, + get message() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_URL") + }, + default: userConfig.url || "", + required: true, + }, + { + name: "options", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_UPYUN_OPTIONS") + }, + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_OPTIONS") + }, + get message() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_OPTIONS") + }, + default: userConfig.options || "", + required: false, + }, + { + name: "path", + type: "input", + get prefix() { + return ctx.i18n.translate("PICBED_UPYUN_PATH") + }, + get alias() { + return ctx.i18n.translate("PICBED_UPYUN_PATH") + }, + get message() { + return ctx.i18n.translate("PICBED_UPYUN_MESSAGE_PATH") + }, + default: userConfig.path || "", + required: false, + }, + ] + return config +} + +export default function register(ctx: IPicGo): void { + ctx.helper.uploader.register("upyun", { + get name() { + return ctx.i18n.translate("PICBED_UPYUN") + }, + handle, + config, + }) +} diff --git a/libs/Universal-PicGo-Core/src/types/index.d.ts b/libs/Universal-PicGo-Core/src/types/index.d.ts index eba0b03..08f8751 100644 --- a/libs/Universal-PicGo-Core/src/types/index.d.ts +++ b/libs/Universal-PicGo-Core/src/types/index.d.ts @@ -8,9 +8,11 @@ */ -import { EventEmitter, Buffer } from "../utils/nodePolyfill" +import { Buffer, EventEmitter } from "../utils/nodePolyfill" import { ILogger } from "zhi-lib-base" -import {AxiosRequestConfig} from "axios"; +import { AxiosRequestConfig } from "axios" +import { IJSON } from "universal-picgo-store" +import { PicgoTypeEnum } from "../utils/enums" export interface IPicGo extends EventEmitter { /** @@ -23,6 +25,12 @@ export interface IPicGo extends EventEmitter { * the picgo configPath's baseDir */ baseDir: string + /** + * default is baseDir, when set, using this value + * + * the picgo plugins baseDir, not to be confused with configPath's baseDir + */ + pluginBaseDir: string /** * picgo logger factory */ @@ -109,6 +117,7 @@ export interface ISmmsConfig { token: string backupDomain?: string } + /** 七牛云图床配置项 */ export interface IQiniuConfig { accessKey: string @@ -118,12 +127,13 @@ export interface IQiniuConfig { /** 自定义域名 */ url: string /** 存储区域编号 */ - area: 'z0' | 'z1' | 'z2' | 'na0' | 'as0' | string + area: "z0" | "z1" | "z2" | "na0" | "as0" | string /** 网址后缀,比如使用 `?imageslim` 可进行[图片瘦身](https://developer.qiniu.com/dora/api/1271/image-thin-body-imageslim) */ options: string /** 自定义存储路径,比如 `img/` */ path: string } + /** 又拍云图床配置项 */ export interface IUpyunConfig { /** 存储空间名,及你的服务名 */ @@ -139,6 +149,7 @@ export interface IUpyunConfig { /** 加速域名,注意要加 `http://` 或者 `https://` */ url: string } + /** 腾讯云图床配置项 */ export interface ITcyunConfig { secretId: string @@ -155,12 +166,13 @@ export interface ITcyunConfig { /** 自定义域名,注意要加 `http://` 或者 `https://` */ customUrl: string /** COS 版本,v4 或者 v5 */ - version: 'v5' | 'v4' + version: "v5" | "v4" /** 针对图片的一些后缀处理参数 PicGo 2.4.0+ PicGo-Core 1.5.0+ */ options: string /** 是否支持极智压缩 */ slim: boolean } + /** GitHub 图床配置项 */ export interface IGithubConfig { /** 仓库名,格式是 `username/reponame` */ @@ -174,6 +186,27 @@ export interface IGithubConfig { /** 分支名,默认是 `main` */ branch: string } + +/** Gitlab 图床配置项 */ +export interface IGitlabConfig { + /** gitlab 地址,例如:http://localhost:8002 */ + url: string + /** 仓库名,格式是 `username/reponame` */ + repo: string + /** 分支名,默认是 `main` */ + branch: string + /** gitlab token */ + token: string + /** 自定义存储路径,比如 `img/` */ + path: string + /** 作者邮箱 */ + authorMail: string + /** 作者姓名 */ + authorName: string + /** 提交信息 */ + commitMessage: string +} + /** 阿里云图床配置项 */ export interface IAliyunConfig { accessKeyId: string @@ -189,6 +222,7 @@ export interface IAliyunConfig { /** 针对图片的一些后缀处理参数 PicGo 2.2.0+ PicGo-Core 1.4.0+ */ options: string } + /** Imgur 图床配置项 */ export interface IImgurConfig { /** imgur 的 `clientId` */ @@ -228,6 +262,7 @@ export interface IConfig { npmProxy?: string [others: string]: any } + [configOptions: string]: any } @@ -240,10 +275,11 @@ export interface IPlugin { name?: string /** The config of this handler */ config?: (ctx: IPicGo) => IPluginConfig[] + [propName: string]: any } -export type IPluginNameType = 'simple' | 'scope' | 'normal' | 'unknown' +export type IPluginNameType = "simple" | "scope" | "normal" | "unknown" export interface ILocale { [key: string]: any @@ -271,6 +307,7 @@ export interface IImgInfo { height?: number extname?: string imgUrl?: string + [propName: string]: any } @@ -328,6 +365,7 @@ export interface IPicGoPluginInterface { /** * for picgo gui plugins */ + // guiMenu?: (ctx: IPicGo) => IGuiMenuItem[] /** @@ -349,6 +387,7 @@ export interface IPluginConfig { default?: any alias?: string message?: string + // prefix?: string // for cli options [propName: string]: any } @@ -397,6 +436,7 @@ export interface IPlugin { name?: string /** The config of this handler */ config?: (ctx: IPicGo) => IPluginConfig[] + [propName: string]: any } @@ -468,7 +508,7 @@ export interface IConfigChangePayload { // ===================================================================================================================== // request start -export type IPicGoRequest = (config: U) => Promise> +export type IPicGoRequest = (config: U) => Promise> /** * for PicGo new request api, the response will be json format @@ -481,7 +521,7 @@ export type IReqOptions = AxiosRequestConfig & { * for PicGo new request api, the response will be Buffer */ export type IReqOptionsWithArrayBufferRes = IReqOptions & { - responseType: 'arraybuffer' + responseType: "arraybuffer" } /** @@ -494,9 +534,9 @@ export type IFullResponse = AxiosResponse & { body: T } -type AxiosResponse = import('axios').AxiosResponse +type AxiosResponse = import("axios").AxiosResponse -type AxiosRequestConfig = import('axios').AxiosRequestConfig +type AxiosRequestConfig = import("axios").AxiosRequestConfig interface IRequestOptionsWithFullResponse { resolveWithFullResponse: true @@ -507,7 +547,7 @@ interface IRequestOptionsWithJSON { } interface IRequestOptionsWithResponseTypeArrayBuffer { - responseType: 'arraybuffer' + responseType: "arraybuffer" } /** @@ -515,8 +555,78 @@ interface IRequestOptionsWithResponseTypeArrayBuffer { * U is the config type */ export type IResponse = U extends IRequestOptionsWithFullResponse ? IFullResponse - : U extends IRequestOptionsWithJSON ? T - : U extends IRequestOptionsWithResponseTypeArrayBuffer ? Buffer - : U extends IReqOptionsWithBodyResOnly ? T - : string -// request end \ No newline at end of file + : U extends IRequestOptionsWithJSON ? T + : U extends IRequestOptionsWithResponseTypeArrayBuffer ? Buffer + : U extends IReqOptionsWithBodyResOnly ? T + : string + +// request end + +/** + * 外部 Picgo 配置接口 + */ +interface IExternalPicgoConfig { + useBundledPicgo?: boolean + + picgoType?: PicgoTypeEnum + + /** + * extPicgoApiUrl 是一个字符串,表示外部 Picgo API 的 URL + */ + extPicgoApiUrl?: string + + + /** + * 其他配置项,可以是任意类型 + */ + [key: string]: any +} + +/** + * PicGo 统一存储接口 + */ +interface IPicgoDb { + key: string + + initialValue: any + + read(flush?: boolean): IJSON + + get(key: string): any + + set(key: string, value: any): void + + has(key: string): boolean + + unset(key: string, value: any): boolean + + saveConfig(config: Partial): void + + removeConfig(config: T): void +} + +/** + * 图床类型定义 + */ +interface IPicBedType { + type: string + name: string + visible: boolean +} + +/** + * 某个PicGO平台配置列表 + */ +interface IUploaderConfigItem { + configList: IUploaderConfigListItem[] + defaultId: string +} + +type IUploaderConfigListItem = IStringKeyMap & IUploaderListItemMetaInfo + +interface IUploaderListItemMetaInfo { + _id: string + _configName: string + _updatedAt: number + _createdAt: number +} \ No newline at end of file diff --git a/libs/Universal-PicGo-Core/src/utils/browserUtils.ts b/libs/Universal-PicGo-Core/src/utils/browserUtils.ts index 0919eb6..0de2339 100644 --- a/libs/Universal-PicGo-Core/src/utils/browserUtils.ts +++ b/libs/Universal-PicGo-Core/src/utils/browserUtils.ts @@ -27,11 +27,16 @@ export const browserPathJoin = (...paths: string[]) => { // 过滤掉空路径 const filteredPaths = paths.filter((path) => path && path.trim() !== "") - // 使用斜杠连接路径 - const joinedPath = filteredPaths.join("/") + // 如果路径数组为空,则返回空字符串 + if (filteredPaths.length === 0) { + return "" + } + + // 连接所有路径 + let joinedPath = filteredPaths.join("/") // 处理多余的斜杠 - const normalizedPath = joinedPath.replace(/\/{2,}/g, "/") + joinedPath = joinedPath.replace(/([^:/]|^)\/{2,}/g, "$1/") - return normalizedPath + return joinedPath } diff --git a/libs/Universal-PicGo-Core/src/utils/common.ts b/libs/Universal-PicGo-Core/src/utils/common.ts index 2722b48..a46f4b5 100644 --- a/libs/Universal-PicGo-Core/src/utils/common.ts +++ b/libs/Universal-PicGo-Core/src/utils/common.ts @@ -10,24 +10,74 @@ import { IImgSize, IPathTransformedImgInfo, IPicGo, IPluginNameType } from "../types" import { hasNodeEnv, win } from "universal-picgo-store" import imageSize from "./image-size" +import { calculateHash } from "./hashUtil" +import { Buffer } from "./nodePolyfill" export const isUrl = (url: string): boolean => url.startsWith("http://") || url.startsWith("https://") -export const isUrlEncode = (url: string): boolean => { - url = url || "" - try { - // the whole url encode or decode shold not use encodeURIComponent or decodeURIComponent - return url !== decodeURI(url) - } catch (e) { - // if some error caught, try to let it go - return false +/** + * 检测输入是否为 base64 编码的字符串 + * + * @param input - 输入字符串或 Buffer + * @returns- 如果是 base64 编码则返回 true,否则返回 false + */ +export const isBase64 = (input: any) => { + if (typeof input === "string") { + // 检查字符串是否为 base64 编码 + return /^data:image\/[a-zA-Z]*;base64,/.test(input) } + + // 如果输入不是字符串,则直接返回 false + return false } -export const handleUrlEncode = (url: string): string => { - if (!isUrlEncode(url)) { - url = encodeURI(url) + +function extractImageInfoFromBase64(base64ImageData: string): any { + const mimeAndBase64Regex = new RegExp("data:([^;]+);base64,(.+)") + const match = base64ImageData.match(mimeAndBase64Regex) + + if (match) { + const mimeType = match[1] + const base64Data = match[2] + + // 提取 mime 类型的基础文件扩展名 + const ext = mimeType.split("/")[1] + + // 使用 HashUtil.calculateHash 函数生成默认图片名称 + const imageName = `${calculateHash(base64Data)}.${ext}` + + return { + mimeType, + imageBase64: base64Data, + imageName, + } + } else { + throw new Error("Mime type and base64 data extraction failed") + } +} + +export const base64ToBuffer = (base64: string): Buffer | typeof win.Buffer => { + let imageBuffer + if (hasNodeEnv) { + imageBuffer = win.Buffer.from(base64, "base64") + } else { + imageBuffer = Buffer.from(base64, "base64") + } + return imageBuffer +} + +export const bufferToBase64 = (buffer: Buffer | typeof win.Buffer) => { + return buffer.toString("base64") +} + +export const getBase64File = async (base64: string): Promise => { + const imgInfo = extractImageInfoFromBase64(base64) + const imageBuffer = base64ToBuffer(imgInfo.imageBase64) + return { + success: true, + buffer: imageBuffer, + fileName: "", // will use getImageSize result + extname: "", // will use getImageSize result } - return url } export const getFSFile = async (filePath: string): Promise => { @@ -48,8 +98,67 @@ export const getFSFile = async (filePath: string): Promise { + return isBlob(val) || isFile(val) +} + +export const isBuffer = (val: any): boolean => { + return toString.call(val) === "[object Buffer]" +} + +/** + * 将 file 对象转换为 Buffer + * + * @param file - file + * @author terwer + * @version 0.9.0 + * @since 0.9.0 + */ +export const fileToBuffer = async (file: any): Promise => { + if (hasNodeEnv) { + return new Promise((resolve, reject) => { + const reader = new win.FileReader() + reader.onload = (e: any) => { + // 将 ArrayBuffer 转换成 Buffer 对象 + const buffer = win.Buffer.from(e.target.result) + resolve(buffer) + } + reader.onerror = reject + reader.readAsArrayBuffer(file) + }) + } else { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e: any) => { + // 将 ArrayBuffer 转换成 Buffer 对象 + const buffer = Buffer.from(e.target.result) + resolve(buffer) + } + reader.onerror = reject + reader.readAsArrayBuffer(file) + }) + } +} + +export const getBlobFile = async (blob: any): Promise => { + const buffer = await fileToBuffer(blob) + return { + success: true, + buffer, + fileName: "", // will use getImageSize result + extname: "", // will use getImageSize result + } +} + export const getURLFile = async (url: string, ctx: IPicGo): Promise => { - url = handleUrlEncode(url) let isImage = false let extname = "" let timeoutId: any @@ -57,31 +166,60 @@ export const getURLFile = async (url: string, ctx: IPicGo): Promise((resolve, reject) => { ;(async () => { try { - const res = await ctx - .request({ - method: "get", - url, - resolveWithFullResponse: true, - responseType: "arraybuffer", - }) - .then((resp: any) => { - const contentType = resp.headers["content-type"] - if (contentType?.includes("image")) { - isImage = true - extname = `.${contentType.split("image/")[1]}` - } - return resp.data as Buffer - }) + let res: any + if (hasNodeEnv) { + res = await ctx + .request({ + method: "get", + url, + resolveWithFullResponse: true, + responseType: "arraybuffer", + }) + .then((resp: any) => { + if (resp.status !== 200) { + resolve({ + success: false, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + reason: `File not found from url ${url} , please try again later`, + }) + } + const contentType = resp.headers["content-type"] + if (contentType?.includes("image")) { + isImage = true + extname = `.${contentType.split("image/")[1]}` + } + return resp.data as any + }) + } else { + // 浏览器环境单独出处理,直接跳出 promise + const response = await window.fetch(url) + if (response.status !== 200) { + resolve({ + success: false, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + reason: `File not found from url ${url} , please try again later`, + }) + } + const blob = await response.blob() + const imgInfo = await getBlobFile(blob) + clearTimeout(timeoutId) + resolve(imgInfo) + } clearTimeout(timeoutId) if (isImage) { const urlPath = new URL(url).pathname - const fileName = urlPath.split("/").pop() - // if (hasNodeEnv) { - // const path = win.require("path") - // fileName = path.basename(urlPath) - // } + let fileName: string + let imgBuffer: Buffer | typeof win.Buffer + if (hasNodeEnv) { + const path = win.require("path") + fileName = path.basename(urlPath) + imgBuffer = win.Buffer.from(res) + } else { + fileName = urlPath.split("/").pop() ?? "" + imgBuffer = Buffer.from(res) + } resolve({ - buffer: res, + buffer: imgBuffer, fileName: fileName, extname, success: true, @@ -283,7 +421,7 @@ export const forceNumber = (num: string | number = 0): number => { // return process.env.NODE_ENV === 'production' // } -export const getImageSize = (file: typeof win.Buffer): IImgSize => { +export const getImageSize = (file: Buffer | typeof win.Buffer): IImgSize => { try { const { width = 0, height = 0, type } = imageSize(file) const extname = type ? `.${type}` : ".png" diff --git a/libs/Universal-PicGo-Core/src/utils/createContext.ts b/libs/Universal-PicGo-Core/src/utils/createContext.ts index 8b20c69..de5c327 100644 --- a/libs/Universal-PicGo-Core/src/utils/createContext.ts +++ b/libs/Universal-PicGo-Core/src/utils/createContext.ts @@ -18,6 +18,7 @@ const createContext = (ctx: IPicGo): IPicGo => { return { configPath: ctx.configPath, baseDir: ctx.baseDir, + pluginBaseDir: ctx.pluginBaseDir, log: ctx.log, // cmd: ctx.cmd, output: [], diff --git a/libs/Universal-PicGo-Core/src/utils/enums.ts b/libs/Universal-PicGo-Core/src/utils/enums.ts index 5e30eb9..9bc334c 100644 --- a/libs/Universal-PicGo-Core/src/utils/enums.ts +++ b/libs/Universal-PicGo-Core/src/utils/enums.ts @@ -29,3 +29,16 @@ export enum IBuildInEvent { export enum IBusEvent { CONFIG_CHANGE = "CONFIG_CHANGE", } + +/** + * PicGo 类型枚举 + * + * @version 1.6.0 + * @since 1.6.0 + * @author terwer + */ +export enum PicgoTypeEnum { + Bundled = "bundled", + App = "app", + // Core = "core", +} diff --git a/libs/Universal-PicGo-Core/src/utils/hashUtil.ts b/libs/Universal-PicGo-Core/src/utils/hashUtil.ts new file mode 100644 index 0000000..dc98b02 --- /dev/null +++ b/libs/Universal-PicGo-Core/src/utils/hashUtil.ts @@ -0,0 +1,26 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +const calculateHash = (input: string) => { + let hash = 0 + + if (input.length === 0) { + return hash + } + + for (let i = 0; i < input.length; i++) { + const char = input.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32bit integer + } + + return Math.abs(hash).toString(16) +} + +export { calculateHash } diff --git a/libs/Universal-PicGo-Core/src/utils/image-size/index.ts b/libs/Universal-PicGo-Core/src/utils/image-size/index.ts index d37ffe1..c07bbbe 100644 --- a/libs/Universal-PicGo-Core/src/utils/image-size/index.ts +++ b/libs/Universal-PicGo-Core/src/utils/image-size/index.ts @@ -120,8 +120,6 @@ export function imageSize(input: string, callback: CallbackFn): void * @param {Function=} [callback] - optional function for async detection */ export function imageSize(input: typeof win.Uint8Array | string, callback?: CallbackFn): ISizeCalculationResult | void { - const path = win.require("path") - // Handle Uint8Array input if (input instanceof win.Uint8Array) { return lookup(input) @@ -133,6 +131,7 @@ export function imageSize(input: typeof win.Uint8Array | string, callback?: Call } // resolve the file path + const path = win.require("path") const filepath = path.resolve(input) if (typeof callback === "function") { queue.push(() => diff --git a/libs/Universal-PicGo-Core/vite.config.ts b/libs/Universal-PicGo-Core/vite.config.ts index 28c77d7..86060dd 100644 --- a/libs/Universal-PicGo-Core/vite.config.ts +++ b/libs/Universal-PicGo-Core/vite.config.ts @@ -13,10 +13,6 @@ import fs from "fs" const packageJson = fs.readFileSync("./package.json").toString() const pkg = JSON.parse(packageJson) || {} -const getAppBase = (): string => { - return "/plugins/siyuan-plugin-picgo/" -} - const getDefineEnv = (isDevMode: boolean) => { const mode = process.env.NODE_ENV const isTest = mode === "test" @@ -25,8 +21,7 @@ const getDefineEnv = (isDevMode: boolean) => { const defaultEnv = { DEV_MODE: `${isDevMode || isTest}`, - APP_BASE: `${appBase}`, - NODE_ENV: "development", + NODE_ENV: isDevMode ? "development" : "production", PICGO_VERSION: pkg.version, } const env = loadEnv(mode, process.cwd()) @@ -54,7 +49,6 @@ const isWatch = args.watch || args.w || false const isDev = isServe || isWatch const devDistDir = "./dist" const distDir = isWatch ? devDistDir : "./dist" -const appBase = getAppBase() console.log("isWatch=>", isWatch) console.log("distDir=>", distDir) diff --git a/libs/Universal-PicGo-Store/src/index.ts b/libs/Universal-PicGo-Store/src/index.ts index f8c14ce..1bc4aff 100644 --- a/libs/Universal-PicGo-Store/src/index.ts +++ b/libs/Universal-PicGo-Store/src/index.ts @@ -1,7 +1,7 @@ import { JSONStore } from "./lib/JSONStore" -import { win, hasNodeEnv } from "./lib/utils" +import { win, currentWin, parentWin, hasNodeEnv } from "./lib/utils" import { IJSON } from "./types" export { type IJSON } export { JSONStore } -export { win, hasNodeEnv } +export { win, currentWin, parentWin, hasNodeEnv } diff --git a/libs/Universal-PicGo-Store/src/lib/utils.ts b/libs/Universal-PicGo-Store/src/lib/utils.ts index 736e20a..e2adb15 100644 --- a/libs/Universal-PicGo-Store/src/lib/utils.ts +++ b/libs/Universal-PicGo-Store/src/lib/utils.ts @@ -7,7 +7,7 @@ * of this license document, but changing it is not allowed. */ -const currentWin = (window || globalThis || undefined) as any -const parentWin = (window?.parent || window?.top || window?.self || undefined) as any -export const win = currentWin?.fs ? currentWin : parentWin?.fs ? parentWin : currentWin -export const hasNodeEnv = typeof win?.fs !== "undefined" +export const currentWin = (window || globalThis || undefined) as any +export const parentWin = (window?.parent || window?.top || window?.self || undefined) as any +export const win = currentWin?.fs?.rm ? currentWin : parentWin?.fs?.rm ? parentWin : currentWin +export const hasNodeEnv = typeof parentWin?.fs?.rm !== "undefined" diff --git a/libs/zhi-siyuan-picgo/.eslintrc.cjs b/libs/zhi-siyuan-picgo/.eslintrc.cjs new file mode 100644 index 0000000..e69de29 diff --git a/libs/zhi-siyuan-picgo/.gitignore b/libs/zhi-siyuan-picgo/.gitignore new file mode 100644 index 0000000..dda5297 --- /dev/null +++ b/libs/zhi-siyuan-picgo/.gitignore @@ -0,0 +1,3 @@ +.idea +.DS_Store +dist \ No newline at end of file diff --git a/libs/zhi-siyuan-picgo/.prettierignore b/libs/zhi-siyuan-picgo/.prettierignore new file mode 100644 index 0000000..e69de29 diff --git a/libs/zhi-siyuan-picgo/.prettierrc.cjs b/libs/zhi-siyuan-picgo/.prettierrc.cjs new file mode 100644 index 0000000..eec8622 --- /dev/null +++ b/libs/zhi-siyuan-picgo/.prettierrc.cjs @@ -0,0 +1,30 @@ +/* + * 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. + */ + +module.exports = { + semi: false, + singleQuote: false, + printWidth: 120 +} diff --git a/libs/zhi-siyuan-picgo/README.md b/libs/zhi-siyuan-picgo/README.md new file mode 100644 index 0000000..e69de29 diff --git a/libs/zhi-siyuan-picgo/index.html b/libs/zhi-siyuan-picgo/index.html new file mode 100644 index 0000000..f0d45f8 --- /dev/null +++ b/libs/zhi-siyuan-picgo/index.html @@ -0,0 +1,12 @@ + + + + + + Vite + TS + + + This file is for lib hot load test only, see /src/index.ts + + + diff --git a/libs/zhi-siyuan-picgo/package.json b/libs/zhi-siyuan-picgo/package.json new file mode 100644 index 0000000..35ef607 --- /dev/null +++ b/libs/zhi-siyuan-picgo/package.json @@ -0,0 +1,44 @@ +{ + "name": "zhi-siyuan-picgo", + "version": "0.0.1", + "type": "module", + "description": "picgo lib for siyuan-note", + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "repository": "terwer/zhi", + "homepage": "https://github.com/terwer/zhi/tree/main/libs/zhi-siyuan-picgo", + "author": "terwer", + "license": "MIT", + "files": [ + "dist", + "README.md" + ], + "keywords": [ + "zhi", + "lib" + ], + "scripts": { + "serve": "vite", + "dev": "vite build --watch", + "build": "vite build", + "start": "vite preview", + "test": "npx vitest --watch" + }, + "devDependencies": { + "@terwer/eslint-config-custom": "^1.3.6", + "@terwer/vite-config-custom": "^0.7.6", + "@types/uuid": "^9.0.8" + }, + "dependencies": { + "js-md5": "^0.8.3", + "universal-picgo": "workspace:*", + "uuid": "^9.0.1", + "zhi-common": "^1.33.0", + "zhi-device": "^2.11.0", + "zhi-lib-base": "^0.8.0", + "zhi-siyuan-api": "^2.19.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/libs/zhi-siyuan-picgo/src/index.spec.ts b/libs/zhi-siyuan-picgo/src/index.spec.ts new file mode 100644 index 0000000..141ad47 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/index.spec.ts @@ -0,0 +1,8 @@ +import { describe, it } from "vitest" +import { getFileHash } from "./lib/utils/md5Util" + +describe("index", () => { + it("test index", () => { + console.log(getFileHash("hello")) + }) +}) diff --git a/libs/zhi-siyuan-picgo/src/index.ts b/libs/zhi-siyuan-picgo/src/index.ts new file mode 100644 index 0000000..a7eb0ab --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/index.ts @@ -0,0 +1,32 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { SiyuanPicgoPostApi } from "./lib/siyuanPicgoPostApi" +import { + ConfigDb, + ExternalPicgoConfigDb, + IExternalPicgoConfig, + IImgInfo, + IPicGo, + IPicgoDb, + IConfig, + PicgoTypeEnum, + PluginLoaderDb, + IPluginConfig, +} from "universal-picgo" +import { SiyuanConfig as SiyuanPicgoConfig } from "zhi-siyuan-api" +import { PicgoHelper } from "./lib/picgoHelper" +import { retrieveImageFromClipboardAsBlob } from "./lib/utils/browserClipboard" +import { copyToClipboardInBrowser } from "./lib/utils/utils" + +export { SiyuanPicgoConfig, SiyuanPicgoPostApi, PicgoHelper } +export { retrieveImageFromClipboardAsBlob, copyToClipboardInBrowser } +export { ConfigDb, PluginLoaderDb, ExternalPicgoConfigDb } +export { PicgoTypeEnum } +export { type IPicGo, type IImgInfo, type IPicgoDb, type IConfig, type IExternalPicgoConfig, type IPluginConfig } diff --git a/packages/picgo-plugin-app/custom.d.ts b/libs/zhi-siyuan-picgo/src/lib/constants.ts similarity index 56% rename from packages/picgo-plugin-app/custom.d.ts rename to libs/zhi-siyuan-picgo/src/lib/constants.ts index aba7512..86ad9b9 100644 --- a/packages/picgo-plugin-app/custom.d.ts +++ b/libs/zhi-siyuan-picgo/src/lib/constants.ts @@ -2,7 +2,12 @@ * GNU GENERAL PUBLIC LICENSE * Version 3, 29 June 2007 * - * Copyright (C) 2023-2024 Terwer, Inc. + * Copyright (C) 2024 Terwer, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ + +/** + * 文章PicGO图片信息Key + */ +export const SIYUAN_PICGO_FILE_MAP_KEY = "custom-picgo-file-map-key" diff --git a/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts b/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts new file mode 100644 index 0000000..3913f10 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/ImageItem.ts @@ -0,0 +1,94 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2023-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { getFileHash } from "../utils/md5Util" + +/** + * 提取文件名 + * + * @param url + * @version 1.6.0 + * @since 1.6.0 + */ +const extractFileName = (url: string) => { + url = decodeURIComponent(url) + const parts = url.split("/") + let fileName = null + + for (let i = parts.length - 1; i >= 0; i--) { + if (parts[i].includes(".") && parts[i] !== "") { + fileName = parts[i].split("?")[0] + break + } + } + + if (!fileName) { + fileName = url + } + + return fileName +} + +// // 测试用例 +// const testUrls = [ +// "http://example.com/folder/filename.jpg?size=large", +// "http://example.com/folder/filename.jpg/aaaaaa", +// "http://example.com/folder/filename.jpg/aaaaaa/bbb?ccc", +// "http://localhost:8002/api/v4/projects/terwer%2Fgitlab-upload/repository/files/img%2F20240326111449.jpg/raw", +// ] +// +// testUrls.forEach((url) => { +// const fileName = extractFileName(url) +// console.log(`URL: ${url} => File Name: ${fileName}`) +// }) + +/** + * 图片信息 + */ +export class ImageItem { + /** + * 文件, + */ + name: string + /** + * 文件名称的Hash,构造函数指定 + */ + hash: string + /** + * 原始资源地址 + */ + originUrl: string + /** + * 资源地址 + */ + url: string + /** + * 资源备注 + */ + alt?: string + /** + * 标题 + */ + title?: string + /** + * 是否本地 + */ + isLocal: boolean + + constructor(originUrl: string, url: string, isLocal: boolean, alt?: string, title?: string) { + this.originUrl = originUrl + // this.name = originUrl.substring(originUrl.lastIndexOf("/") + 1) + this.name = extractFileName(originUrl) + this.hash = getFileHash(this.name) + this.url = url + this.isLocal = isLocal + this.alt = alt ?? "" + this.title = title ?? "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts b/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts new file mode 100644 index 0000000..bda9996 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/ParsedImage.ts @@ -0,0 +1,40 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/** + * 解析的图片 + * + * @author terwer + * @since 0.8.0 + */ +export class ParsedImage { + /** + * 链接 + */ + url: string + /** + * 备注 + */ + alt: string + /** + * 标题 + */ + title: string + /** + * 是否本地 + */ + isLocal: boolean + + constructor() { + this.url = "" + this.isLocal = false + this.alt = "" + this.title = "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts b/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts new file mode 100644 index 0000000..b7f8d9d --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/models/PicgoPostResult.ts @@ -0,0 +1,37 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/** + * Picgo处理文章统一返回结果 + */ +export class PicgoPostResult { + /** + * 是否成功 + */ + flag: boolean + /** + * 是否有图片 + */ + hasImages: boolean + /** + * 处理后的文章链接 + */ + mdContent: string + /** + * 错误信息 + */ + errmsg: string + + constructor() { + this.flag = false + this.hasImages = false + this.mdContent = "" + this.errmsg = "" + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts b/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts new file mode 100644 index 0000000..debf582 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/parser/ImageParser.ts @@ -0,0 +1,242 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ILogger, simpleLogger } from "zhi-lib-base" +import { ParsedImage } from "../models/ParsedImage" +import { ImageItem } from "../models/ImageItem" + +/** + * 图片解析器 + * + * 自动解析文章中的img标签, + * 自动处理src外链、base64数据 + * + * @author terwer + * @since 0.1.0 + */ +export class ImageParser { + private readonly logger: ILogger + + constructor(isDev?: boolean) { + this.logger = simpleLogger("image-parser", "zhi-siyuan-picgo", isDev) + } + + /** + * 检测是否有外链图片 + * + * @param content 文章正文 + */ + public hasExternalImages(content: string): boolean { + const flag = false + + const imgRegex = /!\[.*]\((http|https):\/.*\/.*\)/g + const matches = content.match(imgRegex) + if (matches != null && matches.length > 0) { + return true + } + + const imgBase64Regex = /!\[.*]\((data:image):\/.*\/.*\)/g + const base64Matches = content.match(imgBase64Regex) + if (base64Matches != null && base64Matches.length > 0) { + return true + } + + return flag + } + + /** + * 剔除外链图片 + * + * @param content 文章正文 + */ + public removeImages(content: string): string { + let newcontent = content + + newcontent = newcontent.replace(/!\[.*]\((http|https):\/.*\/.*\)/g, "") + + return newcontent + } + + /** + * 解析图片块为图片链接 + * + * @param content 图片块 + * @private + */ + public parseImagesToArray(content: string): ParsedImage[] { + let ret = [] as ParsedImage[] + const remoteImages = this.parseRemoteImagesToArray(content) + const localImages = this.parseLocalImagesToArray(content) + + // 会有很多重复值 + // ret = ret.concat(remoteImages, localImages) + // 下面的写法可以去重 + ret = [...new Set([...remoteImages, ...localImages])] + + return ret + } + + /** + * 解析图片块为远程图片链接 + * + * @param markdownText 图片块 + * @private + */ + public parseRemoteImagesToArray(markdownText: string): ParsedImage[] { + this.logger.debug("准备解析文本中的远程图片=>", markdownText) + // 定义正则表达式来匹配以 http 或 https 开头的 Markdown 格式的图片链接 + // 只能匹配有属性和备注的情况 + // const regex = /!\[(.*?)\]\(((https|http|ftp)?:\/\/[^\s/$.?#].[^\s]*)\s*"(.*?)"\)\s*{:\s*([^\n]*)}/g + // 同时兼容有属性和没有属性的情况 + const regex = /!\[(.*?)\]\((https?:\/\/\S+\.(?:jpe?g|png|gif))(?:\s+"(?:[^"\\]|\\.)*")?\s*(?:{:\s*([^\n]*)})?\)/g + + // 匹配普通图片链接: + // ![Cat](https://example.com/cat.png) + // 匹配结果: + // match[1]: "Cat" + // match[2]: "https://example.com/cat.png" + // match[3]: undefined + + // 匹配带注释的图片链接: + // ![Dog](https://example.com/dog.jpg "A dog in the park") + // 匹配结果: + // match[1]: "Dog" + // match[2]: "https://example.com/dog.jpg" + // match[3]: "A dog in the park" + + // 匹配带属性的图片链接: + // ![Fish](https://example.com/fish.gif){width=200 height=150} + // 匹配结果: + // match[1]: "Fish" + // match[2]: "https://example.com/fish.gif" + // match[3]: "width=200 height=150" + + // 使用正则表达式来匹配 Markdown 格式的图片链接,并提取其中的各个属性 + const ParsedImages = [] + for (const match of markdownText.matchAll(regex)) { + const altText = match[1] ? match[1] : "" + const url = match[2] ? match[2] : "" + const title = match[3] ? match[3].replace(/"/g, "") : "" + + // 将图片链接的各个属性封装成一个对象,并添加到数组中 + ParsedImages.push({ + url, + alt: altText, + title, + isLocal: false, + }) + } + this.logger.debug("远程图片解析完毕.", ParsedImages) + return ParsedImages + } + + /** + * 解析图片块为本地图片链接 + * + * @param markdownText 图片块 + */ + public parseLocalImagesToArray(markdownText: string): ParsedImage[] { + this.logger.debug("准备解析文本中的本地图片=>", markdownText) + + // 定义正则表达式来匹配以 assets 开头但不以 http、https 或 ftp 开头的 Markdown 格式的图片链接 + // const regex = /!\[(.*?)\]\(((?!http|https|ftp)assets\/.*?)\s*("(?:.*[^"])")?\)\s*(\{(?:.*[^"])})?/g + const regex = /!\[(.*?)\]\(((?!http|https|ftp)assets\/.*?)\s*("(?:[^"\\]|\\.)*")?\s*(\{(?:[^"\\]|\\.)*\})?\)/g + // 这样的正则表达式可以同时匹配到以下格式的图片链接: + // ![图片](assets/image.png) + // ![带注释的图片](assets/image.png "注释") + // ![带属性的图片](assets/image.png){width=100 height=200} + + // 使用正则表达式来匹配 Markdown 格式的图片链接,并提取其中的各个属性 + const ParsedImages = [] + for (const match of markdownText.matchAll(regex)) { + const altText = match[1] ? match[1] : "" + const url = match[2] ? match[2] : "" + const title = match[3] ? match[3].replace(/"/g, "") : "" + + // 将图片链接的各个属性封装成一个对象,并添加到数组中 + ParsedImages.push({ + url, + alt: altText, + title, + isLocal: true, + }) + } + this.logger.debug("本地图片解析完毕.", ParsedImages) + return ParsedImages + } + + /** + * 将外链外链图片替换为图床链接 + * + * @param content 正文 + * @param replaceMap 替换信息 + */ + public replaceImagesWithImageItemArray(content: string, replaceMap: any): string { + let newcontent = content + + const imgRegex = /!\[.*]\(assets\/.*\..*\)/g + const matches = newcontent.match(imgRegex) + // 没有图片,无需处理 + if (matches == null || matches.length === 0) { + this.logger.warn("未匹配到本地图片,将不会替换图片链接") + return newcontent + } + + for (let i = 0; i < matches.length; i++) { + const img = matches[i] + this.logger.debug("img=>", img) + + const src = img.replace(/!\[.*]\(/g, "").replace(/\)/, "") + this.logger.debug("src=>", src) + let url + let title + const urlAttrs = src.split(" ") + if (urlAttrs.length > 1) { + url = urlAttrs[0] + title = urlAttrs[1].replace(/"/g, "") + } else { + url = urlAttrs[0] + } + + const tempImageItem = new ImageItem(url, "", true) + const hash = tempImageItem.hash + const replaceImageItem: ImageItem = replaceMap[hash] + const alt = replaceImageItem?.alt ?? "" + + let newImg = `![${alt}](${replaceImageItem?.url})` + if (title) { + newImg = `![${alt}](${replaceImageItem?.url} "${title}")` + } + this.logger.debug("newImg=>", newImg) + + // 使用正则表达式和replace方法来实现replaceAll方法 + // 将search转换为正则表达式,使用g标志表示全局匹配 + const imgRegex = new RegExp(img, "g") + newcontent = newcontent.replace(imgRegex, newImg) + } + + return newcontent + } + + /** + * 下载图片到本地并打包成zip + * + * @@deprecated 不再支持 + */ + // public async downloadMdWithImages(): Promise {} + + /** + * 下载图片到本地并保存到思源 + * + * @deprecated 思源笔记已经有此功能 + */ + // public async downloadImagesToSiyuan(): Promise { + // throw new Error("思源笔记已经有此功能,无需重新实现") + // } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/picgoHelper.ts b/libs/zhi-siyuan-picgo/src/lib/picgoHelper.ts new file mode 100644 index 0000000..519d362 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/picgoHelper.ts @@ -0,0 +1,426 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import _ from "lodash-es" +import { + eventBus, + IBusEvent, + IConfig, + IPicBedType, + IPicGo, + IUploaderConfigItem, + IUploaderConfigListItem, +} from "universal-picgo" +import { getRawData, trimValues } from "./utils/utils" +import { readonly } from "vue" +import IdUtil from "./utils/idUtil" + +/** + * picgo 工具类 + * + * @version 1.6.0 + * @since 1.6.0 + * @author terwer + */ +class PicgoHelper { + private readonly ctx: IPicGo + /** + * !!! 这个 cfg 是响应式的,修改这个会自动完成持久化 + * + * !!! 这个 cfg 是响应式的,修改这个会自动完成持久化 + * + * !!! 这个 cfg 是响应式的,修改这个会自动完成持久化 + * + * @private + */ + private readonly reactiveCfg: IConfig + private readonly readonlyCfg: IConfig + + /** + * 狗子 PicGo 帮组类 + * + * @param ctx 上下文 + * @param reactiveCfg 响应式配置对象 + */ + constructor(ctx: IPicGo, reactiveCfg: IConfig) { + if (!ctx) { + throw new Error("PicGo ctx cannot be null") + } + if (!reactiveCfg) { + throw new Error("PicGo reactive config cannot be null") + } + this.ctx = ctx + this.reactiveCfg = reactiveCfg + this.readonlyCfg = readonly(this.reactiveCfg) + } + + /** + * 根据 key 获取配置项 + * + * @param key + * @param defaultValue + */ + public getPicgoConfig(key?: string, defaultValue?: any) { + if (!key) { + return this.readonlyCfg as unknown + } + return _.get(this.readonlyCfg, key, defaultValue) + } + + /** + * 保存配置 + * + * @param cfg + */ + public savePicgoConfig(cfg: Partial) { + if (!cfg) { + console.warn(`cfg can not be undefined `) + return + } + // 刷新 + Object.keys(cfg).forEach((name: string) => { + const rawCfg = getRawData(cfg) + _.set(this.reactiveCfg, name, rawCfg[name]) + eventBus.emit(IBusEvent.CONFIG_CHANGE, { + configName: name, + value: rawCfg[name], + }) + }) + } + + /** + * 获取所有的图床列表 + */ + public getPicBeds(): IPicBedType[] { + const picBedTypes = this.ctx.helper.uploader.getIdList() + const picBedFromDB = this.getPicgoConfig("picBed.list") || [] + + const picBeds = picBedTypes + .map((item: any) => { + const visible = picBedFromDB.find((i: any) => i.type === item) // object or undefined + return { + type: item, + name: this.ctx.helper.uploader.get(item).name || item, + visible: visible ? visible.visible : true, + } + }) + .sort((a: any) => { + if (a.type === "github") { + return -1 + } + return 0 + }) + + return picBeds + } + + /** + * 获取启用的图床 + */ + public getVisiablePicBeds(): IPicBedType[] { + const picBeds = this.getPicBeds() + const visiablePicBeds = picBeds + .map((item: IPicBedType) => { + if (item.visible) { + return item + } + return null + }) + .filter((item: any) => item) as IPicBedType[] + + // SM.MS是必选的 + if (visiablePicBeds.length == 0) { + const defaultPicbed = { + type: "smms", + name: "SM.MS", + } as IPicBedType + visiablePicBeds.push(defaultPicbed) + } + return visiablePicBeds + } + + /** + * 获取可用的图床列表名称 + */ + public getVisiablePicBedNames(): string[] { + const picBeds = this.getPicBeds() + return picBeds + .map((item: IPicBedType) => { + if (item.visible) { + return item.name + } + return null + }) + .filter((item: any) => item) as string[] + } + + /** + * 根据图床数据获取可用的图床列表名称 + * + * @param picBeds + */ + public static getVisiablePicBedNamesByPicBeds(picBeds: IPicBedType[]): string[] { + return picBeds + .map((item: IPicBedType) => { + if (item.visible) { + return item.name + } + return null + }) + .filter((item: any) => item) as string[] + } + + /** + * 获取当前图床 + */ + public getCurrentUploader() { + return this.getPicgoConfig("picBed.uploader") || this.getPicgoConfig("picBed.current") || "smms" + } + + public getUploaderConfigList(type: string): IUploaderConfigItem { + if (!type) { + return { + configList: [] as IUploaderConfigListItem[], + defaultId: "", + } + } + const currentUploaderConfig = this.getPicgoConfig(`uploader.${type}`, {}) + let configList = currentUploaderConfig.configList + let defaultId = currentUploaderConfig.defaultId || "" + if (!configList) { + const res = this.upgradeUploaderConfig(type) + configList = res.configList + defaultId = res.defaultId + } + + const configItem = { + configList, + defaultId, + } + // console.warn("获取当前图床配置列表:", configItem) + return configItem + } + + /** + * 选择当前图床 + * + * @param type 当前图床类型 + * @param id 当前图床配置ID + * @author terwer + * @since 0.7.0 + */ + public selectUploaderConfig = (type: string, id: string) => { + const { configList } = this.getUploaderConfigList(type) + const config = configList.find((item: any) => item._id === id) + if (config) { + this.savePicgoConfig({ + [`uploader.${type}.defaultId`]: id, + [`picBed.${type}`]: config, + }) + } + + return config + } + + /** + * 设置默认图床 + * + * @param type + */ + public setDefaultPicBed(type: string) { + this.savePicgoConfig({ + "picBed.current": type, + "picBed.uploader": type, + }) + } + + /** + * get picbed config by type,获取的是表单属性详细信息 + * + * it will trigger the uploader config function & get the uploader config result + * & not just read from + * + * @author terwer + * @since 0.7.0 + */ + public getPicBedConfig(type: string) { + const name = this.ctx.helper.uploader.get(type)?.name || type + if (this.ctx.helper.uploader.get(type)?.config) { + const _config = this.ctx.helper.uploader.get(type).config(this.ctx) + const config = this.handleConfigWithFunction(_config) + return { + config, + name, + } + } else { + return { + config: [], + name, + } + } + } + + /** + * 更新图床配置 + * + * @param type 图床类型 + * @param id 图床配置ID + * @param config 图床配置 + * + * @author terwer + * @since 0.7.0 + */ + public updateUploaderConfig(type: string, id: string, config: IUploaderConfigListItem) { + // ensure raw for save + config = getRawData(config) + const uploaderConfig = this.getUploaderConfigList(type) + let configList = uploaderConfig.configList + // ensure raw for save + configList = getRawData(configList) + const defaultId = uploaderConfig.ddefaultId + const existConfig = configList.find((item: IUploaderConfigListItem) => item._id === id) + let updatedConfig + let updatedDefaultId = defaultId + if (existConfig) { + updatedConfig = Object.assign(existConfig, trimValues(config), { + _updatedAt: Date.now(), + }) + } else { + updatedConfig = this.completeUploaderMetaConfig(config) + updatedDefaultId = updatedConfig._id + configList.push(updatedConfig) + } + this.savePicgoConfig({ + [`uploader.${type}.configList`]: configList, + [`uploader.${type}.defaultId`]: updatedDefaultId, + [`picBed.${type}`]: updatedConfig, + }) + } + + /** + * delete uploader config by type & id + */ + public deleteUploaderConfig(type: string, id: string) { + const uploaderConfig = this.getUploaderConfigList(type) + let configList = uploaderConfig.configList + // ensure raw for save + configList = getRawData(configList) + const defaultId = uploaderConfig.ddefaultId + if (configList.length <= 1) { + return + } + let newDefaultId = defaultId + const updatedConfigList = configList.filter((item: any) => item._id !== id) + if (id === defaultId) { + newDefaultId = updatedConfigList[0]._id + this.changeCurrentUploader(type, updatedConfigList[0], updatedConfigList[0]._id) + } + this.savePicgoConfig({ + [`uploader.${type}.configList`]: updatedConfigList, + }) + return { + configList: updatedConfigList, + defaultId: newDefaultId, + } + } + + // =================================================================================================================== + + /** + * 切换当前上传图床 + * + * @param type 图床类型 + * @param config 图床配置 + * @param id 配置id + */ + private changeCurrentUploader(type: string, config: any, id: string) { + if (!type) { + return + } + if (id) { + this.savePicgoConfig({ + [`uploader.${type}.defaultId`]: id, + }) + } + if (config) { + this.savePicgoConfig({ + [`picBed.${type}`]: config, + }) + } + this.savePicgoConfig({ + "picBed.current": type, + "picBed.uploader": type, + }) + } + + /** + * upgrade old uploader config to new format + * + * @param type type + * @author terwer + * @since 0.7.0 + */ + private upgradeUploaderConfig = (type: string) => { + const uploaderConfig = this.getPicgoConfig(`picBed.${type}`, {}) + if (!uploaderConfig._id) { + Object.assign(uploaderConfig, this.completeUploaderMetaConfig(uploaderConfig)) + } + + const uploaderConfigList = [uploaderConfig] + this.savePicgoConfig({ + [`uploader.${type}`]: { + configList: uploaderConfigList, + defaultId: uploaderConfig._id, + }, + [`picBed.${type}`]: uploaderConfig, + }) + return { + configList: uploaderConfigList as IUploaderConfigListItem[], + defaultId: uploaderConfig._id as string, + } + } + + /** + * 增加配置元数据 + * + * @param originData 原始数据 + */ + private completeUploaderMetaConfig(originData: any) { + return Object.assign( + { + _configName: "Default", + }, + trimValues(originData), + { + _id: IdUtil.newUuid(), + _createdAt: Date.now(), + _updatedAt: Date.now(), + } + ) + } + + /** + * 配置处理 + * + * @param config 配置 + */ + private handleConfigWithFunction(config: any) { + for (const i in config) { + if (typeof config[i].default === "function") { + config[i].default = config[i].default() + } + if (typeof config[i].choices === "function") { + config[i].choices = config[i].choices() + } + } + return config + } +} + +export { PicgoHelper } diff --git a/libs/zhi-siyuan-picgo/src/lib/siyuanPicGoUploadApi.ts b/libs/zhi-siyuan-picgo/src/lib/siyuanPicGoUploadApi.ts new file mode 100644 index 0000000..ae29649 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/siyuanPicGoUploadApi.ts @@ -0,0 +1,51 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ExternalPicgo, IImgInfo, IPicGo, PicgoTypeEnum, UniversalPicGo } from "universal-picgo" +import { ILogger } from "zhi-lib-base" + +/** + * 思源笔记专属的图片上传 API + * + * @version 1.6.0 + * @since 0.6.0 + * @author terwer + */ +class SiyuanPicGoUploadApi { + public readonly picgo: IPicGo + private readonly externalPicGo: ExternalPicgo + private readonly logger: ILogger + + constructor(isDev?: boolean) { + // 初始化 PicGO + this.picgo = new UniversalPicGo("", "", isDev) + this.externalPicGo = new ExternalPicgo(this.picgo, isDev) + this.logger = this.picgo.getLogger("siyuan-picgo-upload-api") + this.logger.debug("picgo upload api inited") + } + + /** + * 上传图片到PicGO + * + * @param input 路径数组,可为空,为空上传剪贴板 + */ + public async upload(input?: any[]): Promise { + const useBundledPicgo = this.externalPicGo.db.get("useBundledPicgo") + if (useBundledPicgo) { + const picgoType = this.externalPicGo.db.get("picgoType") + if (picgoType !== PicgoTypeEnum.Bundled) { + throw new Error("当前配置使用内置PicGo,请先在配置页面选择使用内置PicGo") + } + return this.picgo.upload(input) + } + return this.externalPicGo.upload(input) + } +} + +export { SiyuanPicGoUploadApi } diff --git a/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts b/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts new file mode 100644 index 0000000..6657169 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/siyuanPicgoPostApi.ts @@ -0,0 +1,394 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { ILogger, simpleLogger } from "zhi-lib-base" +import { SiyuanPicGoUploadApi } from "./siyuanPicGoUploadApi" +import { hasNodeEnv, IImgInfo, IPicGo, win } from "universal-picgo" +import { ParsedImage } from "./models/ParsedImage" +import { ImageItem } from "./models/ImageItem" +import { SIYUAN_PICGO_FILE_MAP_KEY } from "./constants" +import { JsonUtil, StrUtil } from "zhi-common" +import { SiyuanConfig, SiyuanKernelApi } from "zhi-siyuan-api" +import { ImageParser } from "./parser/ImageParser" +import { PicgoPostResult } from "./models/PicgoPostResult" +import { DeviceDetection, DeviceTypeEnum, SiyuanDevice } from "zhi-device" +import { isFileOrBlob } from "universal-picgo" + +/** + * Picgo与文章交互的通用方法 + */ +class SiyuanPicgoPostApi { + private readonly logger: ILogger + private readonly imageParser: ImageParser + private readonly siyuanApi: SiyuanKernelApi + private readonly siyuanConfig: SiyuanConfig + private readonly isSiyuanOrSiyuanNewWin: boolean + private readonly picgoApi: SiyuanPicGoUploadApi + public cfgUpdating: boolean + + constructor(siyuanConfig: SiyuanConfig, isDev?: boolean) { + this.logger = simpleLogger("picgo-post-api", "zhi-siyuan-picgo", isDev) + + this.imageParser = new ImageParser(isDev) + + this.siyuanConfig = siyuanConfig + this.siyuanApi = new SiyuanKernelApi(siyuanConfig) + + this.isSiyuanOrSiyuanNewWin = (() => { + const deviceType = DeviceDetection.getDevice() + // 三种情况,主窗口、挂件、新窗口 + const isSiyuanOrSiyuanNewWin = + deviceType === DeviceTypeEnum.DeviceType_Siyuan_MainWin || + deviceType === DeviceTypeEnum.DeviceType_Siyuan_RendererWin || + deviceType === DeviceTypeEnum.DeviceType_Siyuan_Widget + this.logger.debug("deviceType=>", deviceType) + this.logger.debug("isSiyuanOrSiyuanNewWin=>", String(isSiyuanOrSiyuanNewWin)) + return isSiyuanOrSiyuanNewWin + })() + + // 初始化 PicGO + this.picgoApi = new SiyuanPicGoUploadApi(isDev) + this.cfgUpdating = false + + this.updateConfig() + } + + /** + * 内置 PicGo 上下文 + */ + public ctx(): IPicGo { + return this.picgoApi.picgo + } + + /** + * 上传图片到PicGO,此方法不会修改元数据 + * + * @param input 路径数组,可为空,为空上传剪贴板 + */ + public async upload(input?: any[]): Promise { + return this.picgoApi.upload(input) + } + + /** + * 将字符串数组格式的图片信息转换成图片对象数组 + * + * @param attrs 文章属性 + * @param retImgs 字符串数组格式的图片信息 + * @param imageBaseUrl - 本地图片前缀,一般是思源的地址 + */ + public async doConvertImagesToImagesItemArray( + attrs: any, + retImgs: ParsedImage[], + imageBaseUrl?: string + ): Promise { + 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 = {} as any + this.logger.debug("attrs=>", attrs) + if (!StrUtil.isEmptyString(attrs[SIYUAN_PICGO_FILE_MAP_KEY])) { + fileMap = JsonUtil.safeParse(attrs[SIYUAN_PICGO_FILE_MAP_KEY], {}) + this.logger.debug("fileMap=>", fileMap) + } + + // 处理思源本地图片预览 + // 这个是从思源查出来解析的是否是本地 + if (retImg.isLocal) { + const 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 { + 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 = {} as any + 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[SIYUAN_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: any) { + ret.flag = false + ret.errmsg = e.toString() + 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 { + const mapInfoStr = attrs[SIYUAN_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 + // blob 或者 file 直接上传 + if (isFileOrBlob(imageItem.url)) { + imageFullPath = imageItem.url + } else { + 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.upload(filePaths) + this.logger.debug("图片上传完成,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.debug("newFileMap=>", fileMap) + + const newFileMapStr = JSON.stringify(fileMap) + await this.siyuanApi.setBlockAttrs(pageId, { + [SIYUAN_PICGO_FILE_MAP_KEY]: newFileMapStr, + }) + + return imageJsonObj + } + + // =================================================================================================================== + + private updateConfig() { + // 迁移旧插件配置 + let legacyCfgfolder = "" + // 初始化思源 PicGO 配置 + const workspaceDir = win?.siyuan?.config?.system?.workspaceDir ?? "" + if (hasNodeEnv && workspaceDir !== "") { + const path = win.require("path") + legacyCfgfolder = path.join(workspaceDir, "data", "storage", "syp", "picgo") + // 如果新插件采用了不同的目录,需要迁移旧插件 node_modules 文件夹 + if (legacyCfgfolder !== this.picgoApi.picgo.baseDir) { + void this.moveFile(legacyCfgfolder, this.picgoApi.picgo.baseDir) + } + } + + // 旧的配置位置 + // [工作空间]/data/storage/syp/picgo/picgo.cfg.json + // [工作空间]/data/storage/syp/picgo/package.json + // [工作空间]/data/storage/syp/picgo/mac.applescript + // [工作空间]/data/storage/syp/picgo/i18n-cli + // [工作空间]/data/storage/syp/picgo/picgo-clipboard-images + // + // 新配置位置 + // ~/.universal-picgo + } + + private async moveFile(from: string, to: string) { + const fs = win.fs + const existFrom = fs.existsSync(from) + const existTo = fs.existsSync(to) + + if (!existFrom) { + return + } + + // 存在旧文件采取迁移 + this.cfgUpdating = true + this.logger.info(`will move ${from} to ${to}`) + // 目的地存在复制 + if (existTo) { + this.copyFolder(from, to) + .then(() => { + this.cfgUpdating = false + }) + .catch((e: any) => { + this.cfgUpdating = false + this.logger.error(`copy ${from} to ${to} failed: ${e}`) + }) + } else { + // 不存在移动过去 + fs.promises + .rename(from, to) + .then(() => { + this.cfgUpdating = false + }) + .catch((e: any) => { + this.cfgUpdating = false + this.logger.error(`move ${from} to ${to} failed: ${e}`) + }) + } + } + + private async copyFolder(from: string, to: string) { + const fs = win.fs + const path = win.require("path") + + const files = await fs.promises.readdir(from) + for (const file of files) { + if (file.startsWith(".")) { + continue + } + const sourcePath = path.join(from, file) + const destPath = path.join(to, file) + + const stats = await fs.promises.lstat(sourcePath) + if (stats.isDirectory()) { + await fs.promises.mkdir(destPath, { recursive: true }) + // 递归复制子文件夹 + await this.copyFolder(sourcePath, destPath) + } else { + await fs.promises.copyFile(sourcePath, destPath) + } + } + + // 删除源文件夹 + await fs.promises.rmdir(from, { recursive: true }) + } +} + +export { SiyuanPicgoPostApi } diff --git a/libs/zhi-siyuan-picgo/src/lib/utils/browserClipboard.ts b/libs/zhi-siyuan-picgo/src/lib/utils/browserClipboard.ts new file mode 100644 index 0000000..8a304d5 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/utils/browserClipboard.ts @@ -0,0 +1,49 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/** + * This handler retrieves the images from the clipboard as a blob and returns it in a callback. + * + * @param pasteEvent + * @param callback + */ +export const retrieveImageFromClipboardAsBlob = (pasteEvent: any, callback: any) => { + if (pasteEvent.clipboardData == false) { + if (typeof callback == "function") { + callback(undefined) + } + } + + const items = pasteEvent.clipboardData.items + + if (items == undefined) { + if (typeof callback == "function") { + callback(undefined) + } + } + + let imgLength = 0 + for (let i = 0; i < items!.length; i++) { + // Skip content if not image + if (items[i].type.indexOf("image") == -1) continue + // Retrieve image on clipboard as blob + const blob = items[i].getAsFile() + imgLength++ + if (typeof callback == "function") { + callback(blob) + } + } + + // no img + if (imgLength == 0) { + if (typeof callback == "function") { + callback(undefined) + } + } +} diff --git a/libs/zhi-siyuan-picgo/src/lib/utils/idUtil.ts b/libs/zhi-siyuan-picgo/src/lib/utils/idUtil.ts new file mode 100644 index 0000000..c01ff86 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/utils/idUtil.ts @@ -0,0 +1,14 @@ +import { v4 as uuidv4 } from "uuid" + +/** + * ID生成统一入口 + */ +const newUuid = () => { + return uuidv4() +} + +const IdUtil = { + newUuid, +} + +export default IdUtil diff --git a/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts b/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts new file mode 100644 index 0000000..9467b21 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/utils/md5Util.ts @@ -0,0 +1,26 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { md5 } from "js-md5" + +/** + * 获取文件名的hash + * + * @param filename 文件名 + */ +export const getFileHash = (filename: string): string => { + // import { createHash } from "crypto" + // const hash = createHash("sha256") + // hash.update(filename) + // return hash.digest("hex") + + // Base64.toBase64(filename).substring(0, 8); + + return md5(filename) +} diff --git a/libs/zhi-siyuan-picgo/src/lib/utils/utils.ts b/libs/zhi-siyuan-picgo/src/lib/utils/utils.ts new file mode 100644 index 0000000..b76b830 --- /dev/null +++ b/libs/zhi-siyuan-picgo/src/lib/utils/utils.ts @@ -0,0 +1,87 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { isReactive, isRef, toRaw, unref } from "vue" +import { ElMessage } from "element-plus" + +/** + * 复制网页内容到剪贴板 + * + * @param text - 待复制的文本 + */ +export const copyToClipboardInBrowser = (text: string) => { + if (navigator && navigator.clipboard) { + // Copy the selected text to the clipboard + navigator.clipboard.writeText(text).then( + function () { + // The text has been successfully copied to the clipboard + ElMessage.success("复制成功") + }, + function (e) { + // An error occurred while copying the text + ElMessage.error("复制失败=>" + e) + } + ) + } else { + try { + const input = document.createElement("input") + input.style.position = "fixed" + input.style.opacity = "0" + input.value = text + document.body.appendChild(input) + input.select() + document.execCommand("copy") + document.body.removeChild(input) + ElMessage.success("复制成功") + } catch (e) { + ElMessage.error("复制失败=>" + e) + } + } +} + +/** + * get raw data from reactive or ref + */ +export const getRawData = (args: any): any => { + if (Array.isArray(args)) { + const data = args.map((item: any) => { + if (isRef(item)) { + return unref(item) + } + if (isReactive(item)) { + return toRaw(item) + } + return getRawData(item) + }) + return data + } + if (typeof args === "object") { + const data = {} as any + Object.keys(args).forEach((key) => { + const item = args[key] + if (isRef(item)) { + data[key] = unref(item) + } else if (isReactive(item)) { + data[key] = toRaw(item) + } else { + data[key] = getRawData(item) + } + }) + return data + } + return args +} + +export const trimValues = (obj: any) => { + const newObj = {} as any + Object.keys(obj).forEach((key) => { + newObj[key] = typeof obj[key] === "string" ? obj[key].trim() : obj[key] + }) + return newObj +} diff --git a/libs/zhi-siyuan-picgo/tsconfig.json b/libs/zhi-siyuan-picgo/tsconfig.json new file mode 100644 index 0000000..51eed8f --- /dev/null +++ b/libs/zhi-siyuan-picgo/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "Node", + // "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": [ + "node", + "vite/client" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue", + "custom.d.ts" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ], + "root": "." +} diff --git a/libs/zhi-siyuan-picgo/tsconfig.node.json b/libs/zhi-siyuan-picgo/tsconfig.node.json new file mode 100644 index 0000000..7065ca9 --- /dev/null +++ b/libs/zhi-siyuan-picgo/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/libs/zhi-siyuan-picgo/vite.config.ts b/libs/zhi-siyuan-picgo/vite.config.ts new file mode 100644 index 0000000..aa49bba --- /dev/null +++ b/libs/zhi-siyuan-picgo/vite.config.ts @@ -0,0 +1,77 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +/// + +import { resolve } from "path" +import { defineConfig } from "vite" +import { viteStaticCopy } from "vite-plugin-static-copy" +import dts from "vite-plugin-dts" +import minimist from "minimist" +import livereload from "rollup-plugin-livereload" + +const args = minimist(process.argv.slice(2)) +const isWatch = args.watch || args.w || false +const devDistDir = "./dist" +const distDir = isWatch ? devDistDir : "./dist" +// const distDir = devDistDir + +console.log("isWatch=>", isWatch) +console.log("distDir=>", distDir) + +export default defineConfig({ + plugins: [ + dts(), + + viteStaticCopy({ + targets: [ + { + src: "README.md", + dest: "./", + }, + { + src: "package.json", + dest: "./", + }, + ], + }), + ], + + build: { + // 输出路径 + outDir: distDir, + emptyOutDir: false, + + // 构建后是否生成 source map 文件 + sourcemap: false, + + lib: { + // Could also be a dictionary or array of multiple entry points + entry: resolve(__dirname, "src/index.ts"), + // the proper extensions will be added + // fileName: "index", + formats: ["es"], + }, + rollupOptions: { + plugins: [...(isWatch ? [livereload(devDistDir)] : [])], + // make sure to externalize deps that shouldn't be bundled + // into your library + external: [], + output: { + entryFileNames: "[name].js", + }, + }, + }, + + test: { + globals: true, + environment: "jsdom", + include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], + }, +}) diff --git a/package.json b/package.json index 88f3820..43a3e41 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,6 @@ "devDependencies": { "@terwer/commit-config-custom": "^1.0.9", "@terwer/eslint-config-custom": "^1.3.6", - "turbo": "^1.12.5" + "turbo": "^1.13.0" } } diff --git a/packages/picgo-plugin-app/README.md b/packages/picgo-plugin-app/README.md index 655e3d7..839d095 100644 --- a/packages/picgo-plugin-app/README.md +++ b/packages/picgo-plugin-app/README.md @@ -6,4 +6,38 @@ picgo plugin app for siyuan-note ``` ├── universal-picgo +``` + +## Docs + +New store path from 1.6.0 + +``` +1.5.6 之前的配置位置 + +[工作空间]/data/storage/syp/picgo/picgo.cfg.json + [工作空间]/data/storage/syp/picgo/mac.applescript + [工作空间]/data/storage/syp/picgo/i18n-cli + [工作空间]/data/storage/syp/picgo/picgo-clipboard-images + [工作空间]/data/storage/syp/picgo/external-picgo-cfg.json + [工作空间]/data/storage/syp/picgo/picgo.log + [工作空间]/data/storage/syp/picgo/picgo.log + [工作空间]/data/storage/syp/picgo/package.json + [工作空间]/data/storage/syp/picgo/package-lock.json + [工作空间]/data/storage/syp/picgo/node_modules + + +1.6.0+ 默认插件位置 + +~/.universal-picgo/picgo.cfg.json +~/.universal-picgo/mac.applescript +~/.universal-picgo/i18n-cli +~/.universal-picgo/picgo-clipboard-images +~/.universal-picgo/external-picgo-cfg.json +~/.universal-picgo/picgo.log +~/.universal-picgo/package.json +~/.universal-picgo/package-lock.json +~/.universal-picgo/node_modules + ~/.universal-picgo/node_modules/plugin-1 + ~/.universal-picgo/node_modules/plugin-2 ``` \ No newline at end of file diff --git a/packages/picgo-plugin-app/index.html b/packages/picgo-plugin-app/index.html index 9d53ff9..eac8c51 100644 --- a/packages/picgo-plugin-app/index.html +++ b/packages/picgo-plugin-app/index.html @@ -4,7 +4,7 @@ - PicGO 插件 + PicGO 图床 <%- injectScript %> diff --git a/packages/picgo-plugin-app/package.json b/packages/picgo-plugin-app/package.json index becd38b..d6d0c6d 100644 --- a/packages/picgo-plugin-app/package.json +++ b/packages/picgo-plugin-app/package.json @@ -6,32 +6,37 @@ "scripts": { "serve": "python -u scripts/serve.py && vite", "dev": "python -u scripts/dev.py", - "build": "python -u scripts/build.py" + "build": "python -u scripts/build.py", + "lint": "vue-tsc --noEmit" }, "devDependencies": { "@terwer/eslint-config-custom": "^1.3.6", "@vitejs/plugin-vue": "^5.0.4", "fast-glob": "^3.3.2", - "minimist": "^1.2.5", + "minimist": "^1.2.8", "rollup-plugin-livereload": "^2.0.5", - "typescript": "^5.2.2", + "typescript": "^5.4.3", "unplugin-auto-import": "^0.17.5", "unplugin-vue-components": "^0.26.0", - "vite": "^5.1.6", + "vite": "^5.2.6", "vite-plugin-html": "^3.2.2", "vitest": "^1.4.0", - "vue-tsc": "^2.0.6" + "vue-tsc": "^2.0.7" }, "dependencies": { "@element-plus/icons-vue": "^2.3.1", + "@iconify/json": "^2.2.196", "@vueuse/core": "^10.9.0", - "element-plus": "^2.6.1", + "element-plus": "^2.6.2", "lodash-es": "^4.17.21", + "unplugin-icons": "^0.18.5", "vue": "^3.4.21", - "vue-i18n": "^9.10.1", + "vue-i18n": "^9.10.2", "vue-router": "^4.3.0", - "zhi-common": "^1.31.0", + "zhi-common": "^1.33.0", + "zhi-device": "^2.11.0", "zhi-lib-base": "^0.8.0", - "universal-picgo": "workspace:*" + "zhi-siyuan-api": "^2.19.1", + "zhi-siyuan-picgo": "workspace:*" } } diff --git a/packages/picgo-plugin-app/src/components/ExternalPicgoSetting.vue b/packages/picgo-plugin-app/src/components/ExternalPicgoSetting.vue deleted file mode 100644 index 5b3e433..0000000 --- a/packages/picgo-plugin-app/src/components/ExternalPicgoSetting.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/packages/picgo-plugin-app/src/components/PicgoSetting.vue b/packages/picgo-plugin-app/src/components/PicgoSetting.vue deleted file mode 100644 index bc45dc7..0000000 --- a/packages/picgo-plugin-app/src/components/PicgoSetting.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/packages/picgo-plugin-app/src/components/SiyuanSetting.vue b/packages/picgo-plugin-app/src/components/SiyuanSetting.vue deleted file mode 100644 index cdc6de8..0000000 --- a/packages/picgo-plugin-app/src/components/SiyuanSetting.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/packages/picgo-plugin-app/src/components/TransportSelect.vue b/packages/picgo-plugin-app/src/components/TransportSelect.vue deleted file mode 100644 index a63fd6a..0000000 --- a/packages/picgo-plugin-app/src/components/TransportSelect.vue +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - diff --git a/packages/picgo-plugin-app/src/components/common/BackPage.vue b/packages/picgo-plugin-app/src/components/common/BackPage.vue new file mode 100644 index 0000000..84b4ca6 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/common/BackPage.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/common/ConfigForm.vue b/packages/picgo-plugin-app/src/components/common/ConfigForm.vue new file mode 100644 index 0000000..f03626d --- /dev/null +++ b/packages/picgo-plugin-app/src/components/common/ConfigForm.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/BrowserIndex.vue b/packages/picgo-plugin-app/src/components/home/BrowserIndex.vue index 7558d38..daa6845 100644 --- a/packages/picgo-plugin-app/src/components/home/BrowserIndex.vue +++ b/packages/picgo-plugin-app/src/components/home/BrowserIndex.vue @@ -8,33 +8,99 @@ --> - + diff --git a/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue b/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue index 55197fc..18e839d 100644 --- a/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue +++ b/packages/picgo-plugin-app/src/components/home/ElectronIndex.vue @@ -8,33 +8,100 @@ --> - + diff --git a/packages/picgo-plugin-app/src/components/home/controls/DragUpload.vue b/packages/picgo-plugin-app/src/components/home/controls/DragUpload.vue new file mode 100644 index 0000000..e139c4e --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/controls/DragUpload.vue @@ -0,0 +1,135 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/controls/PictureList.vue b/packages/picgo-plugin-app/src/components/home/controls/PictureList.vue new file mode 100644 index 0000000..9c327d9 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/controls/PictureList.vue @@ -0,0 +1,122 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/controls/UploadButton.vue b/packages/picgo-plugin-app/src/components/home/controls/UploadButton.vue new file mode 100644 index 0000000..f02ce9b --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/controls/UploadButton.vue @@ -0,0 +1,145 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/home/controls/UrlCopy.vue b/packages/picgo-plugin-app/src/components/home/controls/UrlCopy.vue new file mode 100644 index 0000000..1172ecc --- /dev/null +++ b/packages/picgo-plugin-app/src/components/home/controls/UrlCopy.vue @@ -0,0 +1,115 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/PicgoSetting.vue b/packages/picgo-plugin-app/src/components/setting/PicgoSetting.vue new file mode 100644 index 0000000..bab56dd --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/PicgoSetting.vue @@ -0,0 +1,75 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/SiyuanSetting.vue b/packages/picgo-plugin-app/src/components/setting/SiyuanSetting.vue new file mode 100644 index 0000000..92cec2c --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/SiyuanSetting.vue @@ -0,0 +1,46 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/picgo/BundledPicgoSetting.vue b/packages/picgo-plugin-app/src/components/setting/picgo/BundledPicgoSetting.vue new file mode 100644 index 0000000..b945f04 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/picgo/BundledPicgoSetting.vue @@ -0,0 +1,71 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/picgo/ExternalPicgoSetting.vue b/packages/picgo-plugin-app/src/components/setting/picgo/ExternalPicgoSetting.vue new file mode 100644 index 0000000..f86603b --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/picgo/ExternalPicgoSetting.vue @@ -0,0 +1,36 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicbedSetting.vue b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicbedSetting.vue new file mode 100644 index 0000000..17e83f3 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicbedSetting.vue @@ -0,0 +1,388 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoConfigSetting.vue b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoConfigSetting.vue new file mode 100644 index 0000000..0189cb5 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoConfigSetting.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoPluginSetting.vue b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoPluginSetting.vue new file mode 100644 index 0000000..fe9d106 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/setting/picgo/bundled/PicgoPluginSetting.vue @@ -0,0 +1,36 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/test/BrowserTest.vue b/packages/picgo-plugin-app/src/components/test/BrowserTest.vue new file mode 100644 index 0000000..e1bb7ef --- /dev/null +++ b/packages/picgo-plugin-app/src/components/test/BrowserTest.vue @@ -0,0 +1,61 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/components/test/ElectronTest.vue b/packages/picgo-plugin-app/src/components/test/ElectronTest.vue new file mode 100644 index 0000000..2a93f27 --- /dev/null +++ b/packages/picgo-plugin-app/src/components/test/ElectronTest.vue @@ -0,0 +1,129 @@ + + + + + + + diff --git a/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts b/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts new file mode 100644 index 0000000..8d8c57e --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoCommon.ts @@ -0,0 +1,47 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useSiyuanDevice } from "$composables/useSiyuanDevice.ts" +import { reactive } from "vue" +import { isDev } from "@/utils/Constants.ts" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" + +/** + * Picgo公共组件 + * + * @author terwer + * @since 0.6.1 + */ +export const usePicgoCommon = () => { + // private data + const { isInSiyuanOrSiyuanNewWin } = useSiyuanDevice() + + // public data + const picgoCommonData = reactive({ + isUploadLoading: false, + showDebugMsg: isDev, + loggerMsg: "", + isSiyuanOrSiyuanNewWin: isInSiyuanOrSiyuanNewWin(), + fileList: { + files: [], + }, + }) + + // public methods + const picgoCommonMethods = { + getPicgoCommonData: () => { + return picgoCommonData + }, + } + + return { + picgoCommonData, + picgoCommonMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts b/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts new file mode 100644 index 0000000..0d80c4c --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoInitPage.ts @@ -0,0 +1,97 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { createAppLogger } from "@/utils/appLogger.ts" +import { ImageParser } from "zhi-siyuan-picgo/src/lib/parser/ImageParser.ts" +import { isDev } from "@/utils/Constants.ts" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { ParsedImage } from "zhi-siyuan-picgo/src/lib/models/ParsedImage.ts" +import { onMounted, watch } from "vue" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" + +/** + * Picgo页面初始化组件 + */ +export const usePicgoInitPage = (props: any, deps: any) => { + const logger = createAppLogger("picgo-common") + const { kernelApi } = useSiyuanApi() + + // private data + const siyuanApi = kernelApi + const imageParser = new ImageParser(isDev) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // private methods + const initPage = async () => { + const pageId = props.pageId + console.log("pageId=>", pageId) + + // 图片信息 + const imageBlocks: any[] = await siyuanApi.getImageBlocksByID(pageId) + logger.debug("查询文章中的图片块=>", imageBlocks) + + if (!imageBlocks || imageBlocks.length === 0) { + return + } + + // 解析图片地址 + let retImgs: ParsedImage[] = [] + imageBlocks.forEach((page) => { + const parsedImages: ParsedImage[] = imageParser.parseImagesToArray(page.markdown) + + // 会有很多重复值 + // retImgs = retImgs.concat(retImgs, parsedImages) + // 下面的写法可以去重 + retImgs = [...new Set([...retImgs, ...parsedImages])] + }) + logger.debug("解析出来的所有的图片地址=>", retImgs) + + // 将字符串数组格式的图片信息转换成图片对象数组 + const attrs = await siyuanApi.getBlockAttrs(pageId) + const picgoPostApi = await SiyuanPicGo.getInstance() + const imageItemArray = await picgoPostApi.doConvertImagesToImagesItemArray(attrs, retImgs) + + // 页面属性 + for (let i = 0; i < imageItemArray.length; i++) { + const imageItem = imageItemArray[i] + picgoCommonData.fileList.files.push(imageItem) + } + } + + // publish methods + const picgoInitMethods = { + initPage: async () => { + await initPage() + }, + } + + /** + * 监听props + */ + watch( + () => props.pageId, + async () => { + await initPage() + logger.debug("Picgo初始化") + } + ) + + onMounted(async () => { + await initPage() + }) + + return { + picgoInitMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoManage.ts b/packages/picgo-plugin-app/src/composables/usePicgoManage.ts new file mode 100644 index 0000000..ee1f0f4 --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoManage.ts @@ -0,0 +1,166 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useVueI18n } from "$composables/useVueI18n.ts" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { createAppLogger } from "@/utils/appLogger.ts" +import { reactive } from "vue" +import { ElMessage, ElMessageBox } from "element-plus" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" + +/** + * Picgo图片管理组件 + */ +export const usePicgoManage = (props: any, deps: any) => { + const logger = createAppLogger("picgo-manage") + + // private data + const { t } = useVueI18n() + const { kernelApi } = useSiyuanApi() + + const siyuanApi = kernelApi + + // public data + const picgoManageData = reactive({ + dialogImageUrl: "", + dialogPreviewVisible: false, + }) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // private methods + /** + * 处理图片后续(单个图片,替换) + * + * @param imgInfos + * @param imageItem + */ + const doAfterUploadReplace = (imgInfos: any, imageItem: ImageItem) => { + let imageJson + if (typeof imgInfos == "string") { + logger.warn("doAfterUpload返回的是字符串,需要解析") + imageJson = JSON.parse(imgInfos) + } else { + imageJson = imgInfos + } + + picgoCommonData.loggerMsg = JSON.stringify(imgInfos) + logger.debug("doAfterUpload,imgInfos=>", imgInfos) + + if (imageJson && imageJson.length > 0) { + const img = imageJson[0] + const rtnItem = new ImageItem(imageItem.originUrl, img.imgUrl, false) + picgoCommonData.loggerMsg += "\nnewItem=>" + JSON.stringify(rtnItem) + + const newList = picgoCommonData.fileList.files.map((x: ImageItem) => { + if (x.hash === imageItem.hash) { + return rtnItem + } + return x + }) + + // 刷新列表 + picgoCommonData.fileList.files = [] + for (const newItem of newList) { + picgoCommonData.fileList.files.push(newItem) + } + } + ElMessage.success(t("main.opt.success")) + } + + // public methods + const picgoManageMethods = { + handleUploadCurrentImageToBed: async (imageItem: ImageItem) => { + if (!imageItem.isLocal) { + ElMessageBox.confirm("已经是远程图片,是否仍然覆盖上传?", t("main.opt.warning"), { + confirmButtonText: t("main.opt.ok"), + cancelButtonText: t("main.opt.cancel"), + type: "warning", + }) + .then(async () => { + try { + picgoCommonData.isUploadLoading = true + await picgoManageMethods.doUploadCurrentImageToBed(imageItem, true) + picgoCommonData.isUploadLoading = false + + ElMessage.success("图片已经成功重新上传至图床") + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }) + .catch((e) => { + picgoCommonData.isUploadLoading = false + + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + ",图片上传异常=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }) + } else { + try { + await picgoManageMethods.doUploadCurrentImageToBed(imageItem) + picgoCommonData.isUploadLoading = false + + ElMessage.success("图片已经成功上传至图床") + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + + picgoCommonData.isUploadLoading = false + } + }, + + /** + * 单个传,否则无法将图片对应,需要替换 + * + * @param imageItem + * @param forceUpload 强制上传 + */ + doUploadCurrentImageToBed: async (imageItem: ImageItem, forceUpload?: boolean) => { + const pageId = props.pageId + const attrs = await siyuanApi.getBlockAttrs(pageId) + + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.uploadSingleImageToBed(pageId, attrs, imageItem, forceUpload) + + // 处理后续 + doAfterUploadReplace(imgInfos, imageItem) + }, + + handlePictureCardPreview: (url: string) => { + picgoManageData.dialogImageUrl = url ?? "" + picgoManageData.dialogPreviewVisible = true + }, + } + + return { + picgoManageData, + picgoManageMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts b/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts new file mode 100644 index 0000000..e185b15 --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/usePicgoUpload.ts @@ -0,0 +1,261 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2022-2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { useRouter } from "vue-router" +import { createAppLogger } from "@/utils/appLogger.ts" +import { useVueI18n } from "$composables/useVueI18n.ts" +import { reactive } from "vue" +import { ImageItem } from "zhi-siyuan-picgo/src/lib/models/ImageItem.ts" +import { ElMessage } from "element-plus" +import { SiyuanPicGo } from "@/utils/siyuanPicgo.ts" +import { useSiyuanApi } from "$composables/useSiyuanApi.ts" +import { StrUtil } from "zhi-common" + +/** + * Picgo上传组件 + */ +export const usePicgoUpload = (props: any, deps: any, refs: any) => { + // private data + const logger = createAppLogger("picgo-upload") + const { t } = useVueI18n() + const router = useRouter() + const { kernelApi } = useSiyuanApi() + + const siyuanApi = kernelApi + + // public data + const picgoUploadData = reactive({}) + + // deps + const picgoCommonMethods = deps.picgoCommonMethods + + // deps data + const picgoCommonData = picgoCommonMethods.getPicgoCommonData() + + // refs + const refSelectedFiles = refs.refSelectedFiles + + // private methods + /** + * 处理图片后续 + * + * @param imgInfos + * @param imageItem 上传已存在图片需要,新图片留空 + */ + const doAfterUpload = (imgInfos: any, imageItem?: ImageItem) => { + if (imageItem) { + doAfterUploadReplace(imgInfos, imageItem) + } else { + let imageJson + if (typeof imgInfos == "string") { + logger.warn("doAfterUpload返回的是字符串,需要解析") + imageJson = JSON.parse(imgInfos) + } else { + imageJson = imgInfos + } + + picgoCommonData.loggerMsg = JSON.stringify(imgInfos) + logger.debug("doAfterUpload,imgInfos=>", imgInfos) + + const img = imageJson[0] + const rtnItem = new ImageItem(img.imgUrl, img.imgUrl, false) + picgoCommonData.loggerMsg += "\nnewItem=>" + JSON.stringify(rtnItem) + + picgoCommonData.fileList.files.push(rtnItem) + ElMessage.success(t("main.opt.success")) + } + } + + /** + * 处理图片后续(单个图片,替换) + * + * @param imgInfos + * @param imageItem + */ + const doAfterUploadReplace = (imgInfos: any, imageItem: ImageItem) => { + let imageJson + if (typeof imgInfos == "string") { + logger.warn("doAfterUpload返回的是字符串,需要解析") + imageJson = JSON.parse(imgInfos) + } else { + imageJson = imgInfos + } + + picgoCommonData.loggerMsg = JSON.stringify(imgInfos) + logger.debug("doAfterUpload,imgInfos=>", imgInfos) + + if (imageJson && imageJson.length > 0) { + const img = imageJson[0] + const rtnItem = new ImageItem(imageItem.originUrl, img.imgUrl, false) + picgoCommonData.loggerMsg += "\nnewItem=>" + JSON.stringify(rtnItem) + + const newList = picgoCommonData.fileList.files.map((x: ImageItem) => { + if (x.hash === imageItem.hash) { + return rtnItem + } + return x + }) + + // 刷新列表 + picgoCommonData.fileList.files = [] + for (const newItem of newList) { + picgoCommonData.fileList.files.push(newItem) + } + } + ElMessage.success(t("main.opt.success")) + } + + // public methods + const picgoUploadMethods = { + bindFileControl: () => { + refSelectedFiles.value.click() + }, + doUploadPicSelected: async (event: any) => { + picgoCommonData.isUploadLoading = true + + try { + const fileList = event.target.files + + logger.debug("onRequest fileList=>", fileList) + if (!fileList || fileList.length === 0) { + ElMessage.error("请选择图片") + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + "请选择图片" + picgoCommonData.isUploadLoading = false + return + } + + // 获取选择的文件的路径数组 + const filePaths = [] + for (let i = 0; i < fileList.length; i++) { + if (fileList.item(i).path) { + filePaths.push(fileList.item(i).path) + } else { + logger.debug("路径为空,忽略") + } + } + + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.upload(filePaths) + // 处理后续 + doAfterUpload(imgInfos) + + picgoCommonData.isUploadLoading = false + } catch (e: any) { + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + e + picgoCommonData.isUploadLoading = false + } + }, + doUploadPicFromClipboard: async () => { + picgoCommonData.isUploadLoading = true + + try { + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.upload() + // 处理后续 + doAfterUpload(imgInfos) + + picgoCommonData.isUploadLoading = false + } catch (e: any) { + if (e.toString().indexOf("cancel") <= -1) { + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>", e) + } + picgoCommonData.loggerMsg = t("main.opt.failure") + "=>" + e + picgoCommonData.isUploadLoading = false + } + }, + /** + * 单个传,否则无法将图片对应 + * + * @param imageItem + * @param forceUpload 强制上传 + */ + doUploadImageToBed: async (imageItem: ImageItem, forceUpload?: boolean) => { + const pageId = props.pageId + const attrs = await siyuanApi.getBlockAttrs(pageId) + + const picgoPostApi = await SiyuanPicGo.getInstance() + const imgInfos = await picgoPostApi.uploadSingleImageToBed(pageId, attrs, imageItem, forceUpload) + + // 处理后续 + if (forceUpload) { + doAfterUpload(imgInfos, imageItem) + } else { + doAfterUpload(imgInfos) + } + }, + doUploaddAllImagesToBed: async () => { + picgoCommonData.isUploadLoading = true + + try { + let hasLocalImages = false + const imageItemArray = picgoCommonData.fileList.files + + for (let i = 0; i < imageItemArray.length; i++) { + const imageItem = imageItemArray[i] + if (!imageItem.isLocal) { + logger.debug("已经上传过图床,请勿重复上传=>", imageItem.originUrl) + continue + } + + hasLocalImages = true + await picgoUploadMethods.doUploadImageToBed(imageItem, true) + } + + picgoCommonData.isUploadLoading = false + if (!hasLocalImages) { + ElMessage.warning("未发现本地图片,不上传") + } else { + ElMessage.success("图片已经全部上传至图床") + } + } catch (e) { + picgoCommonData.isUploadLoading = false + + ElMessage({ + type: "error", + message: t("main.opt.failure") + "=>" + e, + }) + logger.error(t("main.opt.failure") + "=>" + e) + } + }, + doDownloadAllImagesToLocal: async () => { + if (StrUtil.isEmptyString(props.pageId)) { + ElMessage.error("pageId不能为空") + return + } + + picgoCommonData.isUploadLoading = true + + try { + const ret = await siyuanApi.netAssets2LocalAssets(props.pageId) + logger.debug("ret=>", ret) + ElMessage.success("网络图片下载成功") + } catch (e: any) { + throw new Error("网络图片下载失败" + e.toString()) + } finally { + picgoCommonData.isUploadLoading = false + } + }, + } + + return { + picgoUploadData, + picgoUploadMethods, + } +} diff --git a/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts b/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts new file mode 100644 index 0000000..1291fb9 --- /dev/null +++ b/packages/picgo-plugin-app/src/composables/useSiyuanApi.ts @@ -0,0 +1,63 @@ +/* + * GNU GENERAL PUBLIC LICENSE + * Version 3, 29 June 2007 + * + * Copyright (C) 2024 Terwer, Inc. + * Everyone is permitted to copy and distribute verbatim copies + * of this license document, but changing it is not allowed. + */ + +import { createAppLogger } from "@/utils/appLogger.ts" +import { useSiyuanSetting } from "@/stores/useSiyuanSetting.ts" +import { SiyuanPicgoConfig } from "zhi-siyuan-picgo" +import { useSiyuanDevice } from "$composables/useSiyuanDevice.ts" +import { SiYuanApiAdaptor, SiyuanKernelApi } from "zhi-siyuan-api" + +/** + * 通用 Siyuan API 封装 + */ +export const useSiyuanApi = () => { + const logger = createAppLogger("use-siyuan-api") + const { getSiyuanSetting } = useSiyuanSetting() + + const siyuanSetting = getSiyuanSetting() + const siyuanApiUrl = siyuanSetting.value.apiUrl + const siyuanAuthToken = siyuanSetting.value.password + const siyuanConfig = new SiyuanPicgoConfig(siyuanApiUrl, siyuanAuthToken) + siyuanConfig.cookie = siyuanSetting.value.cookie + + const blogApi = new SiYuanApiAdaptor(siyuanConfig) + const kernelApi = new SiyuanKernelApi(siyuanConfig) + const { isInChromeExtension } = useSiyuanDevice() + + const isStorageViaSiyuanApi = () => { + // docker - 在 .env.docker 配置 VITE_DEFAULT_TYPE=siyuan + // vercel - 在环境变量配置 VITE_DEFAULT_TYPE=siyuan + // node - 启动参数加 VITE_DEFAULT_TYPE=siyuan node VITE_SIYUAN_API_URL=http://127.0.0.1:6806 + // 插件SPA(PC客户端) - VITE_DEFAULT_TYPE: siyuan + // 插件SPA(Docker浏览器客户端) - VITE_DEFAULT_TYPE: siyuan + // 插件SPA(本地客户端浏览器) - VITE_DEFAULT_TYPE: siyuan + // const storeViaSiyuanApi = process.env.VITE_DEFAULT_TYPE === "siyuan" + const defaultType = process.env.VITE_DEFAULT_TYPE ?? "siyuan" + const storeViaSiyuanApi = defaultType === "siyuan" + logger.info("defaultType=>", defaultType) + logger.info("storeViaSiyuanApi=>", String(storeViaSiyuanApi)) + return storeViaSiyuanApi + } + + const isUseSiyuanProxy = () => { + if (isInChromeExtension()) { + return false + } + + return isStorageViaSiyuanApi() + } + + return { + blogApi, + kernelApi, + siyuanConfig, + isStorageViaSiyuanApi, + isUseSiyuanProxy, + } +} diff --git a/packages/picgo-plugin-app/src/i18n/en_US.ts b/packages/picgo-plugin-app/src/i18n/en_US.ts index c569c74..60f71fe 100644 --- a/packages/picgo-plugin-app/src/i18n/en_US.ts +++ b/packages/picgo-plugin-app/src/i18n/en_US.ts @@ -8,6 +8,9 @@ */ export default { + "common.back": "Back", + "common.select": "Select", + "setting.picgo.refer.to.here": "For details, please refer here", "setting.picgo.refer.to": "For details, please refer to:", "setting.picgo.refer.to.online.doc": "Picgo configuration online documentation", "setting.picgo.picbed": "Picbed setting", @@ -19,6 +22,7 @@ export default { "setting.picgo.picbed.unselected.tip": "Unselected", "setting.picgo.picbed.set.default": "Set as default picbed", "setting.picgo.picbed.current.selected.tip": "Current selected picbed is:", + "setting.picgo.picbed.change.tip": "In order to make alterations, kindly select 「Set as default picbed」", "setting.picgo.picbed.current.tip": "Current picbed is:", "setting.picgo.picbed.uploader.config.name": "Picbed config name", "setting.picgo.picbed.uploader.config.name.placeholder": "Please input config name", @@ -91,6 +95,7 @@ export default { "main.opt.success": "Success", "main.opt.failure": "Error", "main.opt.edit": "Edit", + "main.opt.add": "Add", "main.opt.delete": "Delete", "main.opt.loading": "In operation...", "main.opt.warning": "Warn tips", @@ -124,4 +129,19 @@ export default { "setting.blog.siyuan.password": "Siyuan Token", "setting.blog.siyuan.password.tip": "Siyuan Token, which is empty by default", "form.validate.name.required": "Please enter a name", + "upload.select.tip1": "Drop file here, Ctrl+V paste image here or", + "upload.select.tip2": "click to upload", + "upload.select.limit": "jp(e)g/png/gif/svg/webp files with a size less than 500kb, max upload size is", + "upload.tab.upload": "Picture upload", + "upload.tab.setting": "Plugin setting", + "component.test": "Component test", + "siyuan.setting.title": "Siyuan setting", + "upload.adaptor.bundled": "Bundled PicGO", + "upload.adaptor.app": "PicGO(app)", + "upload.adaptor.core": "PicGO-Core", + "setting.cors.title": "CORS Proxy", + "setting.cors.title.tip": + "The CORS proxy is essential for browsers and Docker environments. For further consultation, feel free to contact youweics@163.com.", + "upload.no.beds": + 'No image hosting service is currently available. Kindly proceed to "Image Hosting Settings" to add a new image hosting service.', } diff --git a/packages/picgo-plugin-app/src/i18n/zh_CN.ts b/packages/picgo-plugin-app/src/i18n/zh_CN.ts index 15445bc..55f6a0a 100644 --- a/packages/picgo-plugin-app/src/i18n/zh_CN.ts +++ b/packages/picgo-plugin-app/src/i18n/zh_CN.ts @@ -8,6 +8,9 @@ */ export default { + "common.back": "返回", + "common.select": "请选择", + "setting.picgo.refer.to.here": "详情请参考这里", "setting.picgo.refer.to": "详情请参考:", "setting.picgo.refer.to.online.doc": "PicGO配置在线文档", "setting.picgo.picbed": "图床设置", @@ -15,11 +18,12 @@ export default { "setting.picgo.picgo.open.config.file": "打开配置文件", "setting.picgo.picgo.click.to.open": "点击打开", "setting.picgo.picgo.choose.showed.picbed": "请选择显示的图床", - "setting.picgo.picbed.selected.tip": "已选中", - "setting.picgo.picbed.unselected.tip": "未选中", + "setting.picgo.picbed.selected.tip": "已选择", + "setting.picgo.picbed.unselected.tip": "未选择", "setting.picgo.picbed.set.default": "设为默认图床", - "setting.picgo.picbed.current.selected.tip": "已选中图床:", - "setting.picgo.picbed.current.tip": "当前默认图床是:", + "setting.picgo.picbed.current.selected.tip": "已切换到:", + "setting.picgo.picbed.current.tip": "目前正在使用的图床是:", + "setting.picgo.picbed.change.tip": "。如需修改,请选择对应的配置项。", "setting.picgo.picbed.uploader.config.name": "图床配置名", "setting.picgo.picbed.uploader.config.name.placeholder": "请输入配置名称", "setting.picgo.config.name": "配置名称", @@ -87,6 +91,7 @@ export default { "main.opt.success": "操作成功", "main.opt.failure": "操作失败", "main.opt.edit": "编辑", + "main.opt.add": "新增", "main.opt.delete": "删除", "main.opt.loading": "操作中...", "main.opt.warning": "警告信息", @@ -111,12 +116,26 @@ export default { "picgo.type.switch.active.text": "切换到内置PicGO", "picgo.type.switch.unactive.text": "切换到外部PicGO", "picgo.type.external.title": "外部PicGO设置", - "setting.picgo.external.setting.apiurl": "API地址", - "setting.picgo.external.setting.apiurl.tip": "请输入外部外部PicGO的API地址,默认是:http://127.0.0.1:36677", + "setting.picgo.external.setting.apiurl": "PicGo server 上传接口", + "setting.picgo.external.setting.apiurl.tip": "请输入 PicGo server 上传接口地址,默认是:http://127.0.0.1:36677", "siyuan.config.setting": "思源设置", "setting.blog.siyuan.apiurl": "思源API地址", "setting.blog.siyuan.apiurl.tip": "思源API地址,默认是:http://127.0.0.1:6806", "setting.blog.siyuan.password": "思源Token", "setting.blog.siyuan.password.tip": "思源Token,默认是空", "form.validate.name.required": "请输入名称", + "upload.select.tip1": "拖拽图片到这里、Ctrl+V从剪切板粘贴或者", + "upload.select.tip2": "选择图片", + "upload.select.limit": "支持jp(e)g/png/gif/svg/webp 格式,大小不超过 500kb, 单次最大上传图片数量限制为", + "upload.tab.upload": "图片上传", + "upload.tab.setting": "插件设置", + "component.test": "组件测试", + "siyuan.setting.title": "思源笔记设置", + "upload.default.adaptor": "默认上传器", + "upload.adaptor.bundled": "内置PicGO", + "upload.adaptor.app": "PicGO(app)", + "upload.adaptor.core": "PicGO-Core", + "setting.cors.title": "CORS 代理", + "setting.cors.title.tip": "CORS 代理,浏览器、dcoker以及某些插件可能需要,可付费咨询 youweics@163.com", + "upload.no.beds": "暂无可用图床,请前往「图床设置」新增图床", } diff --git a/packages/picgo-plugin-app/src/layouts/AppLayout.vue b/packages/picgo-plugin-app/src/layouts/AppLayout.vue index 5fec438..69b56ff 100644 --- a/packages/picgo-plugin-app/src/layouts/AppLayout.vue +++ b/packages/picgo-plugin-app/src/layouts/AppLayout.vue @@ -8,9 +8,11 @@ -->