No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

140 líneas
5.3 KiB

  1. import { NextResponse } from 'next/server';
  2. import { writeFile, mkdir, unlink } from 'fs/promises';
  3. import path from 'path';
  4. import { fromBuffer as fileTypeFromBuffer } from 'file-type';
  5. import { UPLOAD_LIMITS } from '@/lib/config';
  6. export const dynamic = 'force-dynamic';
  7. export const maxDuration = 60;
  8. const FONTS_DIR = path.join(process.cwd(), 'data', 'fonts');
  9. const ALLOWED_EXT = new Set(['.woff2', '.woff', '.ttf', '.otf']);
  10. // Magic-bytes: ttf e otf condividono il container SFNT, quindi accettiamo entrambi
  11. // per entrambe le estensioni. woff/woff2 hanno header dedicati.
  12. const ALLOWED_DETECTED: Record<string, string[]> = {
  13. '.woff2': ['woff2'],
  14. '.woff': ['woff'],
  15. '.ttf': ['ttf', 'otf'],
  16. '.otf': ['otf', 'ttf'],
  17. };
  18. // Sanitizza il nome del file: solo basename, solo caratteri sicuri, niente path traversal.
  19. // Per i font conviene preservare il nome originale (la logica di lookup italic/bold in
  20. // app/layout.tsx si basa sui pattern `Name-Italic.woff2`, `Name-Bold.woff2`, ecc.).
  21. function sanitizeFontName(rawName: string): string {
  22. const base = path.basename(rawName);
  23. // Mantieni lettere, cifre, underscore, hyphen, punto. Tutto il resto → underscore.
  24. const sanitized = base.replace(/[^a-zA-Z0-9_\-.]/g, '_');
  25. // Niente percorsi nascosti / nomi vuoti
  26. if (sanitized.startsWith('.') || sanitized.length === 0) return '';
  27. return sanitized;
  28. }
  29. // POST — upload font
  30. export async function POST(request: Request) {
  31. try {
  32. const formData = await request.formData();
  33. const file = formData.get('file') as File | null;
  34. if (!file) {
  35. return NextResponse.json(
  36. { error: 'No font file was received. Choose a font file before clicking Upload.' },
  37. { status: 400 },
  38. );
  39. }
  40. const safeName = sanitizeFontName(file.name);
  41. const ext = path.extname(safeName).toLowerCase();
  42. if (!ALLOWED_EXT.has(ext)) {
  43. return NextResponse.json(
  44. { error: `Font extension "${ext || '(none)'}" is not supported. Allowed formats: ${[...ALLOWED_EXT].join(', ')}.` },
  45. { status: 400 },
  46. );
  47. }
  48. if (!safeName || safeName === ext) {
  49. return NextResponse.json(
  50. { error: 'Invalid font filename. The file name must contain at least one letter, digit, hyphen or underscore before the extension.' },
  51. { status: 400 },
  52. );
  53. }
  54. if (file.size > UPLOAD_LIMITS.font) {
  55. const limitMB = (UPLOAD_LIMITS.font / (1024 * 1024)).toFixed(0);
  56. const actualMB = (file.size / (1024 * 1024)).toFixed(1);
  57. return NextResponse.json(
  58. { error: `Font is too large: ${actualMB} MB (limit: ${limitMB} MB). Web fonts are typically under 500 KB — convert the file to WOFF2 (e.g. with a font-tools subsetter) before uploading.` },
  59. { status: 413 },
  60. );
  61. }
  62. const buffer = Buffer.from(await file.arrayBuffer());
  63. // Magic-bytes: rifiuta se il contenuto non è davvero un font del tipo dichiarato.
  64. const detected = await fileTypeFromBuffer(buffer);
  65. if (!detected) {
  66. return NextResponse.json(
  67. { error: 'Could not identify the font content. The file may be corrupted or not actually a font. Try re-exporting from your font tool.' },
  68. { status: 400 },
  69. );
  70. }
  71. const allowed = ALLOWED_DETECTED[ext] ?? [];
  72. if (!allowed.includes(detected.ext)) {
  73. return NextResponse.json(
  74. { error: `Font content does not match its extension: the file looks like ${detected.ext.toUpperCase()} but the extension is "${ext}". Rename the file or re-export it in the declared format.` },
  75. { status: 400 },
  76. );
  77. }
  78. await mkdir(FONTS_DIR, { recursive: true });
  79. await writeFile(path.join(FONTS_DIR, safeName), buffer);
  80. return NextResponse.json({ ok: true, name: safeName }, { status: 201 });
  81. } catch (error) {
  82. console.error('Font upload error:', error);
  83. return NextResponse.json(
  84. { error: 'Unexpected error while saving the font. The upload was aborted. Check the server logs for details and try again.' },
  85. { status: 500 },
  86. );
  87. }
  88. }
  89. // DELETE — rimuove un font (query ?name=<filename>)
  90. export async function DELETE(request: Request) {
  91. try {
  92. const { searchParams } = new URL(request.url);
  93. const rawName = searchParams.get('name');
  94. if (!rawName) {
  95. return NextResponse.json(
  96. { error: 'Missing "name" parameter. Specify which font file to delete.' },
  97. { status: 400 },
  98. );
  99. }
  100. const safeName = sanitizeFontName(rawName);
  101. const ext = path.extname(safeName).toLowerCase();
  102. if (!ALLOWED_EXT.has(ext) || !safeName) {
  103. return NextResponse.json(
  104. { error: `Invalid font name "${rawName}". Expected a basename ending in ${[...ALLOWED_EXT].join(' / ')}.` },
  105. { status: 400 },
  106. );
  107. }
  108. try {
  109. await unlink(path.join(FONTS_DIR, safeName));
  110. } catch (err) {
  111. const e = err as NodeJS.ErrnoException;
  112. if (e.code === 'ENOENT') {
  113. return NextResponse.json(
  114. { error: `Font "${safeName}" was not found on the server. It may have already been deleted — refresh the page.` },
  115. { status: 404 },
  116. );
  117. }
  118. throw err;
  119. }
  120. return NextResponse.json({ ok: true });
  121. } catch (error) {
  122. console.error('Font delete error:', error);
  123. return NextResponse.json(
  124. { error: 'Unexpected error while deleting the font. Check the server logs and try again.' },
  125. { status: 500 },
  126. );
  127. }
  128. }