import type { TFile, Vault } from "obsidian"; import { requestUrl } from "obsidian"; import { logger } from "../utils/logger"; import { ApiPaths } from "../utils/api-paths"; export default class ImageUploader { private readonly siteUrl: string; private readonly token: string; private readonly headers: Record; private readonly uploadedImagesCache: Map; constructor(siteUrl: string, token: string) { this.siteUrl = siteUrl; this.token = token; this.uploadedImagesCache = new Map(); this.headers = { Authorization: `Bearer ${this.token}`, }; } private toBase64(arrayBuffer: ArrayBuffer): string { const bytes = new Uint8Array(arrayBuffer); let binary = ""; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } public async uploadImage(filePath: string, vault: Vault): Promise { const normalizedPath = filePath.replace(/\\/g, "/"); if (this.uploadedImagesCache.has(normalizedPath)) { return this.uploadedImagesCache.get(normalizedPath) || null; } try { const file = vault.getAbstractFileByPath(normalizedPath); if (!file || !(file instanceof TFile)) { logger.error("ImageUploader", `文件不存在或不是有效文件: ${normalizedPath}`); return null; } const arrayBuffer = await vault.readBinary(file); const base64Data = this.toBase64(arrayBuffer); const filename = normalizedPath.split("/").pop() || "image"; const boundary = `----ObsidianHalo${Date.now()}`; let body = `--${boundary}\r\n`; body += `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`; body += "Content-Type: application/octet-stream\r\n"; body += "Content-Transfer-Encoding: base64\r\n\r\n"; body += base64Data; body += `\r\n--${boundary}--\r\n`; const response = await requestUrl({ url: `${this.siteUrl}${ApiPaths.attachments.upload()}`, method: "POST", headers: { ...this.headers, "Content-Type": `multipart/form-data; boundary=${boundary}`, }, body: body, }); const jsonResponse = response.json; if (jsonResponse?.url) { this.uploadedImagesCache.set(normalizedPath, jsonResponse.url); return jsonResponse.url; } return null; } catch (error) { logger.error("ImageUploader", `上传图片失败: ${filePath}`, error); return null; } } public async uploadImages(filePaths: string[], vault: Vault): Promise> { const result = new Map(); for (const filePath of filePaths) { const url = await this.uploadImage(filePath, vault); if (url) { result.set(filePath, url); } } return result; } public getCachedUrl(filePath: string): string | undefined { const normalizedPath = filePath.replace(/\\/g, "/"); return this.uploadedImagesCache.get(normalizedPath); } }