12a7aebeff
- 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.
100 lines
3.0 KiB
TypeScript
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);
|
|
}
|
|
}
|