דוגמת דינו: שמירת תמונות מויקיפדיה

27/10/2024

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

1. מה בונים

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

אפשר לראות את הקוד המלא בגיטהאב: https://github.com/ynonp/wikipedia-image-fetcher

2. הקובץ main.ts

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

app.get('/up', (c) => {
  return c.json({ok: true})
})

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

הנתיב הבא הוא היותר מעניין. הוא מקבל שפה ונושא, בודק אם יש לי כבר תמונות על הנושא הזה. אם אין תמונות הוא יוריד אותן ואם יש הוא רק יחזיר את רשימת התמונות:

app.get('/images', async (c) => {
  const { topic, lang } = c.req.query();
  if (await lib.hasSavedImages(lang, topic)) {
    const images = await lib.getSavedImages(lang, topic);
    return c.json(images);
  } else {
    const images = await lib.downloadWikipediaImages(lang, topic);
    return c.json(images);
  }
})

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

הונו עובד ב Deno אבל גם ב node.js ו bun. יש לו ביצועים מצוינים ובינתיים אין לי שום תלונות לגביו. אנחנו מפעילים את השרת עם:

Deno.serve(app.fetch)

3. שמירת תמונות ושינוי גודל

הקובץ השני בפרויקט נקרא wikipedia_fetcher.js ושם כתבתי את כל הלוגיקה, ושם גם דברים התחילו להסתבך.

בעיה ראשונה שהיתה לי עם דינו היא שדינו יודע למשוך ספריות מ-3 מאגרים: יש לו את המאגר הרשמי שנקרא JSR, שם אין כמעט חבילות אבל נראה שה Toolchain של דינו עובד איתו הכי טוב. יש מאגר רשמי ישן יותר שנקרא Deno land, שם יש יותר חבילות אבל זה לא נתמך מכל הכלים ויש לו תמיכה בספריות מ npm שם הכלים עובדים די טוב אבל הספריות עצמן לא תמיד נתמכות במלואן. וכך יוצא שמצד אחד אי אפשר להגביל את עצמנו רק ל JSR כי אין שם מספיק חבילות אבל עבודה עם מאגרים אחרים יוצרת בלאגן בפרויקט.

ככה מתחיל קובץ הלוגיקה בפרויקט שלי, ושוב אני מזכיר זה פרויקט ממש קטן שכתבתי בכמה שעות:

import * as fs from "@std/fs";
import * as path from "jsr:@std/path";
import wiki from "wikipedia";
import {basename} from "https://deno.land/std@0.224.0/url/mod.ts";

import {
  ImageMagick,
  initialize,
  MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts";

הקובץ כולל ספריות מ JSR, ספריות מ Deno land ועוד ספריות שהוספתי בעזרת פקודת deno add ולכן נשמרו בתוך הקובץ deno.json ברשימת ה import-ים:

  "imports": {
    "@std/fs": "jsr:@std/fs@^1.0.5",
    "hono": "jsr:@hono/hono@^4.6.6",
    "wikipedia": "npm:wikipedia@^2.1.2"
  },

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

מבחינת קוד העבודה עם דינו היתה מאוד פשוטה בזכות התמיכה בסטנדרטים המתקדמים של פיתוח רשת, כלומר אפשר להשתמש ב fetch כדי למשוך מידע מ URL מרוחק, Promise.all כדי לחכות לכמה בקשות רשת במקביל ויש להם פונקציה בשם Deno.writeFile כדי לכתוב לקובץ. בגדול מבחינת קוד אין שם שום דבר ש Chat GPT לא יכל היה לכתוב. בעיה אחת שכן היתה לי עם הקוד היא הלולאה:

  for (let i=0; i < names.length; i++) {
    const fixed = await modifyImage(buffers[i], 512, 512);
    await Deno.writeFile(path.join(dir, names[i]), fixed);
  }

הלולאה מפעילה את modifyImage בצורה סדרתית תמונה אחרי תמונה. כשניסיתי להפעיל את הפונקציה במקביל על כל התמונות עם Promise.all קיבלתי תוצאות שגויות. לא הצלחתי להבין למה.

בעיה נוספת עם הקוד היתה ש Deno לא הצליח לפענח את קובץ ה index.d.ts של ספריית wikipedia מ npm, ולכן אי אפשר היה להשתמש בספריה מקובץ TypeScript ועברתי ל JavaScript בקובץ הלוגיקה.

4. דוקר

הקובץ האחרון הוא ה Dockerfile וגם פה יש כבר פיתרון טוב לדינו זה נראה ככה:

FROM denoland/deno:2.0.3

# The port that your application listens to.
EXPOSE 8000

WORKDIR /app
RUN bash -c "mkdir -p /app/files && chown -R deno /app/files"

# Prefer not to run as root.
USER deno

# These steps will be re-run upon each file change in your working directory:
COPY . .
# Compile the main app so that it doesn't need to be compiled each startup/entry.
RUN deno cache main.ts

CMD ["run", "-A", "main.ts"]

האימג' deno מ Denoland היא אימג' בסיסי להרצת תוכנית דינו וצריך להוסיף עליה רק את הקבצים שלנו ולהריץ.

בעיה אחת שהיתה לי עם ה Dockerfile הזה היא שהתקנת התלויות קורית בשורה:

RUN deno cache main.ts

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

COPY package*.json ./
RUN npm install

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

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

האתגר הבא מבחינת דינו נשאר לדעתי החיבור ל npm - או על ידי שיפור התמיכה במודולים מ npm והפיכתם ל First Class Citizens באקוסיסטם או על ידי יבוא מאוד מאסיבי של מודולים מ npm ל JSR.