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

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

תמיכה בדפדפנים

דפדפנים מודרניים תומכים במערכת הקבצים הפרטית של המקור, והיא עומדת בתקן של קבוצת העבודה Web Hypertext Application Technology (WHATWG) בתקן העדכני של מערכת קבצים.

תמיכה בדפדפנים

  • Chrome: ‏ 86.
  • Edge: ‏ 86.
  • Firefox: ‏ 111.
  • Safari: 15.2.

מקור

למה בחרנו לעשות זאת?

כשחושבים על קבצים במחשב, סביר להניח שחושב על היררכיית קבצים: קבצים שמאורגנים בתיקיות שאפשר לעיין בהן באמצעות סייר הקבצים של מערכת ההפעלה. לדוגמה, ב-Windows, רשימת המשימות של משתמש בשם Tom עשויה להיות ב-C:\Users\Tom\Documents\ToDo.txt. בדוגמה הזו, ToDo.txt הוא שם הקובץ, ו-Users,‏ Tom ו-Documents הם שמות התיקיות. 'C:‎' ב-Windows מייצג את הספרייה הראשית של הכונן.

הדרך המסורתית לעבוד עם קבצים באינטרנט

כדי לערוך את רשימת המשימות באפליקציית אינטרנט, זהו התהליך הרגיל:

  1. המשתמש מעלה את הקובץ לשרת או פותח אותו בלקוח באמצעות <input type="file">.
  2. המשתמש מבצע את השינויים שלו, ואז מוריד את הקובץ שנוצר עם <a download="ToDo.txt> שהוזן, click() דרך JavaScript באופן פרוגרמטי.
  3. כדי לפתוח תיקיות, משתמשים במאפיין מיוחד ב-<input type="file" webkitdirectory>, שלמרות השם הקנייני שלו, יש לו תמיכה כמעט אוניברסלית בדפדפנים.

דרך מודרנית לעבוד עם קבצים באינטרנט

התהליך הזה לא מייצג את האופן שבו משתמשים חושבים על עריכת קבצים, והוא אומר שהמשתמשים מקבלים עותקים של קובצי הקלט שלהם. לכן, הוספנו ל-File System Access API שלוש שיטות לבחירת פריטים – showOpenFilePicker(),‏ showSaveFilePicker() ו-showDirectoryPicker() – שמבצעות בדיוק את מה שהשם שלהן מציין. הם מאפשרים את התהליך הבא:

  1. פותחים את ToDo.txt באמצעות showOpenFilePicker() ומקבלים אובייקט FileSystemFileHandle.
  2. מהאובייקט FileSystemFileHandle, מקבלים File על ידי קריאה לשיטה getFile() של הידית של הקובץ.
  3. משנים את הקובץ ומפעילים את requestPermission({mode: 'readwrite'}) במזהה.
  4. אם המשתמש מאשר את בקשת ההרשאה, שומרים את השינויים בקובץ המקורי.
  5. לחלופין, אפשר להפעיל את הפונקציה showSaveFilePicker() ולאפשר למשתמש לבחור קובץ חדש. (אם המשתמש יבחר קובץ שנפתח בעבר, התוכן שלו יימחק). כדי לשמור שוב, אפשר לשמור את ה-handle של הקובץ כדי שלא תצטרכו להציג שוב את תיבת הדו-שיח לשמירת הקובץ.

הגבלות על עבודה עם קבצים באינטרנט

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

קבצים כבסיס לעיבוד

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

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

בניגוד למערכת הקבצים הגלויה למשתמשים, שאפשר לעיין בה באמצעות חלון הקבצים של מערכת ההפעלה, עם קבצים ותיקיות שאפשר לקרוא, לכתוב, להעביר ולשנות את השם שלהם, מערכת הקבצים הפרטית של המקור לא מיועדת להיות גלויה למשתמשים. כפי שרומז השם, קבצים ותיקיות במערכת הקבצים הפרטית של המקור הם פרטיים, ובאופן קונקרטי יותר, פרטיים למקור של האתר. כדי לבדוק מה המקור של דף מסוים, מקלידים location.origin במסוף כלי הפיתוח. לדוגמה, המקור של הדף https://developer.chrome.com/articles/ הוא https://developer.chrome.com (כלומר, החלק /articles לא חלק מהמקור). מידע נוסף על תיאוריית המקור זמין במאמר הסבר על 'באותו אתר' ו'באותו מקור'. כל הדפים שיש להם את אותו מקור יכולים לראות את אותם נתונים של מערכת הקבצים הפרטית של המקור, כך ש-https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ יכול לראות את אותם פרטים כמו בדוגמה הקודמת. לכל מקור יש מערכת קבצים פרטית עצמאית משלו, כלומר מערכת הקבצים הפרטית של המקור https://developer.chrome.com שונה לחלוטין מזו של https://web.dev, למשל. ב-Windows, ספריית הבסיס של מערכת הקבצים שגלויים למשתמש היא C:\\. המקבילה למערכת הקבצים הפרטית של המקור היא ספריית root ריקה בהתחלה לכל מקור, שאפשר לגשת אליה באמצעות קריאה לשיטה האסינכרונית navigator.storage.getDirectory(). בתרשים הבא אפשר לראות השוואה בין מערכת הקבצים שגלויה למשתמשים לבין מערכת הקבצים הפרטית של המקור. בתרשים אפשר לראות שבנוסף לתיקיית השורש, כל השאר זהה מבחינה מושגית, עם היררכיה של קבצים ותיקיות שאפשר לארגן ולסדר לפי הצורך בהתאם לנתונים ולצרכי האחסון שלכם.

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

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

בדומה למנגנוני אחסון אחרים בדפדפן (למשל, localStorage או IndexedDB), מערכת הקבצים הפרטית של המקור כפופה להגבלות המכסות בדפדפן. כשמשתמש מנקה את כל נתוני הגלישה או את כל נתוני האתרים, גם מערכת הקבצים הפרטית של המקור נמחקת. קוראים ל-navigator.storage.estimate() ובאובייקט התגובה שנוצר, בודקים את הערך של usage כדי לראות כמה נפח אחסון האפליקציה כבר משתמשת בו. הנתונים מפורטים לפי מנגנון האחסון באובייקט usageDetails, שבו צריך לבדוק את הערך של fileSystem באופן ספציפי. מכיוון שמערכת הקבצים הפרטית של המקור לא גלויה למשתמש, אין בקשות להרשאות ואין בדיקות של 'גלישה בטוחה'.

קבלת גישה לתיקיית השורש

כדי לקבל גישה לספריית השורש, מריצים את הפקודה הבאה. בסיום תקבלו מזהה ספרייה ריק, ובאופן ספציפי, FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Thread ראשי או Web Worker

יש שתי דרכים להשתמש במערכת הקבצים הפרטית של המקור: בשרשור הראשי או בWeb Worker. משימות Web Worker לא יכולות לחסום את ה-thread הראשי, כלומר בהקשר הזה ממשקי API יכולים להיות סינכרוניים, דפוס שאסור בדרך כלל ב-thread הראשי. ממשקי API סינכרוניים יכולים להיות מהירים יותר כי אין צורך לטפל בהבטחות (promises), ופעולות הקובץ הן בדרך כלל סינכרוניות בשפות כמו C שאפשר לקמפל ל-WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

אם אתם צריכים לבצע פעולות קבצים במהירות האפשרית, או אם אתם עובדים עם WebAssembly, תוכלו לדלג אל שימוש במערכת הקבצים הפרטית של המקור ב-Web Worker. אחרת, אפשר להמשיך לקרוא.

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

יצירת קבצים ותיקיות חדשים

אחרי שיוצרים תיקיית root, יוצרים קבצים ותיקיות באמצעות השיטות getFileHandle() ו-getDirectoryHandle(), בהתאמה. אם מעבירים את הערך {create: true}, הקובץ או התיקייה נוצרים אם הם לא קיימים. כדי ליצור היררכיה של קבצים, אפשר להפעיל את הפונקציות האלה באמצעות ספרייה חדשה שנוצרה כנקודת ההתחלה.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

היררכיית הקבצים שנוצרה מדוגמת הקוד הקודמת.

גישה לקבצים ולתיקיות קיימים

אם אתם יודעים את השם שלהם, תוכלו לגשת לקבצים ולתיקיות שנוצרו בעבר על ידי קריאה לשיטות getFileHandle() או getDirectoryHandle(), והעברת השם של הקובץ או התיקייה.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

אחזור הקובץ שמשויך למטמון הקבצים לצורך קריאה

FileSystemFileHandle מייצג קובץ במערכת הקבצים. כדי לקבל את File המשויך, משתמשים בשיטה getFile(). אובייקט File הוא סוג ספציפי של Blob, ואפשר להשתמש בו בכל הקשר שבו אפשר להשתמש ב-Blob. באופן ספציפי, FileReader,‏ URL.createObjectURL(),‏ createImageBitmap() ו-XMLHttpRequest.send() מקבלים גם את Blobs וגם את Files. אפשר לומר שהשגת File מ-FileSystemFileHandle 'משחררת' את הנתונים, כך שתוכלו לגשת אליהם ולהפוך אותם לזמינים למערכת הקבצים שגלויה למשתמשים.

const file = await fileHandle.getFile();
console.log(await file.text());

כתיבת לקובץ באמצעות סטרימינג

כדי להעביר נתונים בסטרימינג לקובץ, קוראים ל-createWritable(), שמייצר FileSystemWritableFileStream שאליו write() את התוכן. בסוף, צריך close() את הסטרימינג.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

