Force download for lesson materials (Content-Disposition: attachment)
Lesson files were served inline (no Content-Disposition) → browsers opened PDFs in a new tab instead of downloading; flaky across browsers/providers. Set Content-Disposition: attachment with the human-readable filename (RFC 5987 for Cyrillic) on upload. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { uploadFile, deleteFile } from "@/lib/s3";
|
||||
import { uploadFile, deleteFile, attachmentDisposition } from "@/lib/s3";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
async function requireAdmin() {
|
||||
@@ -31,7 +31,10 @@ export async function POST(req: NextRequest) {
|
||||
const ext = file.name.split(".").pop() ?? "bin";
|
||||
const key = `lessons/${lessonId}/${randomUUID()}.${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const url = await uploadFile(key, buffer, file.type);
|
||||
// Материалы урока отдаём как attachment (надёжное скачивание во всех браузерах)
|
||||
// с человеческим именем из name (а не UUID).
|
||||
const downloadName = name.toLowerCase().endsWith(`.${ext.toLowerCase()}`) ? name : `${name}.${ext}`;
|
||||
const url = await uploadFile(key, buffer, file.type, attachmentDisposition(downloadName));
|
||||
|
||||
if (existing) {
|
||||
const oldKey = existing.url.split(`/${process.env.S3_BUCKET}/`)[1];
|
||||
|
||||
+12
-1
@@ -17,10 +17,20 @@ export function getPublicUrl(key: string): string {
|
||||
return `${process.env.S3_ENDPOINT}/${BUCKET}/${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Заголовок Content-Disposition для принудительного скачивания с человеческим
|
||||
* именем. RFC 5987: ASCII-фолбэк + UTF-8 (кириллица percent-encoded).
|
||||
*/
|
||||
export function attachmentDisposition(filename: string): string {
|
||||
const ascii = filename.replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "'");
|
||||
return `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(filename)}`;
|
||||
}
|
||||
|
||||
export async function uploadFile(
|
||||
key: string,
|
||||
body: Buffer | Uint8Array,
|
||||
contentType: string
|
||||
contentType: string,
|
||||
contentDisposition?: string
|
||||
): Promise<string> {
|
||||
await s3.send(
|
||||
new PutObjectCommand({
|
||||
@@ -28,6 +38,7 @@ export async function uploadFile(
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
...(contentDisposition ? { ContentDisposition: contentDisposition } : {}),
|
||||
})
|
||||
);
|
||||
return getPublicUrl(key);
|
||||
|
||||
Reference in New Issue
Block a user