Skip to content

Commit 3d6f638

Browse files
committed
Default x-amz-tagging to dv-state=temp when the server omits the field
1 parent 21550f8 commit 3d6f638

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
1616
### Changed
1717

1818
- **BREAKING**: `DirectUploadClient` constructor signature changed from `(filesRepository, maxMultipartRetries = 5)` to `(filesRepository, config: DirectUploadClientConfig = {})`. `config` now holds `maxMultipartRetries` (default 5) and the new `fileUploadTimeoutMs` (default 60000). Existing TypeScript consumers passing the second argument as a number will need to migrate to `{ maxMultipartRetries: N }`.
19-
- Files: `DirectUploadClient` now reads the `tagging` field from the upload-destination response instead of hard-coding `x-amz-tagging: dv-state=temp`. This lets the server gate tagging per-driver for storage backends that don't support it (e.g. IBM Cloud Object Storage). When the server omits the field (older Dataverse releases that predate it), the client sends no `x-amz-tagging` header — lifecycle policies that previously relied on the client setting `dv-state=temp` need to move to bucket-level rules or wait for the matching server release.
19+
- Files: `DirectUploadClient` now reads the `tagging` field from the upload-destination response so operators on storage that doesn't accept S3 tags can opt out per-driver via `dataverse.files.<id>.disable-tagging=true`. The default behaviour is unchanged: when the server omits the field the client still sends `x-amz-tagging: dv-state=temp` (the same tag that earlier SDK versions hard-coded), so the new SDK is backwards-compatible with Dataverse releases that predate the response field. A server that explicitly returns an empty `tagging` value tells the client to skip the header entirely.
2020

2121
### Fixed
2222

src/files/infra/clients/DirectUploadClient.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,19 @@ export class DirectUploadClient implements IDirectUploadClient {
6969
const headers: Record<string, string> = {
7070
'Content-Type': 'application/octet-stream'
7171
}
72-
if (destination.tagging !== undefined) {
73-
headers['x-amz-tagging'] = destination.tagging
72+
// Default to `dv-state=temp` when the upload destination response
73+
// omits the field. That tag is what every Dataverse install emits
74+
// today and what every install before this change had hard-coded;
75+
// making "omitted" mean "no tag" would silently break uploads
76+
// against any S3 bucket whose lifecycle/access policy expects the
77+
// `dv-state=temp` marker, including the default IQSS configuration.
78+
// Operators with storage that doesn't accept S3 tags opt out
79+
// explicitly via `dataverse.files.<id>.disable-tagging=true`,
80+
// which causes the server to return an empty `tagging` field and
81+
// the client below to skip the header.
82+
const tag = destination.tagging ?? 'dv-state=temp'
83+
if (tag !== '') {
84+
headers['x-amz-tagging'] = tag
7485
}
7586
await axios.put(destination.urls[0], arrayBuffer, {
7687
headers,

test/unit/files/DirectUploadClient.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ describe('uploadFile', () => {
116116
)
117117
})
118118

119-
test('should not include S3 tagging header when upload destination omits tagging', async () => {
119+
test('should default to dv-state=temp tagging when upload destination omits tagging', async () => {
120120
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
121121
const testDestination: FileUploadDestination = createSingleFileUploadDestinationModel()
122122
filesRepositoryStub.getFileUploadDestination = jest.fn().mockResolvedValue(testDestination)
@@ -130,6 +130,34 @@ describe('uploadFile', () => {
130130

131131
await sut.uploadFile(1, testFile, progressMock, abortController)
132132

133+
expect(axiosPutSpy).toHaveBeenCalledWith(
134+
testDestination.urls[0],
135+
expect.anything(),
136+
expect.objectContaining({
137+
headers: expect.objectContaining({
138+
'x-amz-tagging': 'dv-state=temp'
139+
})
140+
})
141+
)
142+
})
143+
144+
test('should omit the S3 tagging header when upload destination explicitly returns empty tagging', async () => {
145+
const filesRepositoryStub: IFilesRepository = {} as IFilesRepository
146+
const testDestination: FileUploadDestination = {
147+
...createSingleFileUploadDestinationModel(),
148+
tagging: ''
149+
}
150+
filesRepositoryStub.getFileUploadDestination = jest.fn().mockResolvedValue(testDestination)
151+
152+
const axiosPutSpy = jest.spyOn(axios, 'put').mockResolvedValue(undefined)
153+
154+
const sut = new DirectUploadClient(filesRepositoryStub)
155+
156+
const progressMock = jest.fn()
157+
const abortController = new AbortController()
158+
159+
await sut.uploadFile(1, testFile, progressMock, abortController)
160+
133161
expect(axiosPutSpy).toHaveBeenCalledWith(
134162
testDestination.urls[0],
135163
expect.anything(),

0 commit comments

Comments
 (0)