From 3b5d6021f2eec38ce2b72639ee069e332410ae7b Mon Sep 17 00:00:00 2001 From: Ferhat Elmas Date: Sun, 18 Oct 2026 14:37:17 +0200 Subject: [PATCH] @tus/s3-store: forward content encoding Signed-off-by: Ferhat Elmas --- .changeset/forward-content-encoding.md | 5 +++++ packages/s3-store/README.md | 14 +++++++++++++ packages/s3-store/src/index.ts | 4 ++++ packages/s3-store/src/test/index.ts | 27 ++++++++++++++++++++++++++ 4 files changed, 50 insertions(+) create mode 100644 .changeset/forward-content-encoding.md diff --git a/.changeset/forward-content-encoding.md b/.changeset/forward-content-encoding.md new file mode 100644 index 00000000..7f0866a5 --- /dev/null +++ b/.changeset/forward-content-encoding.md @@ -0,0 +1,5 @@ +--- +"@tus/s3-store": minor +--- + +Forward the `contentEncoding` upload metadata to S3 as `Content-Encoding` when creating the multipart upload, matching `contentType` and `cacheControl`. diff --git a/packages/s3-store/README.md b/packages/s3-store/README.md index fbb89479..d57b6130 100644 --- a/packages/s3-store/README.md +++ b/packages/s3-store/README.md @@ -10,6 +10,7 @@ - [Use](#use) - [API](#api) - [`new S3Store(options)`](#new-s3storeoptions) +- [Metadata](#metadata) - [Extensions](#extensions) - [Examples](#examples) - [Example: using `credentials` to fetch credentials inside a AWS container](#example-using-credentials-to-fetch-credentials-inside-a-aws-container) @@ -138,6 +139,19 @@ overall upload bandwidth available for other concurrent uploads. throughput. Instead, it will result in additional resource consumption without proportional gains in transfer speed. +## Metadata + +These upload metadata keys are forwarded to S3 as headers on the stored object: + +| Metadata key | S3 header | +| ----------------- | ------------------ | +| `contentType` | `Content-Type` | +| `cacheControl` | `Cache-Control` | +| `contentEncoding` | `Content-Encoding` | + +`contentEncoding` only labels the object; S3 does not compress it. Set it (e.g. `gzip`) +only when the client uploads bytes that are already encoded that way. + ## Extensions The tus protocol supports optional [extensions][]. Below is a table of the supported diff --git a/packages/s3-store/src/index.ts b/packages/s3-store/src/index.ts index 87e08ea8..3ee8d8b5 100644 --- a/packages/s3-store/src/index.ts +++ b/packages/s3-store/src/index.ts @@ -594,6 +594,10 @@ export class S3Store extends DataStore { request.CacheControl = upload.metadata.cacheControl } + if (upload.metadata?.contentEncoding) { + request.ContentEncoding = upload.metadata.contentEncoding + } + upload.creation_date = new Date().toISOString() const res = await this.client.createMultipartUpload(request) diff --git a/packages/s3-store/src/test/index.ts b/packages/s3-store/src/test/index.ts index a0328c37..766c6189 100644 --- a/packages/s3-store/src/test/index.ts +++ b/packages/s3-store/src/test/index.ts @@ -7,6 +7,7 @@ import type {AddressInfo} from 'node:net' import {Readable} from 'node:stream' import stream from 'node:stream/promises' import {setTimeout as sleep} from 'node:timers/promises' +import {gzipSync} from 'node:zlib' import {NoSuchKey, NoSuchUpload, NotFound} from '@aws-sdk/client-s3' import sinon from 'sinon' @@ -330,6 +331,32 @@ describe('S3DataStore', () => { assert.equal(await store.getIncompletePartSize(upload.id), undefined) }) + it('should preserve content headers metadata on the completed object', async function () { + const store = this.datastore as S3Store + const body = gzipSync('x') + const upload = new Upload({ + id: shared.testId('content-headers'), + size: body.length, + offset: 0, + metadata: { + contentType: 'text/plain', + cacheControl: 'max-age=3600', + contentEncoding: 'gzip', + }, + }) + + await store.create(upload) + const offset = await store.write(Readable.from(body), upload.id, 0) + assert.equal(offset, body.length) + + // @ts-expect-error protected property + const client = store.client + const head = await client.headObject({Bucket: s3ClientConfig.bucket, Key: upload.id}) + assert.equal(head.ContentType, 'text/plain') + assert.equal(head.CacheControl, 'max-age=3600') + assert.equal(head.ContentEncoding, 'gzip') + }) + for (const [S3Error, completed] of [ [NoSuchUpload, true], [NoSuchKey, true],