mirror of
https://github.com/appleec/e-obsidian-upload-plugin.git
synced 2026-07-22 11:50:27 +00:00
feat: 新增halo博客模式并修复跨域问题
This commit is contained in:
parent
a2abc83109
commit
ba6a1380a6
11 changed files with 616 additions and 27 deletions
|
|
@ -12,19 +12,25 @@
|
|||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^16.11.6",
|
||||
"@types/object-path": "^0.11.4",
|
||||
"@types/qs": "^6.9.16",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"esbuild": "0.17.3",
|
||||
"esbuild-copy-static-files": "^0.1.0",
|
||||
"file-type-checker": "^1.1.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"object-path": "^0.11.8",
|
||||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"object-path": "^0.11.8"
|
||||
"qs": "^6.13.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,19 +4,44 @@ const config = {
|
|||
text: 'Lskypro',
|
||||
value: 'lsky',
|
||||
},
|
||||
{
|
||||
text: 'Halo',
|
||||
value: 'halo',
|
||||
},
|
||||
{
|
||||
text: 'Github',
|
||||
value: 'github',
|
||||
},
|
||||
],
|
||||
reqHeader: {
|
||||
"Authorization": "Bearer Jq7mTKdjN7BxMjLoG8Jj8eaGGV2GuurwumOGFCBm",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "multipart/form-data"
|
||||
lskySetting: {
|
||||
"apiURL": "https://img.elinzy.com/api/v1/upload",
|
||||
"apiReqHeader": {
|
||||
"Authorization": "Bearer Jq7mTKdjN7BxMjLoG8Jj8eaGGV2GuurwumOGFCBm",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
"apiReqBody": {
|
||||
"file": "$FILE",
|
||||
"strategy_id": 2
|
||||
},
|
||||
"imgUrlPath": "data.pathname",
|
||||
"imgUrlPrefix": "https://blog.elinzy.com/upload/eblog/"
|
||||
},
|
||||
reqBody: {
|
||||
"file": "$FILE",
|
||||
"strategy_id": 2
|
||||
haloSetting: {
|
||||
// See: https://api.halo.run/#/
|
||||
"apiURL": "https://blog.elinzy.com/apis/api.console.halo.run/v1alpha1/attachments/upload",
|
||||
"apiReqHeader": {
|
||||
"Authorization": "Bearer pat_eyJraWQiOiI1SG9MUWVvUW10bV9zd2JISFRSODFEd1dUWVNyUUlyc0N3NjE2R1dQTXpnIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2Jsb2cuZWxpbnp5LmNvbS8iLCJzdWIiOiJhZG1pbiIsImlhdCI6MTcxMTk1NjY5MiwianRpIjoiN2E5NDY3NjEtY2QzYi1kODc4LTJkOGMtZWQxNmUwZDk4NzFjIiwicGF0X25hbWUiOiJwYXQtYWRtaW4tbkdnZG0ifQ.cl7n0f9SqP5qwYUW2ZM6DL6Ex6Xh9GvxL_hwEDJuUn8tlyuzOhcjUqkGSCO1D8E0lqym7RtV7xDQzSLkQybKbSwRqeMEDbLbdLMDpzD2Vu1aptz5m0ifsm9pvpm2Wqnn_px68arhqHqmMwP-8N9F1m-fMu1ovBuXI2gzHA56Ne1yWsSObeasxqNP1CNsez_abdv0fPKxbBsV_2JrQeHYc0y90M8V0WZ9d3_zdrgOMg6Hm481VprrHUwB9nxrsVsCxd_ROwf93ZhGo_VPV5YG8_Pz4CwRGzkIABVHO1leUXyMD24nOpG9csGo-mauuik7oi1h6eLUMi2902qKFUctwRsF4lKQSP6h2Li9UUt0FYQp78uaTDbke3eAxLq1nqWfOn3Lplpe1tz8MPXtZGclx2xz4zkQWs5TdOU4cuukMIJoXfc1VCbAR9AuaG1BVMZYnL-scgg9I9GNHjtcfgFMskUaEKQlyYTNiIweaqQHMlBHvdxJjCMko9jdB2qnMNw9cs2QfMreK278A6ucHMOB9WGVh0thpor1gk2MvAcV3k-ai21BKUdOLooxftdXbI7V9xif_fLEZwFvtf2RtRqChJpXLyGS2-xoOwg00kz0khckv43vntSCPZ8pqHnTAWsy_ssf80WOajs2guY25l04PXAshEGndvziB0Aod3AoXdY",
|
||||
"Content-Type": "multipart/form-data",
|
||||
"Host": "blog.elinzy.com",
|
||||
},
|
||||
"apiReqBody": {
|
||||
"file": "$FILE",
|
||||
"policyName": "attachment-policy-gUpSV", // 策略
|
||||
"groupName": "attachment-group-JWlvm" // 分组
|
||||
},
|
||||
"imgUrlPath": "data.metadata.annotations['storage.halo.run/uri']",
|
||||
"imgUrlPrefix": "https://blog.elinzy.com/"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
86
src/haloUpload.ts
Normal file
86
src/haloUpload.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
// Defaults
|
||||
import objectPath from "object-path";
|
||||
import fileTypeChecker from 'file-type-checker';
|
||||
|
||||
// Types
|
||||
import { IUploader, IUploadPluginSettings} from "./type";
|
||||
|
||||
// Utils
|
||||
import { RestClient } from './utils/rest-client';
|
||||
import { FormItems, Media, fileToArrayBuffer } from './utils/utils';
|
||||
import {Notice} from "obsidian";
|
||||
|
||||
// Class
|
||||
class HaloUpload implements IUploader {
|
||||
private readonly settings: IUploadPluginSettings;
|
||||
private readonly client: RestClient;
|
||||
|
||||
constructor(settings: IUploadPluginSettings) {
|
||||
this.settings = settings;
|
||||
|
||||
this.client = new RestClient();
|
||||
}
|
||||
|
||||
async upload(file: File, options?: any): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const uploadBody = JSON.parse(this.settings.haloSetting.apiReqBody);
|
||||
|
||||
const [buf, filename] = await fileToArrayBuffer(file);
|
||||
const fileType = fileTypeChecker.detectFile(buf);
|
||||
const media: Media = {
|
||||
mimeType: fileType?.mimeType ?? 'application/octet-stream',
|
||||
fileName: filename,
|
||||
content: buf
|
||||
}
|
||||
|
||||
const formItems = new FormItems();
|
||||
for (const key in uploadBody) {
|
||||
if (uploadBody[key] == "$FILE") {
|
||||
formItems.append(key, media)
|
||||
}
|
||||
else {
|
||||
formItems.append(key, uploadBody[key])
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Obsidian 是基于 Electron 开发的,对于请求 Halo 的接口来说相当于是在浏览器上请求的,所以存在跨域问题。
|
||||
// See: https://github.com/halo-sigs/obsidian-halo/issues/2
|
||||
// axios.post(this.settings.haloSetting.apiURL, formData, {
|
||||
// "headers": JSON.parse(this.settings.haloSetting.apiReqHeader),
|
||||
// withCredentials: true,
|
||||
// }).then(res => {
|
||||
// const url = [this.settings.haloSetting.imgUrlPrefix, objectPath.get(res.data, this.settings.haloSetting.imgUrlPath)].join('')
|
||||
//
|
||||
// resolve(url)
|
||||
// }, err => {
|
||||
// reject(err)
|
||||
// })
|
||||
|
||||
this.client.httpPost(
|
||||
this.settings.haloSetting.apiURL,
|
||||
formItems,
|
||||
{
|
||||
headers: JSON.parse(this.settings.haloSetting.apiReqHeader),
|
||||
}
|
||||
)
|
||||
.then((res: {}) => {
|
||||
const url = [
|
||||
this.settings.haloSetting.imgUrlPrefix,
|
||||
objectPath.get(res, JSON.parse(this.settings.haloSetting.imgUrlPath))
|
||||
].join('');
|
||||
|
||||
resolve(url)
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
//
|
||||
drawSettingsTab(parentEL: HTMLDivElement) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default HaloUpload;
|
||||
|
|
@ -1,26 +1,27 @@
|
|||
// Defaults
|
||||
import { IUploadPluginSettings } from "./type";
|
||||
import { IUploadPluginSettings, IUploader } from "./type";
|
||||
import axios from "axios";
|
||||
import objectPath from "object-path";
|
||||
import {Notice} from "obsidian";
|
||||
|
||||
class LskyUpload {
|
||||
class LskyUpload implements IUploader {
|
||||
private readonly settings: IUploadPluginSettings;
|
||||
|
||||
constructor(settings: IUploadPluginSettings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
async upload(file: File, fullPath?: string): Promise<string> {
|
||||
async upload(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const formData = new FormData();
|
||||
const uploadBody = JSON.parse(this.settings.lskySetting.apiReqBody);
|
||||
|
||||
for (const key in uploadBody) {
|
||||
if (uploadBody[key] == "$FILE") {
|
||||
formData.append(key, file, file.name)
|
||||
formData.append(key, file, file.name);
|
||||
}
|
||||
else {
|
||||
formData.append(key, uploadBody[key])
|
||||
formData.append(key, uploadBody[key]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
112
src/main.ts
112
src/main.ts
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
App,
|
||||
Editor,
|
||||
MarkdownView,
|
||||
Notice,
|
||||
|
|
@ -44,7 +43,14 @@ const DEFAULT_SETTINGS: IUploadPluginSettings = {
|
|||
apiReqBody: "{\"file\": \"$FILE\"}",
|
||||
imgUrlPath: "",
|
||||
imgUrlPrefix: "",
|
||||
}
|
||||
},
|
||||
haloSetting: {
|
||||
apiURL: "",
|
||||
apiReqHeader: "",
|
||||
apiReqBody: "{\"file\": \"$FILE\"}",
|
||||
imgUrlPath: "",
|
||||
imgUrlPrefix: "",
|
||||
},
|
||||
}
|
||||
|
||||
class UploadPlugin extends Plugin {
|
||||
|
|
@ -63,7 +69,9 @@ class UploadPlugin extends Plugin {
|
|||
|
||||
// 赋值
|
||||
this.helper = new Helper(this.app);
|
||||
this.uploader = uploader(this.settings);
|
||||
|
||||
// 初始化设置 uploader
|
||||
this.setupUploader();
|
||||
|
||||
// 图标
|
||||
addIcon(
|
||||
|
|
@ -150,7 +158,7 @@ class UploadPlugin extends Plugin {
|
|||
|
||||
// 自定义方法
|
||||
// 右键菜单上传文件
|
||||
async menuUploadFile(file: TFile) {
|
||||
async menuUploadFileTest(file: TFile) {
|
||||
new Notice('准备上传');
|
||||
|
||||
let content = this.helper.getValue();
|
||||
|
|
@ -232,6 +240,93 @@ class UploadPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
async menuUploadFile(file: TFile) {
|
||||
if (!file) {
|
||||
new Notice('不合法文件,请重新尝试');
|
||||
return;
|
||||
}
|
||||
if (!this.uploader) {
|
||||
new Notice("未配置上传器,请检查设置");
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice('准备上传');
|
||||
|
||||
let content = this.helper.getValue();
|
||||
const basePath = this.helper.getBasePath();
|
||||
const fileArray = this.helper.getAllFiles();
|
||||
|
||||
let imageList: IImage[] = [];
|
||||
|
||||
// 查找文档中相同路径文件
|
||||
for (const match of fileArray) {
|
||||
const imageName = match.name;
|
||||
const encodedUri = match.path;
|
||||
|
||||
const fileName = basename(decodeURI(encodedUri));
|
||||
|
||||
if (file && file.name === fileName) {
|
||||
const abstractImageFile = join(basePath, file.path);
|
||||
|
||||
if (isAssetTypeAnImage(abstractImageFile)) {
|
||||
imageList.push({
|
||||
path: abstractImageFile,
|
||||
name: imageName,
|
||||
source: match.source,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (imageList.length === 0) {
|
||||
new Notice("没有匹配到文件");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = [];
|
||||
|
||||
// 通过文件路径读取文件,并转化为文件格式
|
||||
for (let i = 0; i < imageList.length; i++) {
|
||||
const { path, name } = imageList[i];
|
||||
const fileName = basename(decodeURI(path));
|
||||
const buffer: Buffer = await new Promise((resolve, reject) => {
|
||||
readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
const arrayBuffer = bufferToArrayBuffer(buffer);
|
||||
files.push(new File([arrayBuffer], fileName));
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
new Notice("文件读取失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过uploader上传文件
|
||||
this.uploader.upload(files[0])
|
||||
.then(res => {
|
||||
// 文件上传成功后,替换文档中相同路径文件的link
|
||||
imageList.map(item => {
|
||||
content = content.replaceAll(
|
||||
item.source,
|
||||
``
|
||||
);
|
||||
});
|
||||
|
||||
// 更新文档
|
||||
this.helper.setValue(content);
|
||||
|
||||
new Notice("上传成功");
|
||||
})
|
||||
.catch(err => {
|
||||
new Notice(`上传失败: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册右键菜单事件
|
||||
registerMenuEvent() {
|
||||
this.registerEvent(
|
||||
|
|
@ -256,6 +351,15 @@ class UploadPlugin extends Plugin {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
// 设置 uploader
|
||||
setupUploader() {
|
||||
try {
|
||||
this.uploader = uploader(this.settings);
|
||||
} catch (e) {
|
||||
console.log(`设置 Uploader 失败: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UploadPlugin;
|
||||
|
|
|
|||
|
|
@ -60,8 +60,9 @@ class UploadSettingsTab extends PluginSettingTab {
|
|||
e.setValue(this.plugin.settings.mode)
|
||||
e.onChange(async value => {
|
||||
this.plugin.settings.mode = value;
|
||||
// NOTE: 重新设置uploader
|
||||
this.plugin.setupUploader();
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
await this.drawModeSettings(this.modeDiv, value);
|
||||
})
|
||||
})
|
||||
|
|
@ -76,6 +77,9 @@ class UploadSettingsTab extends PluginSettingTab {
|
|||
case 'lsky':
|
||||
this.drawLskySettings(parentEl);
|
||||
break;
|
||||
case 'halo':
|
||||
this.drawHaloSettings(parentEl);
|
||||
break;
|
||||
case 'github':
|
||||
this.drawGithubSettings(parentEl);
|
||||
break;
|
||||
|
|
@ -186,6 +190,103 @@ class UploadSettingsTab extends PluginSettingTab {
|
|||
})
|
||||
}
|
||||
|
||||
// Halo 设置
|
||||
private drawHaloSettings(parentEL: HTMLDivElement) {
|
||||
// Api URL
|
||||
new Setting(parentEL)
|
||||
.setName("API 地址")
|
||||
.setDesc("请求地址")
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.haloSetting.apiURL)
|
||||
.onChange(async (value) => {
|
||||
try {
|
||||
this.plugin.settings.haloSetting.apiURL = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Api request header
|
||||
new Setting(parentEL)
|
||||
.setName("POST Header")
|
||||
.setDesc("请求头,json 格式")
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("输入合法的请求头")
|
||||
.setValue(this.plugin.settings.haloSetting.apiReqHeader)
|
||||
.onChange(async (value) => {
|
||||
try {
|
||||
this.plugin.settings.haloSetting.apiReqHeader = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
text.inputEl.rows = 5
|
||||
text.inputEl.cols = 40
|
||||
})
|
||||
|
||||
// Api request body
|
||||
new Setting(parentEL)
|
||||
.setName("POST Body")
|
||||
.setDesc("请求体,json 格式")
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("输入合法的请求体")
|
||||
.setValue(this.plugin.settings.haloSetting.apiReqBody)
|
||||
.onChange(async (value) => {
|
||||
try {
|
||||
this.plugin.settings.haloSetting.apiReqBody = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
text.inputEl.rows = 5
|
||||
text.inputEl.cols = 40
|
||||
})
|
||||
new Setting(parentEL)
|
||||
.setName("图片 URL 路径")
|
||||
.setDesc("返回json数据中的图片URL字段的路径,如:data/pathname")
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.haloSetting.imgUrlPath)
|
||||
.onChange(async (value) => {
|
||||
try {
|
||||
this.plugin.settings.haloSetting.imgUrlPath = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(parentEL)
|
||||
.setName("图片 URL 前缀")
|
||||
.setDesc("可选,当填入时,URL = 前缀 + 图片的路径值")
|
||||
.addText(text => {
|
||||
text
|
||||
.setPlaceholder("")
|
||||
.setValue(this.plugin.settings.haloSetting.imgUrlPrefix)
|
||||
.onChange(async (value) => {
|
||||
try {
|
||||
this.plugin.settings.haloSetting.imgUrlPrefix = value;
|
||||
await this.plugin.saveSettings();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Github 设置
|
||||
private drawGithubSettings(parentEL: HTMLDivElement) {}
|
||||
}
|
||||
|
|
|
|||
10
src/type.ts
10
src/type.ts
|
|
@ -22,8 +22,16 @@ export interface IUploadPluginSettings {
|
|||
imgUrlPrefix: string;
|
||||
|
||||
lskySetting: ILskySettings;
|
||||
haloSetting: ILskySettings;
|
||||
}
|
||||
|
||||
export interface IUploader {
|
||||
upload(file: File, fullPath?: string): Promise<string>;
|
||||
upload(file: File | IMedia, options?: Record<any, any>): Promise<string>;
|
||||
}
|
||||
|
||||
export interface IMedia {
|
||||
mimeType: string;
|
||||
fileName: string;
|
||||
content: ArrayBuffer;
|
||||
}
|
||||
export type SafeAny = any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import LskyUpload from "./lskyUpload";
|
||||
import HaloUpload from "./haloUpload";
|
||||
|
||||
// Types
|
||||
import { IUploadPluginSettings, IUploader } from './type';
|
||||
|
|
@ -7,6 +8,8 @@ function Uploader(settings: IUploadPluginSettings): IUploader {
|
|||
switch (settings.mode) {
|
||||
case 'lsky':
|
||||
return new LskyUpload(settings);
|
||||
case 'halo':
|
||||
return new HaloUpload(settings);
|
||||
default:
|
||||
throw new Error('should not reach here!')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { MarkdownView, App } from "obsidian";
|
||||
import { MarkdownView, App, TFile, FileSystemAdapter } from "obsidian";
|
||||
import { parse } from "path";
|
||||
|
||||
interface Image {
|
||||
|
|
@ -119,4 +119,14 @@ export default class Helper {
|
|||
|
||||
return blackDomainList.some(blackDomain => domain.includes(blackDomain));
|
||||
}
|
||||
|
||||
async readBinary(file: TFile) {
|
||||
return this.app.vault.readBinary(file);
|
||||
}
|
||||
|
||||
getBasePath() {
|
||||
return (
|
||||
this.app.vault.adapter as FileSystemAdapter
|
||||
).getBasePath()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/utils/rest-client.ts
Normal file
81
src/utils/rest-client.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { requestUrl } from 'obsidian';
|
||||
import { getBoundary, FormItems, type FormItemNameMapper } from './utils';
|
||||
|
||||
interface RestOptions {
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export class RestClient {
|
||||
constructor(
|
||||
private readonly options?: RestOptions
|
||||
) {
|
||||
console.log(options);
|
||||
}
|
||||
|
||||
async httpGet(
|
||||
url: string,
|
||||
options?: {
|
||||
headers: Record<string, string>
|
||||
}
|
||||
): Promise<unknown> {
|
||||
const endpoint = url;
|
||||
const opts = {
|
||||
headers: {},
|
||||
...options
|
||||
};
|
||||
console.log('REST GET', endpoint, opts);
|
||||
const response = await requestUrl({
|
||||
url: endpoint,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'user-agent': 'obsidian.md',
|
||||
...opts.headers
|
||||
}
|
||||
});
|
||||
console.log('GET response', response);
|
||||
return response.json;
|
||||
}
|
||||
|
||||
async httpPost(
|
||||
url: string,
|
||||
body: any,
|
||||
options?: {
|
||||
headers: Record<string, string>;
|
||||
formItemNameMapper?: FormItemNameMapper;
|
||||
}
|
||||
): Promise<unknown> {
|
||||
const endpoint = url;
|
||||
const predefinedHeaders: Record<string, string> = {};
|
||||
let requestBody: any;
|
||||
|
||||
if (body instanceof FormItems) {
|
||||
const boundary = getBoundary();
|
||||
requestBody = await body.toArrayBuffer({
|
||||
boundary,
|
||||
nameMapper: options?.formItemNameMapper
|
||||
});
|
||||
predefinedHeaders['content-type'] = `multipart/form-data; boundary=${boundary}`;
|
||||
} else if (body instanceof ArrayBuffer) {
|
||||
requestBody = body;
|
||||
} else {
|
||||
requestBody = JSON.stringify(body);
|
||||
predefinedHeaders['content-type'] = 'application/json';
|
||||
}
|
||||
|
||||
const response = await requestUrl({
|
||||
url: endpoint,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'user-agent': 'obsidian.md',
|
||||
...predefinedHeaders,
|
||||
...options?.headers
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
console.log('POST response', response);
|
||||
return response.json;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
import { resolve, extname, relative, join, parse, posix } from "path";
|
||||
import { resolve, extname, relative, join, parse, posix, basename} from "path";
|
||||
import { format } from 'date-fns';
|
||||
import { isArray, isFunction, isString, template } from 'lodash-es';
|
||||
import { SafeAny, IMedia } from "../type";
|
||||
|
||||
const IMAGE_EXT_LIST = [
|
||||
".png",
|
||||
".jpg",
|
||||
|
|
@ -11,14 +15,30 @@ const IMAGE_EXT_LIST = [
|
|||
".avif",
|
||||
];
|
||||
|
||||
export function isAnImage(ext: string) {
|
||||
return IMAGE_EXT_LIST.includes(ext.toLowerCase());
|
||||
}
|
||||
|
||||
export function isAssetTypeAnImage(path: string): Boolean {
|
||||
return isAnImage(extname(path));
|
||||
/**
|
||||
* File 转 ArrayBuffer
|
||||
* @param file
|
||||
*/
|
||||
export async function fileToArrayBuffer(file: File) {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
const fr = new FileReader();
|
||||
const filename = file.name;
|
||||
|
||||
fr.readAsArrayBuffer(file);
|
||||
fr.addEventListener("loadend",(e: any) => {
|
||||
const buf = e.target.result;
|
||||
resolve([buf, filename]);
|
||||
},false);
|
||||
fr.addEventListener("error", (e: any) => {
|
||||
reject(e);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* buffer 转 ArrayBuffer
|
||||
* @param buffer
|
||||
*/
|
||||
export function bufferToArrayBuffer(buffer: Buffer) {
|
||||
const arrayBuffer = new ArrayBuffer(buffer.length);
|
||||
const view = new Uint8Array(arrayBuffer);
|
||||
|
|
@ -27,3 +47,147 @@ export function bufferToArrayBuffer(buffer: Buffer) {
|
|||
}
|
||||
return arrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 boundary
|
||||
*/
|
||||
export function getBoundary(): string {
|
||||
return `----obsidianBoundary${format(new Date(), 'yyyyMMddHHmmss')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验图片是否合法
|
||||
* @param ext
|
||||
*/
|
||||
export function isAnImage(ext: string) {
|
||||
return IMAGE_EXT_LIST.includes(ext.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检验图片路径是否合法
|
||||
* @param path
|
||||
*/
|
||||
export function isAssetTypeAnImage(path: string): Boolean {
|
||||
return isAnImage(extname(path));
|
||||
}
|
||||
|
||||
|
||||
type UrlGetter = () => string;
|
||||
|
||||
/**
|
||||
* 获取 URL
|
||||
* @param url
|
||||
* @param defaultValue
|
||||
* @param params
|
||||
*/
|
||||
export function getUrl(
|
||||
url: string | UrlGetter | undefined,
|
||||
defaultValue: string,
|
||||
params?: { [p: string]: string | number }
|
||||
): string {
|
||||
let resultUrl: string;
|
||||
if (isString(url)) {
|
||||
resultUrl = url;
|
||||
} else if (isFunction(url)) {
|
||||
resultUrl = url();
|
||||
} else {
|
||||
resultUrl = defaultValue;
|
||||
}
|
||||
if (params) {
|
||||
const compiled = template(resultUrl);
|
||||
return compiled(params);
|
||||
} else {
|
||||
return resultUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是合法的Media
|
||||
* @param obj
|
||||
*/
|
||||
export function isMedia(obj: any): obj is IMedia {
|
||||
return (
|
||||
typeof obj === 'object'
|
||||
&& obj !== null
|
||||
&& 'mimeType' in obj && typeof obj.mimeType === 'string'
|
||||
&& 'fileName' in obj && typeof obj.fileName === 'string'
|
||||
&& 'content' in obj && obj.content instanceof ArrayBuffer
|
||||
);
|
||||
}
|
||||
|
||||
export interface Media {
|
||||
mimeType: string;
|
||||
fileName: string;
|
||||
content: ArrayBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert original item name to custom one.
|
||||
*
|
||||
* @param name original item name. If `isArray` is `true`, which means is in an array, the `name` will be appended by `[]`
|
||||
* @param isArray whether this item is in an array
|
||||
*/
|
||||
export type FormItemNameMapper = (name: string, isArray: boolean) => string;
|
||||
|
||||
export class FormItems {
|
||||
#formData: Record<string, SafeAny> = {};
|
||||
|
||||
append(name: string, data: string): FormItems;
|
||||
append(name: string, data: IMedia): FormItems;
|
||||
append(name: string, data: string | IMedia): FormItems {
|
||||
const existing = this.#formData[name];
|
||||
if (existing) {
|
||||
this.#formData[name] = [ existing ];
|
||||
this.#formData[name].push(data);
|
||||
} else {
|
||||
this.#formData[name] = data;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
toArrayBuffer(option: {
|
||||
boundary: string;
|
||||
nameMapper?: FormItemNameMapper;
|
||||
}): Promise<ArrayBuffer> {
|
||||
const CRLF = '\r\n';
|
||||
const itemPart = (name: string, data: string | IMedia, isArray: boolean) => {
|
||||
let itemName = name;
|
||||
if (option.nameMapper) {
|
||||
itemName = option.nameMapper(name, isArray);
|
||||
}
|
||||
|
||||
body.push(encodedItemStart);
|
||||
if (isString(data)) {
|
||||
body.push(encoder.encode(`Content-Disposition: form-data; name="${itemName}"${CRLF}${CRLF}`));
|
||||
body.push(encoder.encode(data));
|
||||
} else {
|
||||
const media = data;
|
||||
body.push(encoder.encode(`Content-Disposition: form-data; name="${itemName}"; filename="${media.fileName}"${CRLF}Content-Type: ${media.mimeType}${CRLF}${CRLF}`));
|
||||
body.push(media.content);
|
||||
}
|
||||
body.push(encoder.encode(CRLF));
|
||||
};
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const encodedItemStart = encoder.encode(`--${option.boundary}${CRLF}`);
|
||||
const body: ArrayBuffer[] = [];
|
||||
Object.entries(this.#formData).forEach(([ name, data ]) => {
|
||||
if (isArray(data)) {
|
||||
data.forEach(item => {
|
||||
itemPart(`${name}[]`, item, true);
|
||||
});
|
||||
} else {
|
||||
itemPart(name, data, false);
|
||||
}
|
||||
});
|
||||
body.push(encoder.encode(`--${option.boundary}--`));
|
||||
return new Blob(body).arrayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
export function formItemNameMapper(name: string, isArray: boolean): string {
|
||||
if (name === 'file' && !isArray) {
|
||||
return 'media[]';
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue