Files
Toju/toju-app/src/app/domains/attachment/infrastructure/attachment-storage.service.ts
Myx 11917f3412 fix: Make attachments unique when downloaded
Fixes the issue with attachments replacing each other locally so files with same filename appears as the same file
2026-03-30 00:08:53 +02:00

132 lines
3.4 KiB
TypeScript

import { Injectable, inject } from '@angular/core';
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service';
import type { Attachment } from '../domain/attachment.models';
import {
resolveAttachmentStorageBucket,
resolveAttachmentStoredFilename,
sanitizeAttachmentRoomName
} from './attachment-storage.helpers';
@Injectable({ providedIn: 'root' })
export class AttachmentStorageService {
private readonly electronBridge = inject(ElectronBridgeService);
async resolveExistingPath(
attachment: Pick<Attachment, 'filePath' | 'savedPath'>
): Promise<string | null> {
return this.findExistingPath([attachment.filePath, attachment.savedPath]);
}
async resolveLegacyImagePath(filename: string, roomName: string): Promise<string | null> {
const appDataPath = await this.resolveAppDataPath();
if (!appDataPath) {
return null;
}
return this.findExistingPath([`${appDataPath}/server/${sanitizeAttachmentRoomName(roomName)}/image/${filename}`]);
}
async readFile(filePath: string): Promise<string | null> {
const electronApi = this.electronBridge.getApi();
if (!electronApi || !filePath) {
return null;
}
try {
return await electronApi.readFile(filePath);
} catch {
return null;
}
}
async saveBlob(
attachment: Pick<Attachment, 'id' | 'filename' | 'mime'>,
blob: Blob,
roomName: string
): Promise<string | null> {
const electronApi = this.electronBridge.getApi();
const appDataPath = await this.resolveAppDataPath();
if (!electronApi || !appDataPath) {
return null;
}
try {
const directoryPath = `${appDataPath}/server/${sanitizeAttachmentRoomName(roomName)}/${resolveAttachmentStorageBucket(attachment.mime)}`;
await electronApi.ensureDir(directoryPath);
const arrayBuffer = await blob.arrayBuffer();
const diskPath = `${directoryPath}/${resolveAttachmentStoredFilename(attachment.id, attachment.filename)}`;
await electronApi.writeFile(diskPath, this.arrayBufferToBase64(arrayBuffer));
return diskPath;
} catch {
return null;
}
}
async deleteFile(filePath: string): Promise<void> {
const electronApi = this.electronBridge.getApi();
if (!electronApi || !filePath) {
return;
}
try {
await electronApi.deleteFile(filePath);
} catch { /* best-effort cleanup */ }
}
private async resolveAppDataPath(): Promise<string | null> {
const electronApi = this.electronBridge.getApi();
if (!electronApi) {
return null;
}
try {
return await electronApi.getAppDataPath();
} catch {
return null;
}
}
private async findExistingPath(candidates: (string | null | undefined)[]): Promise<string | null> {
const electronApi = this.electronBridge.getApi();
if (!electronApi) {
return null;
}
for (const candidatePath of candidates) {
if (!candidatePath) {
continue;
}
try {
if (await electronApi.fileExists(candidatePath)) {
return candidatePath;
}
} catch { /* keep trying remaining candidates */ }
}
return null;
}
private arrayBufferToBase64(buffer: ArrayBuffer): string {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let index = 0; index < bytes.byteLength; index++) {
binary += String.fromCharCode(bytes[index]);
}
return btoa(binary);
}
}