איך האפליקציה Kiwix PWA מאפשרת למשתמשים לאחסן ג'יגה-בייט של נתונים מהאינטרנט לשימוש לא מקוון

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

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

בניתוח המקרה הזה נסביר איך Kiwix, ארגון ללא מטרות רווח, משתמשת בטכנולוגיה של אפליקציות אינטרנט מתקדמות (Progressive Web Apps) וב-File System Access API כדי לאפשר למשתמשים להוריד ולאחסן בארכיונים גדולים באינטרנט לשימוש אופליין. הסבר על ההטמעה הטכנית של הקוד שעוסק ב-Origin Private File System (OPFS), תכונת דפדפן חדשה ב-Kiwix PWA שמשפרת את ניהול הקבצים, ומעניקה גישה משופרת לארכיונים ללא בקשות הרשאה. במאמר הזה נדון באתגרים ונציג פיתוחים פוטנציאליים עתידיים במערכת הקבצים החדשה הזו.

מידע על Kiwix

יותר מ-30 שנה אחרי הולדת האינטרנט, שליש מאוכלוסיית העולם עדיין ממתין לגישה אמינה לאינטרנט, לפי האיחוד הבינלאומי של טלקומוניקציות (ITU). כאן מסתיים הסיפור? כמובן שלא. הצוות של Kiwix, עמותה ששוכנת בשווייץ, פיתח סביבה עסקית של אפליקציות ותוכן בקוד פתוח שמטרתה להנגיש ידע לאנשים עם גישה מוגבלת לאינטרנט או ללא גישה בכלל. הרעיון הוא שאם אתם לא מצליחים לגשת לאינטרנט בקלות, מישהו יוכל להוריד בשבילכם משאבי מפתח, איפה ומתי הקישוריות זמינה, ולאחסן אותם באופן מקומי לשימוש מאוחר יותר במצב אופליין. עכשיו אפשר להמיר לארכיונים דחוסים מאוד, שנקראים קובצי ZIM, אתרים חיוניים רבים, כמו ויקיפדיה, Project Gutenberg,‏ Stack Exchange ואפילו הרצאות TED, ולקרוא אותם בזמן אמת באמצעות דפדפן Kiwix.

ארכיוני ZIM משתמשים בדחיסת Zstandard (ZSTD) יעילה מאוד (בגרסאות ישנות יותר נעשה שימוש ב-XZ), בעיקר לאחסון HTML, JavaScript ו-CSS, ואילו התמונות בדרך כלל מומרות לפורמט WebP דחוס. כל ZIM כוללת גם כתובת URL ואינדקס של כותרים. הדחיסה היא המפתח כאן, כי כל תוכן ויקיפדיה באנגלית (6.4 מיליון מאמרים ותמונות) נדחס ל-97GB אחרי ההמרה לפורמט ZIM. נשמע הרבה, אבל אם תחשבו את זה, תוכלו להבין שכל הידע האנושי יכול להיכנס עכשיו לטלפון Android מדור הביניים. יש גם הרבה מקורות מידע קטנים יותר, כולל גרסאות של ויקיפדיה לפי נושאים, כמו מתמטיקה, רפואה ועוד.

ב-Kiwix יש מגוון של אפליקציות נייטיב שמטרגטות מחשב (Windows/Linux/macOS) וגם שימוש בנייד (iOS/Android). עם זאת, בניתוח הזה נתמקד באפליקציה מסוג Progressive Web App ‏(PWA), שמטרתה לספק פתרון אוניברסלי ופשוט לכל מכשיר עם דפדפן מודרני.

נבחן את האתגרים שמתעוררים במהלך הפיתוח של אפליקציית אינטרנט אוניברסלית שצריכה לספק גישה מהירה לארכיונים גדולים של תוכן במצב אופליין מלא, וגם כמה ממשקי API מודרניים של JavaScript, במיוחד File System Access API ו-Origin Private File System, שמספקים פתרונות חדשניים ומעניינים לאתגרים האלה.

אפליקציית אינטרנט לשימוש במצב אופליין?

