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

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

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

מידע על Kiwix

יותר מ-30 שנה אחרי לידת האינטרנט, שליש מאוכלוסיית העולם עדיין ממתינה לגישה מהימנה לאינטרנט לפי איגוד התקשורת הבינלאומי. האם כאן הסתיים הסיפור? בוודאי שלא. אנשים ב-Kiwix, עמותה שבסיסה בשווייץ, פיתחו סביבה עסקית של אפליקציות ותכנים בקוד פתוח, שמטרתה להנגיש את הידע לאנשים עם גישה מוגבלת או בלי גישה לאינטרנט. הרעיון הוא שאם אתם לא מצליחים לגשת לאינטרנט בקלות, הם יוכלו להוריד בשבילכם משאבים מרכזיים, איפה ומתי קישוריות זמינה, ולאחסן אותם באופן מקומי כדי להשתמש בהם מאוחר יותר במצב אופליין. ניתן עכשיו להמיר אתרים חיוניים רבים, כמו ויקיפדיה, פרויקט גוטנברג, Stack Exchange או אפילו הרצאות TED, לארכיונים דחוסים ביותר, שנקראים קובצי ZIM, ולקרוא אותם במהירות באמצעות דפדפן 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 מנסה לכסות כמה שיותר תרחישים לדוגמה, אבל בארגון גם הבינו שאפשר להגיע לעוד יותר משתמשים באמצעות תוכנת התוכנה האוניברסלית ביותר בכל מכשיר: דפדפן האינטרנט. לכן, בהשראת חוק אטווד, שלפיו כל אפליקציה שניתן לכתוב ב-JavaScript, תיכתב בסופו של דבר ב-JavaScript. חלק ממפתחי Kiwix, לפני כ-10 שנים, מגדירים את ההעברה של תוכנת Kiwix מ-C++ ל-JavaScript.

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

דפדפן לא מקוון ב-Kiwix JS

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

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

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

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

ה-File API מאפשר ל-Kiwix JS לקרוא ארכיונים עצומים, מתוך מאות GB (אחד מארכיוני ZIM!) גם במכשירים עם נפח זיכרון נמוך. ה-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 הוא framework מדהים שמספק תכונות מתקדמות, כולל גישה מלאה למערכת הקבצים באמצעות ממשקי Node API. עם זאת, יש לה כמה חסרונות ידועים:

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

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

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

ממשק API של File System Access להצלה

בסביבות שנת 2019, נודע ל-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, אבל אם אתם מעדיפים סוכר מסוג async/await, תוכלו לעיין במדריך של Chrome for Developers):

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 }, צריך פשוט לעטוף את כל ההבטחות שמעובדות כל כינוי בהצהרת 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) { … });.

מתבצע עיבוד של הכינוי של הקובץ או הספרייה

אחרי שמקבלים את הכינוי, צריך לעבד אותו כך שהפונקציה 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 התקשרו אליו, והוא צפוי מאוד.

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

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

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

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

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

איך פועל OPFS

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

כדי להשתמש ב-OPFS, השלב הראשון הוא לבקש גישה אליו באמצעות navigator.storage.getDirectory() (שוב, אם אתם מעדיפים לראות את הקוד באמצעות await, קראו את המאמר The 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:

לוח שמציג את נפח האחסון שבשימוש באחוזים ואת נפח האחסון הפנוי בגיגה-בייט.

החלונית מאוכלסת באמצעות 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()? לשם כך משתמשים ב-method fileHandle.createWriteable(), שמאפשרת למעשה לשדר לכל קובץ ל-OPFS. הדפדפן מטפל בכל העבודה הקשה. (ב-Kiwix משתמשים כאן ב-Promises למטרות שקשורות ל-codebase מדור קודם, אבל צריך לומר שבמקרה הזה 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 (צריך להגדיר כינוי בקובץ כדי לשמור אותו, מבקשים מהמשתמש לבחור מיקום לשמירת הקובץ עם 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. צוות ה-PWA של Kiwix מודה מאוד למפתחי Chromium ולתומכים שהציעו ותכננו לראשונה את File System Access API, ועל העבודה הקשה להשגת קונצנזוס בין ספקי הדפדפנים לגבי החשיבות של מערכת הקבצים הפרטית של מקור. ב-Kiwix JS PWA הוא פתר הרבה מהבעיות בחוויית המשתמש שגרמו לבעיות באפליקציה בעבר, והוא עזר לנו לשפר את הנגישות לתוכן ב-Kiwix לכולם. אנחנו מזמינים אתכם לנסות את ה-PWA של Kiwix ולספר למפתחים מה דעתכם.

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