File System Access API: דרך פשוטה יותר לגשת לקבצים מקומיים

File System Access API מאפשר לאפליקציות אינטרנט לקרוא או לשמור שינויים ישירות בקבצים ובתיקיות במכשיר של המשתמש.

מהו File System Access API?

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

אם עבדתם בעבר עם קריאה וכתיבה של קבצים, הרבה מהדברים שאשתף עכשיו יהיו מוכרים לכם. מומלץ לקרוא אותו בכל מקרה, כי לא כל המערכות דומות.

יש תמיכה ב-File System Access API ברוב דפדפני Chromium ב-Windows, ב-macOS, ב-ChromeOS וב-Linux. יוצאת דופן בולטת היא Brave, שבה התכונה זמינה כרגע רק באמצעות דגל. אנחנו עובדים על תמיכה ב-Android בהקשר של crbug.com/1011535.

שימוש ב-File System Access API

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

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

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

  • Chrome: ‏ 86.
  • Edge: ‏ 86.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

זיהוי תכונות

כדי לבדוק אם יש תמיכה ב-File System Access API, צריך לבדוק אם שיטת הבחירה הרצויה קיימת.

if ('showOpenFilePicker' in self) {
  // The `showOpenFilePicker()` method of the File System Access API is supported.
}

רוצה לנסות?

אפשר לראות את File System Access API בפעולה בדמו של כלי לעריכת טקסט.

קריאת קובץ ממערכת הקבצים המקומית

התרחיש הראשון שאני רוצה להתייחס אליו הוא בקשה מהמשתמש לבחור קובץ, ואז פתיחה וקריאה של הקובץ מהדיסק.

איך מבקשים מהמשתמש לבחור קובץ לקריאה

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

כמו הרבה ממשקי API חזקים אחרים, צריך לבצע קריאה ל-showOpenFilePicker() בהקשר מאובטח, וצריך לבצע אותה מתוך פעולת משתמש.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  // Destructure the one-element array.
  [fileHandle] = await window.showOpenFilePicker();
  // Do something with the file handle.
});

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

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

קריאת קובץ ממערכת הקבצים

עכשיו, כשיש לכם מאחז לקובץ, אתם יכולים לקבל את המאפיינים של הקובץ או לגשת לקובץ עצמו. בינתיים, אקרא את התוכן שלו. קריאה ל-handle.getFile() מחזירה אובייקט File שמכיל blob. כדי לקבל את הנתונים מה-blob, צריך להפעיל את אחד מהשיטות שלו (slice(),‏ stream(),‏ text() או arrayBuffer()).

const file = await fileHandle.getFile();
const contents = await file.text();

אפשר לקרוא לאובייקט File שמוחזר על ידי FileSystemFileHandle.getFile() רק כל עוד הקובץ הבסיסי בדיסק לא השתנה. אם הקובץ בדיסק ישתנה, לא תהיה אפשרות לקרוא את האובייקט File ותצטרכו לבצע קריאה חוזרת ל-getFile() כדי לקבל אובייקט File חדש שיקרא את הנתונים שהשתנו.

סיכום של כל המידע

כשהמשתמשים לוחצים על הלחצן פתיחה, מוצג להם בורר קבצים בדפדפן. אחרי שבוחרים קובץ, האפליקציה קוראת את התוכן ומעבירה אותו ל-<textarea>.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const contents = await file.text();
  textArea.value = contents;
});

כתיבת הקובץ במערכת הקבצים המקומית

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

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

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

async function getNewFileHandle() {
  const options = {
    types: [
      {
        description: 'Text Files',
        accept: {
          'text/plain': ['.txt'],
        },
      },
    ],
  };
  const handle = await window.showSaveFilePicker(options);
  return handle;
}

שמירת השינויים בדיסק

כל הקוד לשמירת שינויים בקובץ זמין בדמו של עורך הטקסט שלי ב-GitHub. האינטראקציות של הליבה של מערכת הקבצים נמצאות ב-fs-helpers.js. באופן הפשוט ביותר, התהליך נראה כמו הקוד הבא. אסביר על כל שלב בתהליך.

