fix(storage): preserve explicit S3 MIME types in SeaweedFS

This commit is contained in:
2026-09-28 14:34:30 +02:00
parent a3f4a7c004
commit 790f20b499
8 changed files with 142 additions and 8 deletions

View File

@@ -13,7 +13,8 @@ Die fachliche Abnahme erfolgt zusätzlich auf der konkreten Instanz.
## Was der Pilot leistet und wo seine Grenze liegt ## Was der Pilot leistet und wo seine Grenze liegt
- Vorhandenes MinIO verwenden; kein Abruf von `minio/minio` oder `mc` erforderlich. - Vorhandenes MinIO verwenden; kein Abruf von `minio/minio` oder `mc` erforderlich.
- Feste SeaweedFS-Version 4.47; Offline-Images, SHA-256-geprüftes Paket. - SeaweedFS 4.47 mit dokumentierter FEDEO-Metadatenkorrektur; Offline-Images,
SHA-256-geprüftes Paket.
- Alle Objekte des einzigen FEDEO-Buckets übernehmen, inklusive Exporte und - Alle Objekte des einzigen FEDEO-Buckets übernehmen, inklusive Exporte und
Objekte ohne aktuellen Datenbankverweis. Schlüssel und Metadaten bleiben erhalten. Objekte ohne aktuellen Datenbankverweis. Schlüssel und Metadaten bleiben erhalten.
- Alle bekannten Schreiber anhalten, ihre automatischen Neustarts sperren, - Alle bekannten Schreiber anhalten, ihre automatischen Neustarts sperren,
@@ -40,6 +41,27 @@ auf SeaweedFS und der Standardinstaller sind nachfolgende Arbeitspakete.
Wer diese Einschränkungen noch nicht im Betrieb tragen möchte, führt zunächst Wer diese Einschränkungen noch nicht im Betrieb tragen möchte, führt zunächst
nur die Vorprüfung durch und bleibt mit der Instanz auf MinIO. nur die Vorprüfung durch und bleibt mit der Instanz auf MinIO.
### Warum das Pilotimage eine gezielte Korrektur enthält
Das unveränderte SeaweedFS 4.47 entfernt beim Speichern den expliziten MIME-Typ
`application/octet-stream`. Bei einem `.pdf`-Schlüssel liefern HEAD und GET
dadurch später `application/pdf`. Diese Abweichung wurde beim ersten betreuten
Versuch durch den strengen Metadatenvergleich erkannt; die Instanz wurde vor
Schreibfreigabe erfolgreich auf MinIO zurückgeführt.
`scripts/storage/Dockerfile.seaweedfs` baut deshalb die feste Herstellerrevision
`c5073360007d28385a33426a42ac3e4ec504c5a3` mit
`preserve-s3-content-type.patch` als `4.47-fedeo.1`. Die Korrektur bewahrt den
expliziten Typ für S3-Einträge; die Normalisierung gewöhnlicher Dateisystemeinträge
bleibt erhalten. Quellarchiv, Go-Buildimage und Hersteller-Basisimage sind per
SHA-256 festgelegt. Gezielte Go-Tests laufen beim Build; das Paket enthält den
Buildplan und Patch mit Prüfsummen. Der S3-Vertrag prüft den generischen MIME-Typ
zusätzlich mit HEAD, GET und dem Original-Backend-SDK.
Dies ist eine eigene Buildvariante. Bei einem späteren SeaweedFS-Update muss die
Korrektur weitergeführt oder durch einen nachgewiesenen Herstellerfix ersetzt
werden; das ungeänderte Herstellerimage ist für diese 1:1-Übernahme nicht freigegeben.
## Voraussetzungen, die der Plan prüft ## Voraussetzungen, die der Plan prüft
Linux amd64, Python ab 3.10, lokaler Docker-Daemon, Docker Compose und `tar`/`du`. Linux amd64, Python ab 3.10, lokaler Docker-Daemon, Docker Compose und `tar`/`du`.
@@ -211,7 +233,7 @@ nicht automatisch gelöscht.
## Nachgewiesene Tests und ausstehende Freigaben ## Nachgewiesene Tests und ausstehende Freigaben
Der lokale S3-Test verwendet das offizielle MinIO-Release-Binary vom 7. September Der lokale S3-Test verwendet das offizielle MinIO-Release-Binary vom 7. September
2025 und SeaweedFS 4.47 mit festem Digest. 1.005 Objekte / 1.073.752.750 Bytes, 2025 und SeaweedFS 4.47 mit der dokumentierten MIME-Korrektur. 1.005 Objekte / 1.073.752.750 Bytes,
einschließlich 1-GiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren einschließlich 1-GiB-Grenzdatei, Pagination, Sonderzeichen, Metadaten, leeren
Dateien, Streaming, 24-Stunden-Signaturen, fremdem Bucket und ungültigen Zugängen Dateien, Streaming, 24-Stunden-Signaturen, fremdem Bucket und ungültigen Zugängen
werden geprüft. Beschädigte Zielobjekte werden erkannt und beim Fortsetzen werden geprüft. Beschädigte Zielobjekte werden erkannt und beim Fortsetzen