משתמשי Kiwix הם קבוצה אקלקטית עם צרכים רבים ושונים, ול-Kiwix יש שליטה מועטה או אפסית במכשירים ובמערכות ההפעלה שבהם המשתמשים יגשו לתוכן שלהם. חלק מהמכשירים האלה עשויים להיות איטיים או מיושנים, במיוחד באזורים עם הכנסה נמוכה בעולם. ב-Kiwix מנסים לטפל בכמה שיותר תרחישים לדוגמה, אבל בארגון גם הבינו שאפשר להגיע ליותר משתמשים באמצעות שימוש ברכיב התוכנה האוניברסלי ביותר בכל מכשיר: דפדפן האינטרנט. בהשראת חוק Atwood, שלפיו כל אפליקציה שאפשר לכתוב ב-JavaScript תכתב בסופו של דבר ב-JavaScript, לפני כ-10 שנים כמה מפתחי Kiwix החלו להעביר את תוכנת Kiwix מ-C++ ל-JavaScript.

הגרסה הראשונה של הגרסה הזו, שנקראת Kiwix HTML5, הייתה ל-Firefox OS (מערכת ההפעלה של Firefox, שכבר לא קיימת) ולתוספים לדפדפנים. בלב הקוד היה (ועדיין יש) מנוע דחיסה של C++‎ (XZ ו-ZSTD) שעבר הידור לשפת JavaScript ביניים של ASM.js, ולאחר מכן ל-Wasm, או WebAssembly, באמצעות מַעבד Emscripten. מאוחר יותר, השם Kiwix JS והתוספים לדפדפן עדיין מפותחים באופן פעיל.

דפדפן אופליין של Kiwix JS

מזינים את Progressive Web App (PWA). מפתחי Kiwix הבינו את הפוטנציאל של הטכנולוגיה הזו, ופיתחו גרסת PWA ייעודית של Kiwix JS, והתחילו להוסיף שילובים עם מערכות הפעלה שיאפשרו לאפליקציה להציע יכולות כמו של אפליקציה מקומית, במיוחד בתחומים של שימוש אופליין, התקנה, טיפול בקבצים וגישה למערכת הקבצים.

אפליקציות PWA שמותאמות אופליין הן קלות מאוד, ולכן הן מושלמות בהקשרים שבהם יש אינטרנט לנייד יקר או לסירוגין. הטכנולוגיה שעומדת מאחורי התכונה הזו היא Service Worker API ו-Cache API הקשור, שבהם משתמשות כל האפליקציות שמבוססות על Kiwix JS. ממשקי ה-API האלה מאפשרים לאפליקציות לפעול כשרתי צד לקוח, ליירט בקשות אחזור מהמסמך או מהמאמר הראשי שמוצג, ולהפנות אותן לקצה העורפי (JS) כדי לחלץ ולבנות תשובה מהארכיון של ZIM.

אחסון, אחסון בכל מקום

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

בהתחלה, File API אפשר ל-Kiwix JS לקרוא ארכיונים ענקיים של מאות GB (אחד מהארכיונים שלנו בפורמט ZIM הוא בגודל 166GB!), גם במכשירים עם נפח זיכרון נמוך. ה-API הזה נתמך באופן אוניברסלי בכל דפדפן, אפילו בדפדפנים ישנים מאוד, ולכן הוא משמש כחלופה אוניברסלית כשאין תמיכה בממשקי API חדשים יותר. זה פשוט כמו להגדיר אלמנט input ב-HTML, במקרה של Kiwix:

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

לאחר הבחירה, רכיב הקלט מכיל את האובייקטים של הקובץ, שהם למעשה מטא-נתונים שמפנים לנתונים הבסיסיים באחסון. מבחינה טכנית, הקצה העורפי מבוסס-האובייקט של Kiwix, שכתוב ב-JavaScript טהור בצד הלקוח, קורא קטעים קטנים מהארכיון הגדול לפי הצורך. אם צריך לבצע לחתיכות האלה לחץ, הקצה העורפי מעביר אותן למפריד ה-Wasm, ומקבל עוד חתיכות אם צריך, עד שהלחץ של blob מלא (בדרך כלל מאמר או נכס) יבוטל. כלומר, אף פעם לא צריך לקרוא את הארכיון הגדול במלואו לזיכרון.

למרות שהוא אוניברסלי, ל-File API יש חיסרון שגרם לאפליקציות Kiwix JS להיראות מסורבלות ומיושנות בהשוואה לאפליקציות ילידיות: המשתמש צריך לבחור ארכיונים באמצעות בורר קבצים, או לגרור ולשחרר קובץ באפליקציה, בכל פעם שמפעילים את האפליקציה, כי באמצעות ה-API הזה אין דרך לשמור הרשאות גישה מסשן אחד לסשן הבא.