// fileHandle is an instance of FileSystemFileHandle..
async function writeFile(fileHandle, contents) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write(contents);
  // Close the file and write the contents to disk.
  await writable.close();
}

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

השיטה write() מקבלת מחרוזת, וזוהי הדרישה של עורך טקסט. אבל הוא יכול גם לקבל BufferSource או Blob. לדוגמה, אפשר להעביר ישירות אליו מקור נתונים:

async function writeURLToFile(fileHandle, url) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Make an HTTP request for the contents.
  const response = await fetch(url);
  // Stream the response into the file.
  await response.body.pipeTo(writable);
  // pipeTo() closes the destination pipe by default, no need to close it.
}

אפשר גם ללחוץ על seek() או על truncate() בתוך הסטרימינג כדי לעדכן את הקובץ במיקום ספציפי או לשנות את הגודל שלו.

ציון הצעה לשם קובץ ולספריית התחלה

במקרים רבים, כדאי שהאפליקציה תציע שם קובץ או מיקום ברירת מחדל. לדוגמה, עורך טקסט יכול להציע שם קובץ ברירת מחדל של Untitled Text.txt במקום Untitled. כדי לעשות זאת, מעבירים נכס suggestedName כחלק מהאפשרויות של showSaveFilePicker.

const fileHandle = await self.showSaveFilePicker({
  suggestedName: 'Untitled Text.txt',
  types: [{
    description: 'Text documents',
    accept: {
      'text/plain': ['.txt'],
    },
  }],
});

אותו הדבר לגבי ספריית ברירת המחדל להתחלה. אם אתם מפתחים כלי לעריכת טקסט, כדאי להתחיל את תיבת הדו-שיח לשמירת קובץ או לפתיחת קובץ בתיקיית ברירת המחדל documents. לעומת זאת, אם אתם מפתחים כלי לעריכת תמונות, כדאי להתחיל בתיקיית ברירת המחדל pictures. כדי להציע ספריית ברירת מחדל להתחלה, מעבירים את המאפיין startIn לשיטות showSaveFilePicker, ‏ showDirectoryPicker() או showOpenFilePicker באופן הבא.

const fileHandle = await self.showOpenFilePicker({
  startIn: 'pictures'
});

רשימת הספריות הידועות של המערכת היא:

  • desktop: ספריית שולחן העבודה של המשתמש, אם היא קיימת.
  • documents: הספרייה שבה מאוחסנים בדרך כלל מסמכים שנוצרו על ידי המשתמש.
  • downloads: ספרייה שבה קבצים שהורדתם מאוחסנים בדרך כלל.
  • music: ספרייה שבה קובצי האודיו מאוחסנים בדרך כלל.
  • pictures: ספרייה שבה בדרך כלל מאוחסנות תמונות ותמונות סטילס אחרות.
  • videos: ספרייה שבה בדרך כלל מאוחסנים סרטונים או סרטים.

בנוסף לספריות מערכת מוכרות, אפשר גם להעביר כערך של startIn את ה-handle של קובץ או ספרייה קיימים. תיבת הדו-שיח תיפתח באותה ספרייה.

// Assume `directoryHandle` is a handle to a previously opened directory.
const fileHandle = await self.showOpenFilePicker({
  startIn: directoryHandle
});

ציון המטרה של בוררי קבצים שונים

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

const fileHandle1 = await self.showSaveFilePicker({
  id: 'openText',
});

const fileHandle2 = await self.showSaveFilePicker({
  id: 'importImage',
});

אחסון של מפתחות קבצים או מפתחות ספריות ב-IndexedDB

אפשר ליצור רצף של מאחזי קבצים ומאחזי תיקיות, כלומר אפשר לשמור מאחז קובץ או מאחז תיקייה ב-IndexedDB, או לבצע קריאה ל-postMessage() כדי לשלוח אותם בין מקורות באותו רמה עליונה.

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

בדוגמת הקוד הבאה מוצג אחסון ואחזור של מאחז קובץ ומאחז ספרייה. אתם יכולים לראות את זה בפעולה ב-Glitch. (כדי לקצר את הקוד, השתמשתי בספרייה idb-keyval).

import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js';

