diff --git a/.changeset/dam-download-content-disposition-filename.md b/.changeset/dam-download-content-disposition-filename.md new file mode 100644 index 00000000000..3438c65c11b --- /dev/null +++ b/.changeset/dam-download-content-disposition-filename.md @@ -0,0 +1,7 @@ +--- +"@dextinity/cms-api": patch +--- + +Add the file name to the `Content-Disposition` header of DAM file downloads + +The header was previously set to `attachment` without a file name, so browsers derived the name of the downloaded file from the URL, which contains the file name without its extension. diff --git a/packages/api/cms-api/package.json b/packages/api/cms-api/package.json index 85393acedc7..224052be3fa 100644 --- a/packages/api/cms-api/package.json +++ b/packages/api/cms-api/package.json @@ -54,6 +54,7 @@ "@tiptap/pm": "^3.22.3", "@tiptap/starter-kit": "^3.22.3", "base64url": "^3.0.1", + "content-disposition": "^3.0.0", "cron-parser": "^3.5.0", "dataloader": "^2.2.3", "date-fns": "^4.4.0", diff --git a/packages/api/cms-api/src/dam/files/files.controller.ts b/packages/api/cms-api/src/dam/files/files.controller.ts index d993e0dbeb5..3f10e78ce1b 100644 --- a/packages/api/cms-api/src/dam/files/files.controller.ts +++ b/packages/api/cms-api/src/dam/files/files.controller.ts @@ -18,6 +18,7 @@ import { } from "@nestjs/common"; import { plainToInstance } from "class-transformer"; import { validate } from "class-validator"; +import { create as createContentDisposition } from "content-disposition"; import { Response } from "express"; import { OutgoingHttpHeaders } from "http"; import { basename, extname } from "path"; @@ -239,7 +240,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new ForbiddenException(); } - res.setHeader("Content-Disposition", "attachment"); + res.setHeader("Content-Disposition", createContentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, private" } }); // Local caches only (1 year) } @@ -264,7 +265,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco throw new BadRequestException("Content Hash mismatch!"); } - res.setHeader("Content-Disposition", "attachment"); + res.setHeader("Content-Disposition", createContentDisposition(file.name)); return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, s-maxage=86400, public" } }); // Public cache, 1 year for browsers, 1 day for proxies/cdn's } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb509d4cab7..afc2726d7f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2077,6 +2077,9 @@ importers: base64url: specifier: ^3.0.1 version: 3.0.1 + content-disposition: + specifier: ^3.0.0 + version: 3.0.0 cron-parser: specifier: ^3.5.0 version: 3.5.0 @@ -10216,6 +10219,10 @@ packages: resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} engines: {node: '>=18'} + content-disposition@3.0.0: + resolution: {integrity: sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==} + engines: {node: '>=22'} + content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -27098,6 +27105,8 @@ snapshots: content-disposition@1.0.1: {} + content-disposition@3.0.0: {} + content-type@1.0.5: {} convert-source-map@1.9.0: {}