Files
Obsidian/obsidian-halo/src/service/image-uploader.ts
T
Serendipity 12a7aebeff feat: Enhance HaloClient with retry logic and improve error handling
- Added retry options to HaloClient for handling transient errors.
- Refactored request methods in HaloClient to utilize retry logic.
- Updated HaloService to include logging for error handling.
- Introduced ApiPaths utility for managing API endpoints.
- Implemented logger utility for consistent logging across services.
- Added tests for ContentService, Error handling, and TaxonomyService.
- Created retry utility for managing retry logic with exponential backoff.
- Updated types to include additional properties for better API response handling.
2026-04-28 18:01:26 +08:00

100 lines
3.0 KiB
TypeScript

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<string, string>;
private readonly uploadedImagesCache: Map<string, string>;
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<string | null> {
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<Map<string, string>> {
const result = new Map<string, string>();
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);
}
}