const pre1 = document.querySelector('pre.file');
const pre2 = document.querySelector('pre.directory');
const button1 = document.querySelector('button.file');
const button2 = document.querySelector('button.directory');

// File handle
button1.addEventListener('click', async () => {
  try {
    const fileHandleOrUndefined = await get('file');
    if (fileHandleOrUndefined) {
      pre1.textContent = `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const [fileHandle] = await window.showOpenFilePicker();
    await set('file', fileHandle);
    pre1.textContent = `Stored file handle for "${fileHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

// Directory handle
button2.addEventListener('click', async () => {
  try {
    const directoryHandleOrUndefined = await get('directory');
    if (directoryHandleOrUndefined) {
      pre2.textContent = `Retrieved directroy handle "${directoryHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const directoryHandle = await window.showDirectoryPicker();
    await set('directory', directoryHandle);
    pre2.textContent = `Stored directory handle for "${directoryHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

הרשאות ומזהים של קבצים או ספריות שמורים

מאחר שהרשאות לא תמיד נשמרות בין סשנים, צריך לוודא שהמשתמש העניק הרשאה לקובץ או לספרייה באמצעות queryPermission(). אם לא, צריך להתקשר למספר requestPermission() כדי לבקש את זה (מחדש). אותו הדבר קורה למזהי קבצים ומנהלי תיקיות. צריך להריץ את fileOrDirectoryHandle.requestPermission(descriptor) או את fileOrDirectoryHandle.queryPermission(descriptor) בהתאמה.

בעורך הטקסט, יצרתי שיטה verifyPermission() שבודקת אם המשתמש כבר העניק הרשאה, ואם צריך, שולחת את הבקשה.

async function verifyPermission(fileHandle, readWrite) {
  const options = {};
  if (readWrite) {
    options.mode = 'readwrite';
  }
  // Check if permission was already granted. If so, return true.
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  // Request permission. If the user grants permission, return true.
  if ((await fileHandle.requestPermission(options)) === 'granted') {
    return true;
  }
  // The user didn't grant permission, so return false.
  return false;
}

ביקשתי הרשאת כתיבה עם בקשת הקריאה, וכך הפחתתי את מספר הבקשות להרשאה. המשתמש רואה בקשה אחת כשפותח את הקובץ, ומעניק הרשאה גם לקריאה וגם לכתיבה בו.

פתיחת ספרייה ורישום התוכן שלה

כדי להכין רשימה של כל הקבצים בספרייה, קוראים לפונקציה showDirectoryPicker(). המשתמש בוחר ספרייה בבורר, ולאחר מכן מוחזר FileSystemDirectoryHandle שמאפשר לכם למנות את הקבצים בספרייה ולגשת אליהם. כברירת מחדל, תהיה לכם גישת קריאה לקבצים בספרייה, אבל אם אתם צריכים גישת כתיבה, תוכלו להעביר את הערך { mode: 'readwrite' } לשיטה.

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.kind, entry.name);
  }
});

אם אתם צריכים לגשת לכל קובץ באמצעות getFile() כדי, למשל, לקבל את גודל הקובץ הספציפי, אל תשתמשו ב-await על כל תוצאה ברצף, אלא תעבדו את כל הקבצים במקביל, למשל באמצעות Promise.all().

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  const promises = [];
  for await (const entry of dirHandle.values()) {
    if (entry.kind !== 'file') {
      continue;
    }
    promises.push(entry.getFile().then((file) => `${file.name} (${file.size})`));
  }
  console.log(await Promise.all(promises));
});

יצירת קבצים ותיקיות בספרייה או גישה אליהם

אפשר ליצור קבצים ותיקיות בספרייה או לגשת אליהם באמצעות ה-method‏ getFileHandle() או ה-method‏ getDirectoryHandle(), בהתאמה. כדי לקבוע אם צריך ליצור קובץ או תיקייה חדשים אם הם לא קיימים, אפשר להעביר אובייקט options אופציונלי עם מפתח create וערך בוליאני של true או false.

// In an existing directory, create a new directory named "My Documents".
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
  create: true,
});
// In this new directory, create a file named "My Notes.txt".
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });

פתרון הנתיב של פריט בספרייה

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