כדי לצמצם את חוויית המשתמש הגרועה, כמו מפתחים רבים, המפתחים של Kiwix JS עברו בהתחלה במסלול Electron. ElectronJS הוא מסגרת מדהימה שמספקת תכונות חזקות, כולל גישה מלאה למערכת הקבצים באמצעות ממשקי Node API. עם זאת, יש לו כמה חסרונות ידועים:

  • הוא פועל רק במערכות הפעלה למחשב.
  • הוא גדול וכבד (70MB-100MB).

גודל אפליקציות Electron, בגלל העובדה שעותק מלא של Chromium כלול בכל אפליקציה, שונה מאוד מ-5.1MB בלבד לאפליקציית ה-PWA המוקטנת והחבילה בחבילה!

אז האם הייתה דרך של Kiwix לשפר את המצב של משתמשי ה-PWA?

File System Access API מציל את המצב

בסביבות 2019, ב-Kiwix נודע ל-Kiwix ממשק API מתפתח שעבר גרסת מקור לניסיון ב-Chrome 78, ואז נקרא Native File System API. הוא הבטיח את היכולת לקבל מאחז קובץ של קובץ או תיקייה ולשמור אותו במסד נתונים של IndexedDB. חשוב לדעת שהכינוי הזה נשמר בין סשנים של האפליקציה, כך שהמשתמש לא נדרש לבחור שוב את הקובץ או התיקייה כשמפעילים מחדש את האפליקציה (אבל הוא צריך לענות על בקשה מהירה להרשאה). כשהוא הגיע לסביבת הייצור, השם שלו השתנה ל-File System Access API, וחלקי הליבה תוקנו על ידי ה-whatWG כ-File System API (FSA).

אז איך פועל החלק File System Access ב-API? כמה נקודות חשובות שכדאי לזכור:

  • זהו ממשק API אסינכרוני (למעט פונקציות מיוחדות ב-Web Workers).
  • כדי להפעיל את בוחרי הקבצים או הספריות באופן פרוגרמטי, מצלמים את התנועה של המשתמש (לוחצים או מקישים על רכיב בממשק המשתמש).
  • כדי שהמשתמש יוכל לתת שוב הרשאה לגשת לקובץ שנבחר בעבר (בסשן חדש), נדרשת גם פעולת משתמש. למעשה, הדפדפן יסרב להציג את בקשת ההרשאה אם היא לא תופעל על ידי פעולת משתמש.

הקוד פשוט יחסית, מלבד הצורך להשתמש ב-IndexedDB API הלא נוח כדי לאחסן את הלחצנים של הקבצים והספריות. החדשות הטובות הן שיש כמה ספריות שמבצעות הרבה מהעבודה הקשה בשבילכם, כמו browser-fs-access. ב-Kiwix JS, החלטנו לעבוד ישירות עם ממשקי ה-API, שיש להם תיעוד מפורט מאוד.

פתיחת בוררי קבצים וספריות

פתיחת בורר קבצים נראית בערך כך (כאן נעשה שימוש ב-Promises, אבל אם אתם מעדיפים את ה-sugar של async/await, תוכלו לעיין במדריך של Chrome למפתחים):

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

הערה: כדי לפשט את הקוד, הוא מעבד רק את הקובץ הראשון שנבחר (ואוסר לבחור יותר מקובץ אחד). אם רוצים לאפשר לבחור כמה קבצים באמצעות { multiple: true }, פשוט עוטפים את כל ה-Promises שמעבדים כל אחיזה בהצהרה Promise.all().then(...), לדוגמה:

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

עם זאת, מומלץ לבקש מהמשתמש לבחור את הספרייה שמכילה את הקבצים האלה, ולא את הקבצים הספציפיים שבה, במיוחד מכיוון שמשתמשי Kiwix נוטים לארגן את כל קובצי ה-ZIM שלהם באותה ספרייה. הקוד להפעלת בורר הספריות כמעט זהה לקוד שלמעלה, אלא שמשתמשים ב-window.showDirectoryPicker.then(function (dirHandle) { … });.

עיבוד ה-handle של הקובץ או הספרייה

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

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

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

עיבוד תיקיות הוא קצת יותר מורכב, כי צריך לעבור על הרשומות בתיקייה שנבחרה באמצעות entries.next() אסינכררוני כדי למצוא את הקבצים או סוגי הקבצים הרצויים. יש כמה דרכים לעשות זאת, אבל זה הקוד שמשמש ב-Kiwix PWA, באופן כללי:

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

