agent-ecosystem/src/main/services/team/TeamAttachmentStore.ts
iliya b163892d20 feat: enhance attachment handling and improve UI components
- Updated local image storage documentation to reflect changes in protocol handling for attachments.
- Added attachment metadata handling in message sending and inbox writing processes, improving attachment management.
- Implemented size validation for attachments in the TeamAttachmentStore, ensuring compliance with storage limits.
- Enhanced UI components to support attachment previews and improved user experience in managing attachments.
- Introduced a new RoleSelect component for better role management within teams.
2026-03-05 12:15:03 +02:00

105 lines
2.8 KiB
TypeScript

import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as path from 'path';
import { atomicWriteAsync } from './atomicWrite';
import type { AttachmentFileData, AttachmentPayload } from '@shared/types';
const logger = createLogger('Service:TeamAttachmentStore');
const ATTACHMENTS_DIR = 'attachments';
const MAX_ATTACHMENTS_FILE_BYTES = 64 * 1024 * 1024; // 64MB safety cap
export class TeamAttachmentStore {
private assertSafePathSegment(label: string, value: string): void {
if (
value.length === 0 ||
value.includes('/') ||
value.includes('\\') ||
value.includes('..') ||
value.includes('\0')
) {
throw new Error(`Invalid ${label}`);
}
}
private getDir(teamName: string): string {
this.assertSafePathSegment('teamName', teamName);
return path.join(getTeamsBasePath(), teamName, ATTACHMENTS_DIR);
}
private getFilePath(teamName: string, messageId: string): string {
this.assertSafePathSegment('messageId', messageId);
return path.join(this.getDir(teamName), `${messageId}.json`);
}
async saveAttachments(
teamName: string,
messageId: string,
attachments: AttachmentPayload[]
): Promise<void> {
if (attachments.length === 0) return;
const fileData: AttachmentFileData[] = attachments.map((a) => ({
id: a.id,
data: a.data,
mimeType: a.mimeType,
}));
await atomicWriteAsync(this.getFilePath(teamName, messageId), JSON.stringify(fileData));
logger.debug(
`[${teamName}] Saved ${attachments.length} attachment(s) for message ${messageId}`
);
}
async getAttachments(teamName: string, messageId: string): Promise<AttachmentFileData[]> {
const filePath = this.getFilePath(teamName, messageId);
let raw: string;
try {
const stat = await fs.promises.stat(filePath);
if (!stat.isFile() || stat.size > MAX_ATTACHMENTS_FILE_BYTES) {
return [];
}
raw = await fs.promises.readFile(filePath, 'utf8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
throw error;
}
let parsed: unknown;
try {
parsed = JSON.parse(raw) as unknown;
} catch {
return [];
}
if (!Array.isArray(parsed)) {
return [];
}
const result: AttachmentFileData[] = [];
for (const item of parsed) {
if (!item || typeof item !== 'object') continue;
const row = item as Partial<AttachmentFileData>;
if (
typeof row.id !== 'string' ||
typeof row.data !== 'string' ||
typeof row.mimeType !== 'string'
) {
continue;
}
result.push({
id: row.id,
data: row.data,
mimeType: row.mimeType,
});
}
return result;
}
}