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

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

מערכת הקבצים הפרטית של המקור נתמכת על ידי דפדפנים מודרניים, והיא מוגדרת על ידי קבוצת העבודה של Web Hypertext Application Technology Group (WHEREWG) במסגרת File System Living Standard.

תמיכה בדפדפן

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

מקור

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

כשחושבים על קבצים במחשב, סביר להניח שמדמיינים היררכיית קבצים: קבצים שמאורגנים בתיקיות שאפשר לעיין בהן באמצעות סייר הקבצים של מערכת ההפעלה. לדוגמה, ב-Windows, עבור משתמש בשם תמיר, יכול להיות שרשימת המשימות שלו נמצאת ב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() ולאפשר למשתמש לבחור קובץ חדש. (אם המשתמש יבחר קובץ שנפתח בעבר, התוכן שלו יימחק). כדי לשמור שוב, אפשר לשמור את מזהה הקובץ כדי שלא תצטרכו להציג שוב את תיבת הדו-שיח לשמירת הקובץ.

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

קבצים ותיקיות שאפשר לגשת אליהם באמצעות השיטות האלה נמצאים במערכת קבצים שאפשר לכנות גלויה למשתמש. קבצים שנשמרו מהאינטרנט, ובמיוחד קובצי הפעלה, מסומנים בסימן האינטרנט, כך שמערכת ההפעלה יכולה להציג אזהרה נוספת לפני הפעלת קובץ שעלול להיות מסוכן. כאמצעי אבטחה נוסף, קבצים שמתקבלים מהאינטרנט מוגנים גם באמצעות התכונה גלישה בטוחה. לשם כך, כדי לשמור על פשטות ובהקשר של המאמר הזה, אפשר לחשוב על סריקת וירוסים מבוססת-ענן. כשכותבים נתונים לקובץ באמצעות 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 Workers לא יכול לחסום את ה-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() או ל-method 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');

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

משנים את השמות של הקבצים והתיקיות ומעבירים אותם באמצעות השיטה 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.