שימו לב שלכל רשומה ב-entryList תצטרכו מאוחר יותר לקבל את הקובץ עם entry.getFile().then(function (file) { … }) כדי להשתמש בו, או את הערך המקביל באמצעות const file = await entry.getFile() ב-async function.

אפשר להמשיך?

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

אבל מה אם לא נצטרך לחכות?! מפתחי Kiwix גילו לאחרונה שאפשר להסיר את כל הבקשות להרשאות כבר עכשיו, באמצעות תכונה חדשה ומגניבה של File Access API שנתמכת בדפדפנים Chromium ו-Firefox (ונתמכת באופן חלקי ב-Safari, אבל עדיין חסרה FileSystemWritableFileStream). התכונה החדשה הזו היא Origin Private File System.

מעבר לשימוש ב-native: מערכת הקבצים הפרטית של Origin

Origin Private File System‏ (OPFS) עדיין תכונה ניסיונית ב-PWA של Kiwix, אבל הצוות מאוד רוצה לעודד משתמשים לנסות אותה כי היא צומצמת את הפער בין אפליקציות מקוריות לאפליקציות אינטרנט. אלה היתרונות העיקריים:

  • אפשר לגשת לארכיונים ב-OPFS בלי בקשות להרשאה, גם בזמן ההפעלה. המשתמשים יכולים להמשיך לקרוא מאמר ולעיין בארכיון מהמקום שבו הפסיקו בסשן הקודם, ללא שום בעיה.
  • הוא מספק גישה אופטימיזציה מאוד לקבצים שמאוחסנים בו: ב-Android אנחנו רואים שיפורים במהירות של פי 5 עד פי 10.

הגישה הרגילה לקבצים ב-Android באמצעות File API היא איטית מאוד, במיוחד (כפי שקורה בדרך כלל למשתמשי Kiwix) אם ארכיונים גדולים מאוחסנים בכרטיס microSD ולא באחסון של המכשיר. כל זה משתנה עם ה-API החדש. רוב המשתמשים לא יוכלו לאחסן קובץ של 97GB ב-OPFS (שצורך נפח אחסון במכשיר, ולא בכרטיס ה-microSD), אבל הוא מושלם לאחסון ארכיונים בגודל קטן עד בינוני. רוצים לקבל את האנציקלופדיה הרפואית המלאה ביותר מ-WikiProject Medicine? אין בעיה, בגודל של 1.7GB הוא נכנס בקלות ל-OPFS. (טיפ: צריך לחפש את הפקודה othermdwiki_en_all_maxi בספרייה שבאפליקציה.)

איך פועלת מערכת OPFS

OPFS היא מערכת קבצים שסופקת על ידי הדפדפן, נפרדת לכל מקור, שאפשר להתייחס אליה כאל אחסון ברמת האפליקציה ב-Android. אפשר לייבא קבצים ל-OPFS ממערכת הקבצים שגלויה למשתמשים, או להוריד אותם ישירות אליה (ה-API מאפשר גם ליצור קבצים ב-OPFS). אחרי שהם נמצאים ב-OPFS, הם מבודדים משאר המכשיר. בדפדפנים מבוססי Chromium למחשב, אפשר גם לייצא קבצים בחזרה מ-OPFS למערכות הקבצים שגלויות למשתמשים.

כדי להשתמש ב-OPFS, השלב הראשון הוא לבקש גישה אליו באמצעות navigator.storage.getDirectory() (שוב, אם אתם מעדיפים לראות קוד באמצעות await, תוכלו לקרוא את המאמר Origin Private File System):

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

הכינוי שתקבלו מאותו סוג כמו FileSystemDirectoryHandle שקיבלתם מ-window.showDirectoryPicker() שצוין למעלה, כלומר תוכלו להשתמש שוב בקוד שמטפל בבעיה הזו (ולשמחתי, אין צורך לאחסן את הקוד ב-indexedDB – פשוט לקבל אותו כשצריך). נניח שכבר יש לכם כמה קבצים ב-OPFS ואתם רוצים להשתמש בהם. לאחר מכן, באמצעות הפונקציה iterateAsyncDirEntries() שהוצגה קודם, אפשר לבצע פעולה כמו:

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

חשוב לזכור שעדיין צריך להשתמש ב-getFile() בכל רשומה שאיתה רוצים לעבוד מהמערך archiveList.

ייבוא קבצים לקובץ OPFS

