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

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

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

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

Browser Support

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

Source

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

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

הן מאפשרות תהליך כזה:

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

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

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

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

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

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

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

בניגוד למערכת הקבצים שגלויים למשתמשים שאפשר לעיין בהם באמצעות סייר הקבצים של מערכת ההפעלה, עם קבצים ותיקיות שאפשר לקרוא, לכתוב, להעביר ולשנות את השם שלהם, מערכת הקבצים הפרטית של המקור לא אמורה להיות גלויה למשתמשים. קבצים ותיקיות במערכת הקבצים הפרטית של המקור, כפי שהשם מרמז, הם פרטיים, ובאופן יותר ספציפי, פרטיים למקור של אתר. כדי לגלות את המקור של דף, מקלידים location.origin במסוף כלי הפיתוח. לדוגמה, המקור של הדף https://developer.chrome.com/articles/ הוא https://developer.chrome.com. מידע נוסף על תיאוריית המקורות זמין במאמר הסבר על המונחים 'אותו אתר' ו'אותו מקור'.

כל הדפים שחולקים את אותו מקור יכולים לראות את הנתונים במערכת הקבצים הפרטית של אותו מקור, כך ש-https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ יכול לראות את אותם הפרטים כמו בדוגמה הקודמת. לכל מקור יש מערכת קבצים פרטית משלו, שפועלת באופן עצמאי. כלומר, מערכת הקבצים הפרטית של מקור https://developer.chrome.com שונה לחלוטין ממערכת הקבצים הפרטית של מקור https://web.dev. ב-Windows, ספריית הבסיס של מערכת הקבצים שגלויים למשתמש היא C:\\.

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

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

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

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

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

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

קבלת גישה לספריית הבסיס

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

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

אחרי שיש לכם תיקיית בסיס, אתם יכולים ליצור קבצים ותיקיות באמצעות ה-method‏ getFileHandle() וה-method‏ 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');

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

משנים את השם של קבצים ותיקיות ומעבירים אותם באמצעות ה-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()) {}

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

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

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

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

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

הדגמה (דמו)

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

סיכום

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

אנחנו ממשיכים לשפר ולעדכן את התקן, ונשמח לקבל משוב על מאגר whatwg/fs בצורת בעיות או בקשות למשיכת שינויים.

תודות

המסמך הזה נבדק על ידי Austin Sully,‏ Etienne Noël וRachel Andrew.