fix(storage): verify migration of exports up to one GiB

This commit is contained in:
2026-09-28 14:08:59 +02:00
parent e9b2e3e229
commit a3f4a7c004
4 changed files with 32 additions and 15 deletions

View File

@@ -17,7 +17,7 @@ python3 -B scripts/storage/tests/host_integration.py
```
Der S3-Test verwendet ausschließlich neu angelegte Testcontainer mit künstlichen
Daten. Er prüft über 1.000 Objekte, Pagination, 256 MiB, leere Dateien,
Daten. Er prüft über 1.000 Objekte, Pagination, eine 1-GiB-Datei, leere Dateien,
Sonderzeichen, Metadaten, SigV4, 24-Stunden-Links, Streaming, verbotene Zugriffe,
beschädigte Zielobjekte, erneute Kopie und echten Restore in ein frisches
Verzeichnis. Die gesicherten Testdaten bleiben unter `/tmp/fedeo-storage-*`
@@ -27,7 +27,7 @@ Der Host-Test verwendet einen echten PostgreSQL-/MinIO-/SeaweedFS-Stack und
künstliche Anwendungscontainer. Er ersetzt keine fachliche Abnahme von FEDEO
(PDF, WebDAV, Exporte, Mandantenrechte) auf der konkreten Instanz.
Die erste Übernahme ist auf 200.000 Objekte und 256 MiB pro Objekt begrenzt.
Die erste Übernahme ist auf 200.000 Objekte und 1 GiB pro Objekt begrenzt.
Das größte Objekt wird auf Platte gespult; RAM-Verbrauch wächst nicht mit dessen
Größe. Multipart-Übernahmen und Sonderkonfigurationen werden abgelehnt.

View File

@@ -15,7 +15,7 @@ const S3 = requireSdk('@aws-sdk/client-s3');
const { getSignedUrl } = requireSdk('@aws-sdk/s3-request-presigner');
// The pilot deliberately has a tested bound; no untested multipart conversion.
export const MAX_OBJECT_BYTES = 256 * 1024 * 1024;
export const MAX_OBJECT_BYTES = 1024 * 1024 * 1024;
export const MAX_OBJECTS = 200_000;
export const client = config => new S3.S3Client({ ...config, forcePathStyle: true, maxAttempts: 3 });
export const call = (s3, operation, input) => s3.send(new S3[`${operation}Command`](input));
@@ -45,7 +45,7 @@ export function validateKeys(objects) {
requireThat(!/[\x00-\x1f\x7f\\]/.test(key) && segments.every(s => s && s !== '.' && s !== '..'),
`Nicht verlustfrei übertragbarer Schlüssel: ${JSON.stringify(key)}`);
requireThat(Number.isSafeInteger(size) && size >= 0 && size <= MAX_OBJECT_BYTES,
`Objekt außerhalb der getesteten Größe (256 MiB): ${JSON.stringify(key)}`);
`Objekt außerhalb der getesteten Größe (${MAX_OBJECT_BYTES / 1024 / 1024} MiB): ${JSON.stringify(key)}`);
for (let i = 1; i < segments.length; i++) {
requireThat(!keys.has(segments.slice(0, i).join('/')), `Datei-/Verzeichniskonflikt: ${JSON.stringify(key)}`);
}

View File

@@ -1,6 +1,6 @@
// Explicit integration run; never discovers or connects to an existing FEDEO stack.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtemp, mkdir, writeFile, readFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@@ -28,6 +28,20 @@ async function ready(s3) {
}
throw new Error('S3 did not become ready');
}
async function restoredContentsReady(s3, bucket, report) {
// Like the host helper, wait for volume registration after a cold start.
// ListBuckets can already succeed while reading object data still returns 500.
const deadline = Date.now() + 180_000;
for (;;) {
try { return await verifyManifest(s3, bucket, report); }
catch (error) {
const transient = [500, 502, 503, 504].includes(error.$metadata?.httpStatusCode) ||
['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH'].includes(error.code);
if (!transient || Date.now() >= deadline) throw error;
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
}
async function weed(name, data) {
await mkdir(join(data, 'master'), { recursive: true });
run(name, '-p', '127.0.0.1::9000', '-v', `${data}:/data`, '-v', `${root}/config:/etc/fedeo:ro`,
@@ -73,11 +87,12 @@ try {
Body: Buffer.from(`fixture-${i}`), ContentType: 'text/plain', Metadata: { custom: 'preserved' } });
await call(source, 'PutObject', { Bucket, Key: 'empty', Body: Buffer.alloc(0), ContentType: 'application/octet-stream' });
await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES,
Body: Readable.from((function* () { for (let i = 0; i < 256; i++) yield Buffer.alloc(1024 * 1024, i); })()),
Body: Readable.from((function* () { for (let i = 0; i < MAX_OBJECT_BYTES / (1024 * 1024); i++) yield Buffer.alloc(1024 * 1024, i % 256); })()),
ContentType: 'application/zip', ContentDisposition: 'attachment; filename="large.zip"',
CacheControl: 'private', ContentLanguage: 'de', Expires: new Date('2030-01-01T00:00:00Z') });
const before = await inventory(source, Bucket);
assert.equal(before.count, 1005);
assert.equal(before.objects.find(o => o.Key === 'large.zip').Size, MAX_OBJECT_BYTES);
await call(source, 'PutObject', { Bucket, Key: 'unsupported-marker/', Body: '' });
await assert.rejects(inventory(source, Bucket), /Schlüssel/);
await call(source, 'DeleteObject', { Bucket, Key: 'unsupported-marker/' });
@@ -95,7 +110,7 @@ try {
const restoredAdmin = client({ ...restoredConfig, credentials: adminCredentials });
try {
await ready(restoredAdmin);
await verifyManifest(restored, Bucket, report);
await restoredContentsReady(restored, Bucket, report);
await contract(restored, restoredConfig, Bucket, { foreignBucket: 'foreign-private' });
} finally { restored.destroy(); restoredAdmin.destroy(); }
console.log(JSON.stringify({ result: 'passed', objects: report.count, bytes: report.bytes,
@@ -103,7 +118,10 @@ try {
process.env.FEDEO_MINIO_TEST_IMAGE || 'fedeo-minio-fixture:2025-09-07', '--format', '{{.Id}}'),
evidence: root }, null, 2));
} catch (error) {
for (const name of containers) { try { console.error(docker('logs', '--tail', '12', name)); } catch {} }
for (const name of containers) {
const logs = spawnSync('docker', ['logs', '--tail', '30', name], { encoding: 'utf8' });
console.error(logs.stdout + logs.stderr);
}
throw error;
} finally {
source?.destroy(); target?.destroy(); admin?.destroy();