אז איך מעבירים קבצים ל-OPFS מלכתחילה? לא כל כך מהר! קודם כל, צריך להעריך את נפח האחסון שעומד לרשותכם, ולוודא שהמשתמשים לא מנסים להעלות קובץ בנפח 97GB אם הוא לא יתאים.

אפשר לחשב בקלות את המכסה המשוערת: navigator.storage.estimate().then(function (estimate) { … });. קשה קצת יותר לנסח איך להציג את זה למשתמשים. באפליקציית Kiwix בחרנו להציג חלונית קטנה בתוך האפליקציה שמופיעה ליד תיבת הסימון, שמאפשרת למשתמשים לנסות את ה-OPFS:

חלונית שבה מוצג נפח האחסון שנמצא בשימוש באחוזים, ונפח האחסון הזמין שנותר בג&#39;יגה-בייט.

החלונית מאוכלסת באמצעות estimate.quota ו-estimate.usage, לדוגמה:

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

כמו שאפשר לראות, יש גם לחצן שמאפשר למשתמשים להוסיף קבצים ל-OPFS ממערכת הקבצים הגלויה למשתמש. החדשות הטובות כאן הן שאפשר פשוט להשתמש ב-File API כדי לקבל את אובייקט הקובץ (או האובייקטים) הדרוש לייבוא. למעשה, חשוב לא להשתמש ב-window.showOpenFilePicker() כי שיטה זו לא נתמכת ב-Firefox, אבל OPFS כן נתמך בהחלט.

הלחצן הגלוי Add file (s) (הוספת קבצים) שמופיע בצילום המסך למעלה אינו כלי לבחירת קבצים מדור קודם, אבל הוא click() משמש כבורר מוסתר מדור קודם (אלמנט <input type="file" multiple … />) כשלוחצים עליו או מקישים עליו. לאחר מכן האפליקציה מתעדת את האירוע change של הקלט של הקובץ המוסתר, בודקת את הגודל של הקבצים ומשייכת אותם אם הם גדולים מדי לתקרה. אם הכל בסדר, שואלים את המשתמש אם הוא רוצה להוסיף את החשבון:

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

תיבת דו-שיח שבה המשתמש מתבקש להוסיף רשימה של קובצי ‎.zim למערכת הקבצים הפרטית של המקור.

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

אז איך Kiwix הטמיעה את הפונקציה importOPFSEntries()? לשם כך משתמשים בשיטה fileHandle.createWriteable(), שמאפשרת להעביר כל קובץ בסטרימינג ל-OPFS. כל העבודה הקשה מתבצעת על ידי הדפדפן. (ב-Kiwix משתמשים ב-Promises מסיבות שקשורות לקוד המורשת שלנו, אבל צריך לציין שבמקרה הזה await יוצר תחביר פשוט יותר ומאפשר להימנע מהאפקט של 'פירמידה של אבדון').

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

הורדת מקור נתונים ישירות ל-OPFS

וריאציה על כך היא היכולת להעביר קובץ בסטרימינג מהאינטרנט ישירות ל-OPFS, או לכל ספרייה שיש לכם להן מזהה ספרייה (כלומר, ספריות שנבחרו באמצעות window.showDirectoryPicker()). הקוד הזה מבוסס על אותם עקרונות כמו הקוד שלמעלה, אבל הוא יוצר Response שמורכב מ-ReadableStream ומבקר שמוסיף לתור את הבייטים שנקראים מהקובץ המרוחק. לאחר מכן, ה-Response.body שמתקבל מועבר לכותב של הקובץ החדש בתוך קובץ ה-OPFS.

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

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

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

הטמעת מנהל קבצים בתוך האפליקציה

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

רציתי להמליץ על התוסף הנהדר OPFS Explorer ל-Chrome (הוא פועל גם ב-Edge). העדכון מוסיף כרטיסייה בכלי הפיתוח שמאפשרת לראות בדיוק מה נמצא ב-OPFS, וגם למחוק קבצים זדוניים או כושלים. היה חשוב לנו מאוד לבדוק אם הקוד עובד, לעקוב אחרי התנהגות ההורדות ובאופן כללי לנקות את ניסויי הפיתוח שלנו.

