Add file module for uploads and storage
- File model + migration: file metadata, content lives in object storage - IFileStorage port with local disk and S3-compatible (RustFS/MinIO) adapters - POST /files, GET /files/:id/download, DELETE /files/:id - MIME sniffed from content; only safe images served inline, nosniff header - FileOwnerRegistry: owner modules register to grant visibility and keep their files from GC; FileGcService removes orphaned files hourly - TransformResponseInterceptor passes StreamableFile through Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5cfc8986d7
commit
f32c6e348d
49 files changed
+1992
-2
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { StoredFile } from './stored-file.entity.js';
|
||||
import { FileForbiddenException } from '../exceptions/file-forbidden.exception.js';
|
||||
|
||||
const build = (mimeType = 'image/png', uploaderId: string | null = 'user-1') =>
|
||||
StoredFile.reconstitute({
|
||||
id: 'file-1',
|
||||
storageKey: 'files/key',
|
||||
fileName: 'photo.png',
|
||||
mimeType,
|
||||
size: 10,
|
||||
uploaderId,
|
||||
createdAt: new Date('2026-01-01T00:00:00.000Z'),
|
||||
});
|
||||
|
||||
describe('StoredFile', () => {
|
||||
it('creates a new file with sanitized name and no id', () => {
|
||||
const file = StoredFile.create({
|
||||
storageKey: 'files/key',
|
||||
fileName: '../secret/report.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 100,
|
||||
uploaderId: 'user-1',
|
||||
});
|
||||
|
||||
expect(file.id).toBeNull();
|
||||
expect(file.fileName).toBe('report.pdf');
|
||||
expect(file.uploaderId).toBe('user-1');
|
||||
});
|
||||
|
||||
it('allows inline view only for safe image types', () => {
|
||||
expect(build('image/png').isInlineViewable).toBe(true);
|
||||
expect(build('image/svg+xml').isInlineViewable).toBe(false);
|
||||
expect(build('text/html').isInlineViewable).toBe(false);
|
||||
expect(build('application/octet-stream').isInlineViewable).toBe(false);
|
||||
});
|
||||
|
||||
it('treats only the uploader as the owner', () => {
|
||||
expect(() => build().assertOwnedBy('user-1')).not.toThrow();
|
||||
expect(() => build().assertOwnedBy('user-2')).toThrow(FileForbiddenException);
|
||||
expect(() => build('image/png', null).assertOwnedBy('user-1')).toThrow(FileForbiddenException);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user