View File

@@ -0,0 +1,18 @@
FROM golang:1.26-alpine@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c AS builder
RUN apk add --no-cache patch
WORKDIR /src
ADD --checksum=sha256:8b3823d4ca2df8250d7e6f35774228cc4ff01ffe2f8c10656df074a8db2f48a5 https://codeload.github.com/seaweedfs/seaweedfs/tar.gz/c50733600 /tmp/source.tar.gz
RUN tar -xzf /tmp/source.tar.gz --strip-components=1 && rm /tmp/source.tar.gz
COPY preserve-s3-content-type.patch /tmp/preserve-s3-content-type.patch
RUN patch -p1 < /tmp/preserve-s3-content-type.patch
ENV CGO_ENABLED=0
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
go test -p 4 ./weed/filer -run '^TestFilerStoreWrapperMimeNormalization$' -count=1 -v \
&& go build -p 4 -trimpath -ldflags '-s -w -X github.com/seaweedfs/seaweedfs/weed/util/version.COMMIT=c5073360007d28385a33426a42ac3e4ec504c5a3-fedeo.1' -o /out/weed ./weed
FROM chrislusf/seaweedfs:4.47@sha256:ce9e796f1fe6f06968f4c04bdaf8f678dad9c8acdfef3d244133d71bfa6bf882
COPY --from=builder /out/weed /usr/bin/weed
COPY --from=builder /src/LICENSE /usr/share/doc/fedeo-seaweedfs/LICENSE
COPY preserve-s3-content-type.patch /usr/share/doc/fedeo-seaweedfs/preserve-s3-content-type.patch
LABEL io.fedeo.storage.patch="preserve-s3-content-type-v1" \
org.opencontainers.image.revision="c5073360007d28385a33426a42ac3e4ec504c5a3-fedeo.1"

View File