ייצוא של קבצים תלוי ביכולת לקבל מאחז קובץ בקובץ או בתיקייה שנבחרו, שבהם Kiwix תשמור את הקובץ שיוצאו. לכן, הפעולה הזו פועלת רק בהקשרים שבהם אפשר להשתמש בשיטה window.showSaveFilePicker(). אם קובצי Kiwix היו קטנים מ-GB, היינו יכולים ליצור blob בזיכרון, לתת לו כתובת URL ואז להוריד אותו למערכת הקבצים שגלויה למשתמש. לצערנו, אי אפשר לעשות זאת עם ארכיונים גדולים כל כך. אם יש תמיכה ביצוא, הוא פשוט למדי: הוא כמעט זהה, רק הפוך, לשמירת קובץ ב-OPFS (מקבלים את ה-handle של הקובץ שרוצים לשמור, מבקשים מהמשתמש לבחור מיקום לשמירה באמצעות window.showSaveFilePicker(), ואז משתמשים ב-createWriteable() ב-saveHandle). אפשר לראות את הקוד במאגר.

מחיקת קבצים נתמכת בכל הדפדפנים, וניתן לבצע אותה באמצעות dirHandle.removeEntry('filename') פשוט. במקרה של Kiwix, העדפנו להריץ את הרשומות ב-OPFS כמו שעשינו למעלה, כדי שנוכל לבדוק קודם אם הקובץ שנבחר קיים ולבקש אישור, אבל יכול להיות שזה לא יהיה נחוץ לכולם. שוב, אפשר לבדוק את הקוד שלנו אם רוצים.

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

תיבת דו-שיח שבה המשתמש מתבקש למחוק קובץ ‎.zim.

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

עבודה של מפתחים אף פעם לא מסתיימת

OPFS הוא חידוש נהדר למפתחי אפליקציות PWA, שמספק תכונות חזקות מאוד לניהול קבצים שעוזר לצמצם את הפער בין אפליקציות מקוריות לאפליקציות אינטרנט. אבל המפתחים הם חבורה אומללה — הם אף פעם לא מרוצים! OPFS כמעט מושלם, אבל לא בדיוק... טוב מאוד שהתכונות העיקריות פועלות גם ב-Chromium וגם ב-Firefox, ושהן מיושמות ב-Android וגם במחשב. אנחנו מקווים שערכת התכונות המלאה תוטמע בקרוב גם ב-Safari וב-iOS. הבעיות הבאות עדיין לא טופלו:

  • כרגע, ב-Firefox יש מגבלה של 10GB על המכסה של OPFS, ללא קשר לנפח האחסון הבסיסי בדיסק. רוב מחברי ה-PWA יכולים להסתפק במכסה הזו, אבל עבור Kiwix היא מגבילה מאוד. למרבה המזל, דפדפני Chromium הרבה יותר נדיבים.
  • כרגע אי אפשר לייצא קבצים גדולים מ-OPFS למערכת הקבצים שגלויה למשתמשים בדפדפנים בנייד או ב-Firefox במחשב, כי window.showSaveFilePicker() לא מוטמע. בדפדפנים האלה, קבצים גדולים נלכדים למעשה ב-OPFS. המדיניות הזו מנוגדת לאתוס של Kiwix, שמבוסס על גישה פתוחה לתוכן ועל היכולת לשתף ארכיונים בין משתמשים, במיוחד באזורים שבהם החיבור לאינטרנט לא יציב או יקר.
  • המשתמשים לא יכולים לקבוע איזה נפח אחסון תצרוך מערכת הקבצים הווירטואלית מ-OPFS. זו בעייתית במיוחד במכשירים ניידים, שבהם למשתמשים יש כמויות גדולות בכרטיס ה-microSD, אבל נפח אחסון קטן מאוד במכשיר.

אבל בסך הכול, מדובר בצ'אטים קלים, שאחרת הם צעד משמעותי קדימה בגישה לקבצים באפליקציות PWA. צוות Kiwix PWA מודה מאוד למפתחים ולתומכים של Chromium שהציעו ועיצבו לראשונה את File System Access API, ועל העבודה הקשה שהובילה להסכמה בין ספקי הדפדפנים לגבי החשיבות של מערכת הקבצים הפרטית של המקור. ב-Kiwix JS PWA, ה-PWA פתר הרבה מהבעיות שקשורות לחוויית המשתמש שהגבילו את האפליקציה בעבר, ועוזר לנו לשפר את הנגישות של תוכן Kiwix לכולם. נשמח לשמוע מה חשבתם על אפליקציית Kiwix ל-PWA ולשלוח משוב למפתחים.

באתרים הבאים אפשר למצוא מקורות מידע מעולים על היכולות של אפליקציות PWA: