Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dam-file-url-missing-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dextinity/cms-api": patch
---

Fix DAM file URLs (both inline "open in new tab" and download links) missing the file extension, which caused browsers to save downloaded files without their extension (e.g. `.pdf`). Downloads now also include the original filename via the `Content-Disposition` header.
11 changes: 9 additions & 2 deletions packages/api/cms-api/src/dam/files/files.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco
throw new ForbiddenException();
}

res.setHeader("Content-Disposition", "attachment");
res.setHeader("Content-Disposition", `attachment; filename="${this.contentDispositionFilename(file.name)}"`);
return this.streamFile(file, res, { range, overrideHeaders: { "cache-control": "max-age=31536000, private" } }); // Local caches only (1 year)
}

Expand All @@ -264,7 +264,7 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco
throw new BadRequestException("Content Hash mismatch!");
}

res.setHeader("Content-Disposition", "attachment");
res.setHeader("Content-Disposition", `attachment; filename="${this.contentDispositionFilename(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
}

Expand Down Expand Up @@ -301,6 +301,13 @@ export function createFilesController({ Scope: PassedScope, damBasePath }: { Sco
return hash === this.filesService.createHash(fileParams);
}

// Strips quotes/backslash plus all ASCII control characters (e.g. NUL), which would otherwise make
// `res.setHeader` throw `ERR_INVALID_CHAR` below and fail the download.
private contentDispositionFilename(filename: string): string {
// eslint-disable-next-line no-control-regex
return filename.replace(/["\\\x00-\x1F\x7F]/g, "");
}

private async streamFile(
file: FileInterface,
res: Response,
Expand Down
6 changes: 3 additions & 3 deletions packages/api/cms-api/src/dam/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createHmac } from "crypto";
import exifr from "exifr";
import { createReadStream } from "fs";
import * as hasha from "hasha";
import { basename, extname, parse } from "path";
import { basename, extname } from "path";
import probe from "probe-image-size";
import * as rimraf from "rimraf";

Expand Down Expand Up @@ -611,7 +611,7 @@ export class FilesService {
}

async createFileUrl(file: FileInterface, { previewDamUrls = false }: { previewDamUrls?: boolean }): Promise<string> {
const filename = parse(file.name).name;
const filename = file.name;

const baseUrl = [`/${this.config.basePath}/files`];

Expand Down Expand Up @@ -642,7 +642,7 @@ export class FilesService {
}

async createFileDownloadUrl(file: FileInterface, { previewDamUrls = false }: { previewDamUrls?: boolean }): Promise<string> {
const filename = parse(file.name).name;
const filename = file.name;

const baseUrl = [`/dam/files/download`];

Expand Down
Loading