@@ -10,6 +10,7 @@ Lokaler Test (Linux amd64, Docker, Node 22):
npm ci --prefix scripts/storage npm ci --prefix scripts/storage
npm test --prefix scripts/storage npm test --prefix scripts/storage
docker pull chrislusf/seaweedfs:4.47@sha256:ce9e796f1fe6f06968f4c04bdaf8f678dad9c8acdfef3d244133d71bfa6bf882 docker pull chrislusf/seaweedfs:4.47@sha256:ce9e796f1fe6f06968f4c04bdaf8f678dad9c8acdfef3d244133d71bfa6bf882
docker build -f scripts/storage/Dockerfile.seaweedfs -t fedeo-seaweedfs:4.47-fedeo.1 scripts/storage
docker build -f scripts/storage/tests/Dockerfile.minio -t fedeo-minio-fixture:2025-09-07 scripts/storage/tests docker build -f scripts/storage/tests/Dockerfile.minio -t fedeo-minio-fixture:2025-09-07 scripts/storage/tests
docker build -t fedeo-storage-tools:pilot scripts/storage docker build -t fedeo-storage-tools:pilot scripts/storage
node scripts/storage/tests/integration.mjs node scripts/storage/tests/integration.mjs
@@ -37,7 +38,16 @@ SHA-256-Checksum-Header. Die Pilot-Anwendung erhält
`AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED`; der Vertragstest prüft diese `AWS_REQUEST_CHECKSUM_CALCULATION=WHEN_REQUIRED`; der Vertragstest prüft diese
Einstellung mit leeren und nicht leeren Streams. SigV4 bleibt aktiv. Einstellung mit leeren und nicht leeren Streams. SigV4 bleibt aktiv.
SeaweedFS-Herkunft: [Release 4.47](https://github.com/seaweedfs/seaweedfs/releases/tag/4.47). Das Pilotimage `4.47-fedeo.1` korrigiert SeaweedFS' Normalisierung expliziter
S3-MIME-Typen. `application/octet-stream` muss auch für `.pdf` erhalten bleiben.
Der Dockerfile pinnt Herstellerquelle, Buildimage und Laufzeitbasis; der beigefügte
Patch umfasst die Korrektur und Go-Tests für alle drei Schreibpfade. Der S3-Test
prüft HEAD/GET, Kopie und Restore einschließlich dieses Falls. Das ungeänderte
Herstellerimage wird vom neuen Vertragstest gezielt abgewiesen. Bei Folgeupdates
die Korrektur beibehalten oder deren Übernahme durch den Hersteller nachweisen.
SeaweedFS-Herkunft: [Release 4.47](https://github.com/seaweedfs/seaweedfs/releases/tag/4.47),
[betroffene Normalisierung](https://github.com/seaweedfs/seaweedfs/blob/c5073360007d28385a33426a42ac3e4ec504c5a3/weed/filer/filerstore_wrapper.go).
Das isolierte MinIO-Testimage verwendet das offizielle, per SHA-256 verifizierte Das isolierte MinIO-Testimage verwendet das offizielle, per SHA-256 verifizierte
[Release-Binary](https://github.com/minio/minio/releases/tag/RELEASE.2025-09-07T16-13-09Z), [Release-Binary](https://github.com/minio/minio/releases/tag/RELEASE.2025-09-07T16-13-09Z),
da die früheren Containerreferenzen nicht mehr abrufbar sind. Es wird nicht im da die früheren Containerreferenzen nicht mehr abrufbar sind. Es wird nicht im

View File

@@ -5,7 +5,9 @@ storage_output="${1:-$storage_repo/.release/storage-pilot}"
test ! -e "$storage_output" test ! -e "$storage_output"
mkdir -p "$storage_output/scripts/storage" "$storage_output/docs" mkdir -p "$storage_output/scripts/storage" "$storage_output/docs"
storage_weed="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["image"])' "$storage_repo/scripts/storage/seaweedfs.json")" storage_weed="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["image"])' "$storage_repo/scripts/storage/seaweedfs.json")"
if ! docker image inspect "$storage_weed" >/dev/null 2>&1; then docker pull "$storage_weed"; fi if ! docker image inspect "$storage_weed" >/dev/null 2>&1; then
docker build -f "$storage_repo/scripts/storage/Dockerfile.seaweedfs" -t "$storage_weed" "$storage_repo/scripts/storage"
fi
docker build -t fedeo-storage-tools:pilot "$storage_repo/scripts/storage" docker build -t fedeo-storage-tools:pilot "$storage_repo/scripts/storage"
storage_tools_id="$(docker image inspect fedeo-storage-tools:pilot --format '{{.Id}}')" storage_tools_id="$(docker image inspect fedeo-storage-tools:pilot --format '{{.Id}}')"
storage_weed_id="$(docker image inspect "$storage_weed" --format '{{.Id}}')" storage_weed_id="$(docker image inspect "$storage_weed" --format '{{.Id}}')"
@@ -14,7 +16,7 @@ storage_weed_local="fedeo-seaweedfs:pilot-${storage_weed_id#sha256:}"
docker image tag "$storage_tools_id" "$storage_tools" docker image tag "$storage_tools_id" "$storage_tools"
docker image tag "$storage_weed_id" "$storage_weed_local" docker image tag "$storage_weed_id" "$storage_weed_local"
cp "$storage_repo/scripts/selfhost-storage-migrate.py" "$storage_repo/scripts/selfhost-update.py" "$storage_output/scripts/" cp "$storage_repo/scripts/selfhost-storage-migrate.py" "$storage_repo/scripts/selfhost-update.py" "$storage_output/scripts/"
cp "$storage_repo/scripts/storage/"{s3.mjs,cli.mjs,seaweedfs.json,start-seaweedfs.sh,package.json,package-lock.json} "$storage_output/scripts/storage/" cp "$storage_repo/scripts/storage/"{s3.mjs,cli.mjs,seaweedfs.json,start-seaweedfs.sh,package.json,package-lock.json,Dockerfile.seaweedfs,preserve-s3-content-type.patch} "$storage_output/scripts/storage/"
cp "$storage_repo/scripts/storage/pilot.py" "$storage_output/pilot.py" cp "$storage_repo/scripts/storage/pilot.py" "$storage_output/pilot.py"
cp "$storage_repo/docs/minio-umstellung-pilot.md" "$storage_output/docs/" cp "$storage_repo/docs/minio-umstellung-pilot.md" "$storage_output/docs/"
docker image save -o "$storage_output/images.tar" "$storage_tools" "$storage_weed_local" docker image save -o "$storage_output/images.tar" "$storage_tools" "$storage_weed_local"
@@ -31,8 +33,11 @@ for p in sorted(root.rglob('*')):
with p.open('rb') as f: with p.open('rb') as f:
for chunk in iter(lambda:f.read(1024*1024), b''): h.update(chunk) for chunk in iter(lambda:f.read(1024*1024), b''): h.update(chunk)
checksums[str(p.relative_to(root))]=h.hexdigest() checksums[str(p.relative_to(root))]=h.hexdigest()
upstream=next(line.split()[1] for line in (root/'scripts/storage/Dockerfile.seaweedfs').read_text().splitlines()
if line.startswith('FROM chrislusf/seaweedfs:'))
(root/'bundle.json').write_text(json.dumps({'schemaVersion':1,'toolsImage':sys.argv[2], (root/'bundle.json').write_text(json.dumps({'schemaVersion':1,'toolsImage':sys.argv[2],
'seaweedImage':sys.argv[3],'upstreamSeaweedImage':sys.argv[4],'sha256':checksums},indent=2)+'\n') 'seaweedImage':sys.argv[3],'upstreamSeaweedImage':upstream,'seaweedBuild':'4.47-fedeo.1',
'sha256':checksums},indent=2)+'\n')
PY PY
tar -czf "$storage_output.tar.gz" -C "$(dirname "$storage_output")" "$(basename "$storage_output")" tar -czf "$storage_output.tar.gz" -C "$(dirname "$storage_output")" "$(basename "$storage_output")"
(cd "$(dirname "$storage_output")" && sha256sum "$(basename "$storage_output").tar.gz" > "$(basename "$storage_output").tar.gz.sha256") (cd "$(dirname "$storage_output")" && sha256sum "$(basename "$storage_output").tar.gz" > "$(basename "$storage_output").tar.gz.sha256")

View File

@@ -0,0 +1,70 @@
--- a/weed/filer/filerstore_wrapper.go
+++ b/weed/filer/filerstore_wrapper.go
@@ -213,7 +213,8 @@
if entry == nil {
return
}
- if !entry.IsDirectory() && entry.Mime == "application/octet-stream" {
+ // An explicit S3 Content-Type is object metadata, not a filesystem default.
+ if !entry.IsDirectory() && !entry.isS3Entry() && entry.Mime == "application/octet-stream" {
entry.Mime = ""
}
}
--- a/weed/filer/filerstore_wrapper_test.go
+++ b/weed/filer/filerstore_wrapper_test.go
@@ -8,6 +8,7 @@
"time"
"github.com/seaweedfs/seaweedfs/weed/util"
+ "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -17,6 +18,7 @@
name string
mode os.FileMode
wantMime string
+ extended map[string][]byte
}{
{
name: "files strip octet-stream",
@@ -24,6 +26,18 @@
wantMime: "",
},
{
+ name: "S3 owner keeps explicit octet-stream",
+ mode: 0o660,
+ wantMime: "application/octet-stream",
+ extended: map[string][]byte{s3_constants.ExtAmzOwnerKey: []byte("test-owner")},
+ },
+ {
+ name: "S3 ETag keeps explicit octet-stream",
+ mode: 0o660,
+ wantMime: "application/octet-stream",
+ extended: map[string][]byte{s3_constants.ExtETagKey: []byte("test-etag")},
+ },
+ {
name: "directories keep octet-stream",
mode: os.ModeDir | 0o770,
wantMime: "application/octet-stream",
@@ -38,6 +52,12 @@
name: "insert",
run: func(fsw *FilerStoreWrapper, ctx context.Context, entry *Entry) error {
return fsw.InsertEntry(ctx, entry)
+ },
+ },
+ {
+ name: "insert known absent",
+ run: func(fsw *FilerStoreWrapper, ctx context.Context, entry *Entry) error {
+ return fsw.InsertEntryKnownAbsent(ctx, entry)
},
},
{
@@ -55,6 +75,7 @@
wrapper := NewFilerStoreWrapper(store)
entry := &Entry{
FullPath: util.FullPath("/buckets/test/object"),
+ Extended: tt.extended,
Attr: Attr{
Mode: tt.mode,
Mime: "application/octet-stream",

View File

@@ -250,6 +250,14 @@ export async function contract(s3, config, Bucket, { foreignBucket, probePath }
requireThat(head.ContentLength === body.length && head.Metadata.test === 'fedeo', 'HEAD-Vertrag fehlgeschlagen.'); requireThat(head.ContentLength === body.length && head.Metadata.test === 'fedeo', 'HEAD-Vertrag fehlgeschlagen.');
const got = await call(s3, 'GetObject', { Bucket, Key }); const got = await call(s3, 'GetObject', { Bucket, Key });
requireThat(Buffer.from(await got.Body.transformToByteArray()).equals(body), 'GET-Vertrag fehlgeschlagen.'); requireThat(Buffer.from(await got.Body.transformToByteArray()).equals(body), 'GET-Vertrag fehlgeschlagen.');
// Generic MIME types are explicit S3 metadata even for a .pdf key. The
// upstream filer used to drop octet-stream and infer PDF on later reads.
await call(s3, 'PutObject', { Bucket, Key, Body: body, ContentType: 'application/octet-stream', Metadata: { test: 'fedeo' } });
const genericHead = await call(s3, 'HeadObject', { Bucket, Key });
const genericGet = await call(s3, 'GetObject', { Bucket, Key });
const genericBody = Buffer.from(await genericGet.Body.transformToByteArray());
requireThat(genericHead.ContentType === 'application/octet-stream' && genericGet.ContentType === 'application/octet-stream'
&& genericHead.Metadata.test === 'fedeo' && genericBody.equals(body), 'Expliziter S3-MIME-Typ wurde verändert.');
// Application exports use streams. Exercise exactly the SDK environment setting // Application exports use streams. Exercise exactly the SDK environment setting
// applied by the pilot Compose file, including empty streams. // applied by the pilot Compose file, including empty streams.
for (const bytes of [Buffer.alloc(0), Buffer.alloc(128 * 1024, 42)]) { for (const bytes of [Buffer.alloc(0), Buffer.alloc(128 * 1024, 42)]) {

View File

@@ -1,5 +1,5 @@
{ {
"image": "chrislusf/seaweedfs:4.47@sha256:ce9e796f1fe6f06968f4c04bdaf8f678dad9c8acdfef3d244133d71bfa6bf882", "image": "fedeo-seaweedfs:4.47-fedeo.1",
"entrypoint": ["/bin/sh", "/etc/fedeo/start-seaweedfs.sh"], "entrypoint": ["/bin/sh", "/etc/fedeo/start-seaweedfs.sh"],
"command": [ "command": [
"server", "-dir=/data", "-master.dir=/data/master", "-ip=127.0.0.1", "-ip.bind=127.0.0.1", "server", "-dir=/data", "-master.dir=/data/master", "-ip=127.0.0.1", "-ip.bind=127.0.0.1",

View File

@@ -10,6 +10,7 @@ import { client, call, contract, inventory, transfer, verifyManifest, MAX_OBJECT
const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8', timeout: 120_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim(); const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8', timeout: 120_000, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
const spec = JSON.parse(await readFile(new URL('../seaweedfs.json', import.meta.url), 'utf8')); const spec = JSON.parse(await readFile(new URL('../seaweedfs.json', import.meta.url), 'utf8'));
if (process.env.FEDEO_SEAWEED_TEST_IMAGE) spec.image = process.env.FEDEO_SEAWEED_TEST_IMAGE;
const root = await mkdtemp(join(tmpdir(), 'fedeo-storage-contract-')); const root = await mkdtemp(join(tmpdir(), 'fedeo-storage-contract-'));
const prefix = `fedeo-contract-${randomUUID().slice(0, 8)}`; const prefix = `fedeo-contract-${randomUUID().slice(0, 8)}`;
const containers = []; const containers = [];
@@ -84,7 +85,7 @@ try {
`const s=require('node:net').connect(${port},${JSON.stringify(ip)});s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1));s.setTimeout(1500,()=>process.exit(1))`)); `const s=require('node:net').connect(${port},${JSON.stringify(ip)});s.on('connect',()=>process.exit(0));s.on('error',()=>process.exit(1));s.setTimeout(1500,()=>process.exit(1))`));
} }
for (let i = 0; i < 1003; i++) await call(source, 'PutObject', { Bucket, Key: `tenant/filesbyid/${i}/ä + % #.txt`, for (let i = 0; i < 1003; i++) await call(source, 'PutObject', { Bucket, Key: `tenant/filesbyid/${i}/ä + % #.txt`,
Body: Buffer.from(`fixture-${i}`), ContentType: 'text/plain', Metadata: { custom: 'preserved' } }); Body: Buffer.from(`fixture-${i}`), ContentType: i === 0 ? 'application/octet-stream' : '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: 'empty', Body: Buffer.alloc(0), ContentType: 'application/octet-stream' });
await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES, await call(source, 'PutObject', { Bucket, Key: 'large.zip', ContentLength: MAX_OBJECT_BYTES,
Body: Readable.from((function* () { for (let i = 0; i < MAX_OBJECT_BYTES / (1024 * 1024); i++) yield Buffer.alloc(1024 * 1024, i % 256); })()), Body: Readable.from((function* () { for (let i = 0; i < MAX_OBJECT_BYTES / (1024 * 1024); i++) yield Buffer.alloc(1024 * 1024, i % 256); })()),