// Resolve the path of the previously created file called "My Notes.txt".
const path = await newDirectoryHandle.resolve(newFileHandle);
// `path` is now ["My Documents", "My Notes.txt"]

מחיקת קבצים ותיקיות בספרייה

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

// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });

מחיקה ישירה של קובץ או תיקייה

אם יש לכם גישה למטמון של קובץ או ספרייה, תוכלו להפעיל את remove() ב-FileSystemFileHandle או ב-FileSystemDirectoryHandle כדי להסיר אותו.

// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();

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

כדי לשנות את השם של קבצים ותיקיות או להעביר אותם למיקום חדש, אפשר להפעיל את הפונקציה move() בממשק FileSystemHandle. ל-FileSystemHandle יש את ממשקי הצאצאים FileSystemFileHandle ו-FileSystemDirectoryHandle. השיטה move() מקבלת פרמטר אחד או שניים. הערך הראשון יכול להיות מחרוזת עם השם החדש או FileSystemDirectoryHandle לתיקיית היעד. במקרה השני, הפרמטר השני האופציונלי הוא מחרוזת עם השם החדש, כך שאפשר להעביר ולשנות את השם בשלב אחד.

// Rename the file.
await file.move('new_name');
// Move the file to a new directory.
await file.move(directory);
// Move the file to a new directory and rename it.
await file.move(directory, 'newer_name');

שילוב באמצעות גרירה ושחרור

ממשקי ה-HTML לגרירה ושחרור מאפשרים לאפליקציות אינטרנט לקבל קבצים שנגררו ושולבו בדף אינטרנט. במהלך פעולת גרירה ושחרור, פריטים של קבצים וספריות שנגררים משויכים לרשומות של קבצים ורשומות של ספריות, בהתאמה. אם הפריט שנגרר הוא קובץ, ה-method‏ DataTransferItem.getAsFileSystemHandle() מחזיר הבטחה עם אובייקט FileSystemFileHandle. אם הפריט שנגרר הוא ספרייה, ה-method‏ DataTransferItem.getAsFileSystemHandle() מחזיר הבטחה עם אובייקט FileSystemDirectoryHandle. ברשימה הבאה אפשר לראות את זה בפעולה. חשוב לזכור ש-DataTransferItem.kind בממשק הגרירה והשחרור הוא "file" גם לקבצים וגם לספריות, ואילו FileSystemHandle.kind ב-File System Access API הוא "file" לקבצים ו-"directory" לספריות.

elem.addEventListener('dragover', (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener('drop', async (e) => {
  e.preventDefault();

  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFileSystemHandle());

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === 'directory') {
      console.log(`Directory: ${handle.name}`);
    } else {
      console.log(`File: ${handle.name}`);
    }
  }
});

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

מערכת הקבצים הפרטית של המקור היא נקודת קצה של אחסון, וכפי שרואים מהשם, היא פרטית למקור הדף. בדרך כלל, הדפדפנים מטמיעים את זה על ידי שמירת התוכן של מערכת הקבצים הפרטית של המקור בדיסק במקום כלשהו, אבל לא נועד שהמשתמשים יוכלו לגשת לתוכן. באופן דומה, אין ציפייה לקיומם של קבצים או ספריות עם שמות שתואמים לשמות של צאצאים של מערכת הקבצים הפרטית המקורית. יכול להיות שהדפדפן ייראה כאילו יש קבצים, אבל בפנים – מכיוון שזו מערכת קבצים פרטית של המקור – יכול להיות שהדפדפן יאחסן את "הקבצים" האלה במסד נתונים או בכל מבנה נתונים אחר. בעיקרון, אם משתמשים ב-API הזה, לא צפויים למצוא את הקבצים שנוצרו בהתאמה אחת לאחת במקום כלשהו בדיסק הקשיח. אחרי שתקבלו גישה ל-root FileSystemDirectoryHandle, תוכלו לפעול כרגיל במערכת הקבצים הפרטית של המקור.

const root = await navigator.storage.getDirectory();
// Create a new file handle.
const fileHandle = await root.getFileHandle('Untitled.txt', { create: true });
// Create a new directory handle.
const dirHandle = await root.getDirectoryHandle('New Folder', { create: true });
// Recursively remove a directory.
await root.removeEntry('Old Stuff', { recursive: true });

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

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

מקור

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

מערכת הקבצים הפרטית של המקור מספקת גישה אופציונלית לסוג מיוחד של קובץ שמותאם במיוחד לביצועים. לדוגמה, היא מספקת גישת כתיבה בלעדית במקום לתוכן של הקובץ. ב-Chromium 102 ואילך, יש שיטה נוספת במערכת הקבצים הפרטית של המקור שמפשטת את הגישה לקבצים: createSyncAccessHandle() (לפעולות קריאה וכתיבה סינכרוניות). הוא מוצג ב-FileSystemFileHandle, אבל רק ב-Web Workers.

// (Read and write operations are synchronous,
// but obtaining the handle is asynchronous.)
// Synchronous access exclusively in Worker contexts.
const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.read(buffer, { at: 1 });

השלמה של תכונות חסרות (polyfilling)

לא ניתן להוסיף polyfill באופן מלא לשיטות של File System Access API.

  • אפשר לקבל הערכה של השיטה showOpenFilePicker() באמצעות רכיב <input type="file">.
  • אפשר לדמות את השיטה showSaveFilePicker() באמצעות רכיב <a download="file_name">, אבל הפעולה הזו מפעילה הורדה פרוגרמטית ולא מאפשרת להחליף קבצים קיימים.
  • אפשר לחקות את השיטה showDirectoryPicker() באופן חלקי באמצעות האלמנט הלא סטנדרטי <input type="file" webkitdirectory>.

פיתחנו ספרייה בשם browser-fs-access שמשתמשת ב-File System Access API בכל מקרה אפשרי, ומשתמשת באפשרויות הטובות הבאות בכל המקרים האחרים.

אבטחה והרשאות

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

פתיחת קובץ או שמירת קובץ חדש

כלי לבחירת קבצים לפתיחת קובץ לקריאה
בורר קבצים שמשמש לפתיח קובץ קיים לקריאה.

כשפותחים קובץ, המשתמש מספק הרשאה לקרוא קובץ או ספרייה באמצעות בורר הקבצים. אפשר להציג את בורר הקבצים הפתוח רק באמצעות מחווה של המשתמש כשהוא מוצג מהקשר מאובטח. אם המשתמשים ישנו את דעתם, הם יוכלו לבטל את הבחירה בבורר הקבצים והאתר לא יקבל גישה לשום דבר. זוהי אותה התנהגות כמו של הרכיב <input type="file">.

בורר קבצים לשמירת קובץ בדיסק.
בורר קבצים שמשמש לשמירת קובץ בדיסק.

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

תיקיות מוגבלות

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

שינוי של קובץ או ספרייה קיימים

אפליקציית אינטרנט לא יכולה לשנות קובץ בדיסק בלי לקבל הרשאה מפורשת מהמשתמש.

בקשת הרשאה

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

בקשה להרשאה שמוצגת לפני שמירת קובץ.
הנחיה שמוצגת למשתמשים לפני שהדפדפן מקבל הרשאת כתיבה בקובץ קיים.

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

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

שקיפות

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

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

התמדת ההרשאות

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

משוב

נשמח לשמוע על החוויה שלכם עם File System Access API.

תיאור של עיצוב ה-API

האם יש משהו ב-API שלא פועל כצפוי? או אולי חסרות שיטות או מאפיינים שאתם צריכים כדי להטמיע את הרעיון? יש לכם שאלות או הערות לגבי מודל האבטחה?

בעיה בהטמעה?

מצאתם באג בהטמעה של Chrome? או שההטמעה שונה מהמפרט?

  • שולחים דיווח על באג בכתובת https://new.crbug.com. חשוב לכלול כמה שיותר פרטים, הוראות לשחזור הבעיה ולהגדיר את Components ל-Blink>Storage>FileSystem. Glitch הוא כלי מצוין לשיתוף של ריפרודוקציות מהירות.

מתכננים להשתמש ב-API?

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

קישורים שימושיים

תודות

המפרט של File System Access API נכתב על ידי Marijn Kruisselbrink.