מחיקה של קבצים ותיקיות

כדי למחוק קבצים ותיקיות, קוראים לשיטה הספציפית remove() של ה-handle של הקובץ או הספרייה. כדי למחוק תיקייה כולל כל התיקיות המשניות שלה, מעבירים את האפשרות {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

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

directoryHandle.removeEntry('my first nested file');

העברת קבצים ותיקיות ושינוי השם שלהם

שינוי השם והעברה של קבצים ותיקיות באמצעות ה-method‏ move(). אפשר להעביר ולשנות את השם של ניתוחים יחד או בנפרד.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

פתרון הנתיב של קובץ או תיקייה

כדי לברר איפה נמצא קובץ או תיקייה מסוימים ביחס לספריית עזר, משתמשים בשיטה resolve() ומעבירים לה את FileSystemHandle כארגומנטים. כדי לקבל את הנתיב המלא של קובץ או תיקייה במערכת הקבצים הפרטית של המקור, משתמשים בספריית הבסיס כספריית העזרה שמתקבלת דרך navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

איך בודקים אם שני שמות של קבצים או תיקיות מפנים לאותו קובץ או אותה תיקייה

לפעמים יש לכם שני כינויים ואתם לא יודעים אם הם מפנים לאותו קובץ או תיקייה. כדי לבדוק אם זה המצב, משתמשים בשיטה isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

הצגת רשימת התכנים של תיקייה

FileSystemDirectoryHandle הוא מחזור אסינכרוני שאפשר לבצע עליו מחזור באמצעות לולאה for await…of. כמעבד איטראציות אסינכרוני, הוא תומך גם בשיטות entries(), values() ו-keys(), שמהן אפשר לבחור בהתאם למידע הנדרש:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

הצגת רשימה של התוכן של תיקייה ושל כל תיקיות המשנה באופן רפלקסיבי

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

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

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

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

אחזור של טיפולי גישה סינכרוניים

נקודת הכניסה לפעולות הקובץ המהירות ביותר היא FileSystemSyncAccessHandle, שמתקבלת מ-FileSystemFileHandle רגיל באמצעות קריאה ל-createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

שיטות סינכרוניות לקבצים במקום

אחרי שתקבלו את ה-handle של הגישה הסינכרונית, תהיה לכם גישה לשיטות מהירות לקבצים במקום, שכולן סינכרוניות.

  • getSize(): הפונקציה מחזירה את גודל הקובץ בבייטים.
  • write(): הכתיבה של תוכן מאגר לקובץ, אופציונלי בהיסט נתון, והחזרת מספר הבייטים שנכתבו. בדיקת מספר הבייטים שנכתבו מאפשרת למבצעי הקריאה לזהות שגיאות וכתיבה חלקית ולטפל בהן.
  • read(): קריאת תוכן הקובץ למאגר, אופציונלי בהיסט נתון.
  • truncate(): שינוי הגודל של הקובץ לגודל הרצוי.
  • flush(): מוודא שתוכן הקובץ מכיל את כל השינויים שבוצעו באמצעות write().
  • close(): סגירת הלחצן של הרשאת הגישה.

הנה דוגמה שבה נעשה שימוש בכל השיטות שצוינו למעלה.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

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

כפי שצוין למעלה, אי אפשר להעביר קבצים ממערכת הקבצים הפרטית של המקור למערכת הקבצים שגלויה למשתמשים, אבל אפשר להעתיק קבצים. מאחר ש-showSaveFilePicker() חשוף רק ב-thread הראשי, אבל לא ב-thread של Worker, חשוב להריץ את הקוד שם.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

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

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

התוסף OPFS Explorer לכלי הפיתוח של Chrome בחנות האינטרנט של Chrome.

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

הדגמה (דמו)

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

מסקנות

מערכת הקבצים הפרטית של המקור, כפי שצוינה על ידי WHATWG, עיצבה את האופן שבו אנחנו משתמשים בקבצים באינטרנט ומקיימים איתם אינטראקציה. היא אפשרה תרחישי שימוש חדשים שלא ניתן היה להשיג באמצעות מערכת הקבצים שגלויה למשתמשים. כל יצרני הדפדפנים הגדולים – Apple,‏ Mozilla ו-Google – משתתפים בתוכנית וחולקים חזון משותף. הפיתוח של מערכת הקבצים הפרטית של המקור הוא מאמץ משותף, והמשוב ממפתחים וממשתמשים חיוני להתקדמות שלו. אנחנו ממשיכים לשפר את התקן, ונשמח לקבל מכם משוב במאגר whatwg/fs באמצעות בעיות או בקשות משיכה.

תודות

הבדיקה של המאמר בוצעה על ידי Austin Sully,‏ Etienne Noël ו-Rachel Andrew. התמונה הראשית (Hero) היא של Christina Rumpf ב-Unsplash.