Dépôt de dossiers : - flag `allowDirectories` (défaut false) par cible dans config.json + schéma. - POST /upload accepte `?path=<chemin relatif>` : l'arborescence est recréée sous `dir`, nom d'origine conservé, fichier écrasé si présent. - src/lib/paths.ts : assainissement segment par segment (pas de `..`, pas de séparateur) + vérification de confinement dans storage.ts -> aucune sortie possible de la cible. Refus 400 si la cible n'autorise pas les dossiers. - GET /targets expose `allowDirectories`. - Page : glisser-déposer d'un dossier (parcours récursif webkitGetAsEntry) + bouton « parcourir un dossier », visibles selon la cible. Très gros fichiers (~10 Go) : - `requestTimeout: 0` / `connectionTimeout: 0` : aucun timeout serveur imposé. - écriture disque avec un tampon de 1 Mio. - doc : `maxFileSizeMb` à l'échelle Go, réglages reverse proxy. Tests : +paths.test.ts, +placement (storage), +dossier & gros fichier (upload) ; 60 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
53 lines
1.9 KiB
TypeScript
53 lines
1.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { ValidationError } from '../src/errors.js';
|
|
import { parseRelativePath } from '../src/lib/paths.js';
|
|
|
|
describe('parseRelativePath', () => {
|
|
it('sépare dossiers et nom de fichier', () => {
|
|
expect(parseRelativePath('vacances/plage/img 001.jpg')).toEqual({
|
|
segments: ['vacances', 'plage'],
|
|
fileName: 'img 001.jpg',
|
|
});
|
|
});
|
|
|
|
it('accepte un simple nom de fichier (aucun sous-dossier)', () => {
|
|
expect(parseRelativePath('photo.png')).toEqual({ segments: [], fileName: 'photo.png' });
|
|
});
|
|
|
|
it('accepte les séparateurs `/` et `\\` et les segments accentués', () => {
|
|
expect(parseRelativePath('Été 2024\\sous-dossier/clé.mp4')).toEqual({
|
|
segments: ['Été 2024', 'sous-dossier'],
|
|
fileName: 'clé.mp4',
|
|
});
|
|
});
|
|
|
|
it('neutralise `.` et `..` (aucune remontée possible)', () => {
|
|
expect(parseRelativePath('../../etc/passwd')).toEqual({
|
|
segments: ['etc'],
|
|
fileName: 'passwd',
|
|
});
|
|
expect(parseRelativePath('a/./b/../c/d.txt')).toEqual({
|
|
segments: ['a', 'b', 'c'],
|
|
fileName: 'd.txt',
|
|
});
|
|
});
|
|
|
|
it('retire les caractères de contrôle et remplace les caractères hostiles', () => {
|
|
const parsed = parseRelativePath(`a${String.fromCharCode(1)}b/fi:le?.txt`);
|
|
expect(parsed.segments).toEqual(['ab']);
|
|
expect(parsed.fileName).toBe('fi_le_.txt');
|
|
});
|
|
|
|
it('rejette un chemin vide après nettoyage', () => {
|
|
expect(() => parseRelativePath('../..')).toThrow(ValidationError);
|
|
expect(() => parseRelativePath('///')).toThrow(ValidationError);
|
|
});
|
|
|
|
it('rejette une arborescence trop profonde ou trop longue', () => {
|
|
expect(() => parseRelativePath(Array(40).fill('d').join('/') + '/f.txt')).toThrow(
|
|
ValidationError,
|
|
);
|
|
expect(() => parseRelativePath('a/'.repeat(3000) + 'f.txt')).toThrow(ValidationError);
|
|
});
|
|
});
|