diff --git a/src/app/api/admin/lesson-files/route.ts b/src/app/api/admin/lesson-files/route.ts index 5af5b75..56603f6 100644 --- a/src/app/api/admin/lesson-files/route.ts +++ b/src/app/api/admin/lesson-files/route.ts @@ -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]; diff --git a/src/lib/s3.ts b/src/lib/s3.ts index b9131b7..238de47 100644 --- a/src/lib/s3.ts +++ b/src/lib/s3.ts @@ -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 { 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);