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

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

תמיכת דפדפן

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

תמיכה בדפדפן

  • 86
  • 86
  • 111
  • 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>, שלמרות השם הקנייני שלו, יש בו תמיכה כמעט אוניברסלית.

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

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

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

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

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

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

בדיוק כמו מנגנוני אחסון אחרים בדפדפן (לדוגמה, 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

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

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

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

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

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

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

הצגה חוזרת של תוכן התיקייה וכל תיקיות המשנה

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

  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 Workers לא יכולים לחסום את ה-thread הראשי, ולכן בהקשר הזה מותר להשתמש בשיטות סינכרוניות.

קבלת כינוי גישה סינכרוני

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

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

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

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

  • 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);
}

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

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

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

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

הדגמה (דמו)

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

מסקנות

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

אישורים

מאמר זה נבדק על ידי Austin Sully , Etienne Noël ו-Rachel Andrew. תמונה ראשית (Hero) מאת כריסטינה רומף ב-UnFlood.