קבצים סטטיים מאקספרס על Deno Deploy

אם תהיתם מאיפה הגיעו כל הפוסטים על node ו deno בימים האחרונים אז תשמחו לשמוע שאני עובד עכשיו על רענון קורס Node שבאתר ומקווה שעד פסח אתם תקבלו קורס מעודכן על Node שכבר כולל אינסוף דוגמאות קוד ב TypeScript ותואם גם ל Node וגם ל Deno. רוב הזמן זה לא נורא מסובך אבל בלי קצת בעיות תאימות החיים לא היו מעניינים. בפוסט היום אני רוצה לדבר על בעיית תאימות קטנה כזאת בשירות Deno Deploy.

למי שלא מכירים Deno Deploy זה שירות איחסון חינמי של Deno אליו אפשר להעלות כל יישום צד שרת והם פשוט מריצים אותו על השרתים שלהם. יש להם מסלולים בתשלום לאנשים שצריכים המון משאבים, אבל למשחקים המסלול החינמי ממש מעולה.

בהינתן תיקיית פרויקט Deno אפשר לכתוב משורת הפקודה:

$ deployctl deploy

והפרויקט עולה לאוויר.

וכמובן אם הפרויקט מאוחסן על גיטהאב אפשר דרך התפריטים במערכת לחבר את דינו דיפלוי לגיטהאב שלכם ואז כל Push מייצר גירסה חדשה של הפרויקט על השרת.

האתגר היחיד הוא בעיית תאימות, כי לדינו יש Web Frameworks שלהם ואקספרס כנראה לא מקבל שם עדיפות. פיצ'ר קטן אבל חשוב של קבצים סטטיים דרש קצת יותר עבודה ממה שהיה צריך. בואו נראה למה.

את קוד הפרויקט אתם יכולים למצוא בגיטהאב שלי בקישור: https://github.com/ynonp/deno-deploy-files

כאן אציג רק את עיקרי הדברים.

1. מצב פיתוח על המכונה שלי

במצב פיתוח על המכונה שלי הגשת קבצים סטטיים עובדת בצורה מובנית באקספרס. זה הקוד:

import express, {Request, Response, NextFunction} from 'express';
import process from 'node:process';
import root from './routes/root.ts';
import path from 'node:path';
import url from 'node:url';
import createError from 'http-errors';

const PORT = process.env.PORT || 3000;
const app = express();

const dirname = path.dirname(url.fileURLToPath(import.meta.url));
app.set('view engine', 'ejs');
app.set('views', path.join(dirname, '/views'));

app.use('/', root);
app.use(express.static('./public'));

app.use((req: Request, res: Response, next: NextFunction) => {
  next(createError(404, 'Not Found'));
})

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  res.status(500).send({ errors: [{ message: "Something went wrong" }] });
});

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

כאשר השורה הרלוונטית היא:

app.use(express.static('./public'));

2. מה קורה כשדוחפים ל Deno Deploy

אחרי העלאה ל Deno Deploy התוכנית עובדת אבל הקובץ הסטטי (בדוגמה שלי זה קובץ CSS) לא נטען. בדיקה בלוג מראה את השגיאה הבאה:

TypeError: Cannot read properties of null (reading 'toUTCString')
    at SendStream.setHeader (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:874:31)
    at SendStream.send (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:620:8)
    at onstat (file:///node_modules/.deno/send@0.18.0/node_modules/send/index.js:725:10)
    at Deno.stat.then.denoErrorToNodeError.syscall (ext:deno_node/_fs/_fs_stat.ts:85:32)
    at eventLoopTick (ext:core/01_core.js:153:7)

פה כבר התחלתי לפחד. זה נראה ש stat של דינו לא עובד בדיוק כמו stat של node.js ולכן משהו נשבר בשידור הקובץ.

במקום לריב איתם הלכתי לכתוב מידלוור משלי שישלח את הקובץ בלי stat. הוא נראה ככה:

app.use(async (req: Request, res: Response, next: NextFunction) => {
  try {
    const userInput = path.join('./public', req.path);
    const safeInput = path.normalize(userInput).replace(/^(\.\.(\/|\\|$))+/, '');
    const mime = contentType(path.extname(safeInput))
    console.log(`File: ${safeInput}. content type = ${mime}`);
    const content = await Deno.readFile(safeInput);
    res.set('Content-Type', mime);
    res.send(buffer.Buffer.from(content));  
  } catch (err) {
    console.log(`file not found ${req.path}`);
    next();
  }
});

ואת התוצאה אפשר לראות אונליין על Deno Deploy עם ה CSS בקישור:

https://ynonp-deno-deploy-files-yfge7qm6f97y.deno.dev/