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

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

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

בניתוח המקרה הזה נסביר איך Kiwix, ארגון ללא מטרות רווח, משתמשת בטכנולוגיית אפליקציות אינטרנט מתקדמות (Progressive Web Apps) וב-File System Access API כדי לאפשר למשתמשים להוריד ולאחסן בארכיונים גדולים באינטרנט לשימוש אופליין. מידע על ההטמעה הטכנית של הקוד שמיועד למערכת הקבצים הפרטית של המקור (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 Offline

נכנסים ל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 הזה אין דרך לשמור הרשאות גישה מסשן אחד לסשן הבא.

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

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

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

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

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

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

אז איך פועל החלק של ה-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;
 
});
}

חשוב לזכור שצריך לספק את הפונקציה ששומרת את ה-file handle. אין שיטות נוחות לכך, אלא אם משתמשים בספריית הפשטה. אפשר לראות את ההטמעה של 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). התכונה החדשה הזו היא Origin Private File System.

מעבר לשימוש ב-Origin Private File System

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: