feat(server): tus hook helpers (metadata parse, extension extract, ownership check)
This commit is contained in:
@@ -0,0 +1,62 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
parseUploadMetadata,
|
||||||
|
extractExtension,
|
||||||
|
buildTusMetadata,
|
||||||
|
isOwnerOfUpload,
|
||||||
|
} from './tusHooks.js';
|
||||||
|
|
||||||
|
describe('parseUploadMetadata', () => {
|
||||||
|
it('parses base64-encoded metadata pairs', () => {
|
||||||
|
const filename = Buffer.from('photo.png').toString('base64');
|
||||||
|
const meta = parseUploadMetadata(`filename ${filename},foo bar`);
|
||||||
|
expect(meta.filename).toBe('photo.png');
|
||||||
|
expect(meta.foo).toBeUndefined(); // 'bar' is not valid base64 padding here
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object on null input', () => {
|
||||||
|
expect(parseUploadMetadata(null)).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractExtension', () => {
|
||||||
|
it('lowercases the extension', () => {
|
||||||
|
expect(extractExtension('Photo.PNG')).toBe('.png');
|
||||||
|
});
|
||||||
|
it('returns empty string when no extension', () => {
|
||||||
|
expect(extractExtension('Makefile')).toBe('');
|
||||||
|
});
|
||||||
|
it('strips path components', () => {
|
||||||
|
expect(extractExtension('../../../etc/passwd.txt')).toBe('.txt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildTusMetadata', () => {
|
||||||
|
it('includes snowflakeId, userId, originalName', () => {
|
||||||
|
const meta = buildTusMetadata({
|
||||||
|
snowflakeId: '1234',
|
||||||
|
userId: 'u-9',
|
||||||
|
originalName: 'photo.png',
|
||||||
|
});
|
||||||
|
expect(meta).toMatchObject({
|
||||||
|
snowflakeId: '1234',
|
||||||
|
userId: 'u-9',
|
||||||
|
originalName: 'photo.png',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isOwnerOfUpload', () => {
|
||||||
|
it('returns true on matching userId', () => {
|
||||||
|
expect(isOwnerOfUpload({ userId: 'u-9' }, 'u-9')).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns false on mismatch', () => {
|
||||||
|
expect(isOwnerOfUpload({ userId: 'u-9' }, 'u-10')).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false on missing metadata', () => {
|
||||||
|
expect(isOwnerOfUpload({}, 'u-9')).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false when metadata.userId is an empty string', () => {
|
||||||
|
expect(isOwnerOfUpload({ userId: '' }, 'u-9')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
export interface UploadMetadata {
|
||||||
|
snowflakeId?: string;
|
||||||
|
userId?: string;
|
||||||
|
originalName?: string;
|
||||||
|
[k: string]: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse the comma-separated `Upload-Metadata` header into a flat object. */
|
||||||
|
export function parseUploadMetadata(raw: string | null | undefined): UploadMetadata {
|
||||||
|
if (!raw) return {};
|
||||||
|
const out: UploadMetadata = {};
|
||||||
|
for (const pair of raw.split(',')) {
|
||||||
|
const trimmed = pair.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
const [key, b64] = trimmed.split(' ');
|
||||||
|
if (!key) continue;
|
||||||
|
if (!b64) {
|
||||||
|
// Boolean-style key (no value)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const decoded = Buffer.from(b64, 'base64').toString('utf8');
|
||||||
|
// Reject if base64 round-trip mismatches — protects against odd encodings
|
||||||
|
if (Buffer.from(decoded, 'utf8').toString('base64').replace(/=+$/, '') !==
|
||||||
|
b64.replace(/=+$/, '')) continue;
|
||||||
|
out[key] = decoded;
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract a sanitized lowercase extension, stripped of path components. */
|
||||||
|
export function extractExtension(originalName: string): string {
|
||||||
|
const base = path.basename(originalName);
|
||||||
|
const ext = path.extname(base).toLowerCase();
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build the metadata object stored on tus uploads at PRE_CREATE. */
|
||||||
|
export function buildTusMetadata(input: {
|
||||||
|
snowflakeId: string;
|
||||||
|
userId: string;
|
||||||
|
originalName: string;
|
||||||
|
}): UploadMetadata {
|
||||||
|
return {
|
||||||
|
snowflakeId: input.snowflakeId,
|
||||||
|
userId: input.userId,
|
||||||
|
originalName: input.originalName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True iff the supplied userId matches metadata.userId. Used in PRE_PATCH. */
|
||||||
|
export function isOwnerOfUpload(metadata: UploadMetadata, userId: string): boolean {
|
||||||
|
return Boolean(metadata.userId) && metadata.userId === userId;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user