Sistem file pribadi origin

Standar Sistem File memperkenalkan sistem file pribadi asal (OPFS) sebagai endpoint penyimpanan pribadi untuk asal halaman dan tidak terlihat oleh pengguna yang menyediakan akses opsional ke jenis file khusus yang sangat dioptimalkan untuk performa.

Dukungan browser

Sistem file pribadi origin didukung oleh browser modern dan distandardisasi oleh Web Hypertext Application Technology Working Group (WHATWG) dalam File System Living Standard.

Browser Support

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

Source

Motivasi

Saat memikirkan file di komputer, Anda mungkin memikirkan hierarki file: file yang disusun dalam folder yang dapat Anda jelajahi dengan penjelajah file sistem operasi Anda. Misalnya, di Windows, untuk pengguna bernama Tom, daftar To Do-nya mungkin ada di C:\Users\Tom\Documents\ToDo.txt. Dalam contoh ini, ToDo.txt adalah nama file, dan Users, Tom, dan Documents adalah nama folder. `C:` di Windows merepresentasikan direktori root drive.

Cara umum untuk bekerja dengan file di web

Untuk mengedit Daftar tugas di aplikasi web, berikut alur yang biasa dilakukan:

  1. Pengguna mengupload file ke server atau membukanya di klien dengan <input type="file">.
  2. Pengguna membuat perubahan, lalu mendownload file yang dihasilkan dengan <a download="ToDo.txt> yang disisipkan yang Anda click() secara terprogram dengan JavaScript.
  3. Untuk membuka folder, Anda menggunakan atribut khusus di <input type="file" webkitdirectory>, yang, meskipun memiliki nama eksklusif, memiliki dukungan browser yang hampir universal.

Cara modern untuk bekerja dengan file di web

Alur ini tidak menggambarkan cara pengguna berpikir tentang pengeditan file, dan berarti pengguna akan mendapatkan salinan file input yang didownload. Oleh karena itu, File System Access API memperkenalkan tiga metode pemilih:

Fitur ini memungkinkan alur sebagai berikut:

  1. Buka ToDo.txt dengan showOpenFilePicker(), dan dapatkan objek FileSystemFileHandle.
  2. Dari objek FileSystemFileHandle, dapatkan File dengan memanggil metode getFile() handle file.
  3. Ubah file, lalu panggil requestPermission({mode: 'readwrite'}) pada handle.
  4. Jika pengguna menyetujui permintaan izin, simpan kembali perubahan ke file asli.
  5. Atau, panggil showSaveFilePicker() dan biarkan pengguna memilih file baru. (Jika pengguna memilih file yang sebelumnya dibuka, isinya akan ditimpa.) Untuk penyimpanan berulang, Anda dapat menyimpan handle file, sehingga Anda tidak perlu menampilkan dialog penyimpanan file lagi.

Batasan saat bekerja dengan file di web

File dan folder yang dapat diakses dengan metode ini berada di sistem file yang dapat disebut sebagai terlihat oleh pengguna. File yang disimpan dari web, dan file yang dapat dieksekusi secara khusus, ditandai dengan tanda web, sehingga ada peringatan tambahan yang dapat ditampilkan sistem operasi sebelum file yang berpotensi berbahaya dieksekusi. Sebagai fitur keamanan tambahan, file yang diperoleh dari web juga dilindungi oleh Safe Browsing, yang, demi kesederhanaan dan dalam konteks dokumen ini, dapat Anda anggap sebagai pemindaian virus berbasis cloud. Saat Anda menulis data ke file menggunakan File System Access API, penulisan tidak dilakukan di tempat, tetapi menggunakan file sementara. File itu sendiri tidak dimodifikasi kecuali jika lulus semua pemeriksaan keamanan ini.

Tugas ini membuat operasi file relatif lambat, meskipun ada peningkatan yang diterapkan jika memungkinkan, misalnya, di macOS. Namun, setiap panggilan write() bersifat mandiri, sehingga di balik layar, panggilan ini membuka file, mencari offset yang diberikan, dan akhirnya menulis data.

File sebagai dasar pemrosesan

Pada saat yang sama, file adalah cara yang sangat baik untuk merekam data. Misalnya, SQLite menyimpan seluruh database dalam satu file. Contoh lainnya adalah mipmap yang digunakan dalam pemrosesan gambar. Mipmap adalah urutan gambar yang telah dihitung sebelumnya dan dioptimalkan, yang masing-masing merupakan representasi resolusi yang semakin rendah dari gambar sebelumnya, sehingga mempercepat banyak operasi seperti melakukan zoom. Jadi, bagaimana cara aplikasi web mendapatkan manfaat file, tetapi tanpa biaya performa pemrosesan file berbasis web? Jawabannya adalah sistem file pribadi origin.

Sistem file pribadi origin versus yang terlihat oleh pengguna

Tidak seperti sistem file yang terlihat oleh pengguna yang dijelajahi menggunakan file explorer sistem operasi, dengan file dan folder yang dapat Anda baca, tulis, pindahkan, dan ganti namanya, sistem file pribadi asal tidak dimaksudkan untuk dilihat oleh pengguna. File dan folder di sistem file pribadi origin, seperti namanya, bersifat pribadi, dan lebih konkretnya, bersifat pribadi untuk origin situs. Temukan asal halaman dengan mengetik location.origin di Konsol DevTools. Misalnya, asal halaman https://developer.chrome.com/articles/ adalah https://developer.chrome.com. Baca selengkapnya tentang teori asal-usul di Memahami "situs yang sama" dan "origin yang sama".

Semua halaman yang memiliki origin yang sama dapat melihat data sistem file pribadi origin yang sama, sehingga https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ dapat melihat detail yang sama seperti contoh sebelumnya. Setiap origin memiliki sistem file pribadi origin independennya sendiri, yang berarti sistem file pribadi origin https://developer.chrome.com benar-benar berbeda dari sistem file pribadi origin, misalnya, https://web.dev. Di Windows, direktori root sistem file yang terlihat oleh pengguna adalah C:\\.

Yang setara untuk sistem file pribadi origin adalah direktori root yang awalnya kosong per origin yang diakses dengan memanggil metode asinkron navigator.storage.getDirectory().

Untuk perbandingan sistem file yang terlihat oleh pengguna dan sistem file pribadi origin, lihat diagram berikut. Diagram menunjukkan bahwa selain direktori root, semuanya secara konseptual sama, dengan hierarki file dan folder untuk mengatur dan menyusun sesuai kebutuhan data dan penyimpanan Anda.

Diagram sistem file yang terlihat oleh pengguna dan sistem file pribadi origin dengan dua hierarki file contoh.
Titik entri untuk sistem file yang terlihat pengguna adalah hard disk simbolis, titik entri untuk sistem file pribadi origin adalah panggilan metode navigator.storage.getDirectory.

Spesifikasi sistem file pribadi origin

Sama seperti mekanisme penyimpanan lainnya di browser (misalnya, localStorage atau IndexedDB), sistem file pribadi asal tunduk pada batasan kuota browser. Jika pengguna menghapus semua data penjelajahan atau semua data situs, sistem file pribadi asal juga akan dihapus.

Panggil navigator.storage.estimate() dan di objek respons yang dihasilkan, lihat entri usage untuk melihat seberapa banyak penyimpanan yang sudah digunakan aplikasi Anda, yang dikelompokkan berdasarkan mekanisme penyimpanan dalam objek usageDetails, tempat Anda ingin melihat entri fileSystem secara khusus. Karena sistem file pribadi asal tidak terlihat oleh pengguna, tidak ada perintah izin dan tidak ada pemeriksaan Safe Browsing.

Mendapatkan akses ke direktori root

Untuk mendapatkan akses ke direktori root, jalankan perintah berikut. Anda akan mendapatkan handle direktori kosong, lebih tepatnya, sebuah FileSystemDirectoryHandle.

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

Thread utama atau Web Worker

Ada dua cara untuk menggunakan sistem file pribadi origin: di thread utama atau di Web Worker. Web Worker tidak dapat memblokir thread utama, yang berarti dalam konteks ini, API dapat bersifat sinkron, sebuah pola yang umumnya tidak diizinkan di thread utama. API sinkron dapat lebih cepat karena tidak perlu menangani promise, dan operasi file biasanya sinkron dalam bahasa seperti C yang dapat dikompilasi ke WebAssembly.

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

Jika Anda memerlukan operasi file secepat mungkin, atau Anda berurusan dengan WebAssembly, lanjutkan ke bagian Menggunakan sistem file pribadi asal di Web Worker.

Menggunakan sistem file pribadi asal di thread utama

Membuat file dan folder baru

Setelah memiliki folder root, buat file dan folder menggunakan metode getFileHandle() dan getDirectoryHandle(). Dengan meneruskan {create: true}, file atau folder akan dibuat jika belum ada. Buat hierarki file dengan memanggil fungsi ini menggunakan direktori yang baru dibuat sebagai titik awal.

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

Hierarki file yang dihasilkan dari contoh kode sebelumnya.

Mengakses file dan folder yang ada

Jika Anda mengetahui namanya, akses file dan folder yang dibuat sebelumnya dengan memanggil metode getFileHandle() atau getDirectoryHandle(), dengan meneruskan nama file atau folder.

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

Mendapatkan file yang terkait dengan handle file untuk dibaca

FileSystemFileHandle mewakili file di sistem file. Untuk mendapatkan File terkait, gunakan metode getFile(). Objek File adalah jenis Blob tertentu, dan dapat digunakan dalam konteks apa pun yang dapat dilakukan oleh Blob.

Secara khusus, FileReader, URL.createObjectURL(), createImageBitmap(), dan XMLHttpRequest.send() menerima Blobs dan Files. Mendapatkan File dari FileSystemFileHandle "membebaskan" data, sehingga Anda dapat mengaksesnya dan menyediakannya untuk sistem file yang terlihat oleh pengguna.

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

Menulis ke file dengan streaming

Streaming data ke dalam file dengan memanggil createWritable() yang membuat FileSystemWritableFileStream yang kemudian Anda write() kontennya. Pada akhirnya, Anda perlu close() aliran.

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();

Menghapus file dan folder

Hapus file dan folder dengan memanggil metode remove() tertentu dari handle file atau direktori. Untuk menghapus folder termasuk semua subfolder, teruskan opsi {recursive: true}.

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

Sebagai alternatif, jika Anda mengetahui nama file atau folder yang akan dihapus dalam direktori, gunakan metode removeEntry().

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

Memindahkan dan mengganti nama file dan folder

Ganti nama dan pindahkan file dan folder menggunakan metode move(). Pemindahan dan penggantian nama dapat terjadi bersamaan atau terpisah.

// 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');

Menyelesaikan jalur file atau folder

Untuk mempelajari lokasi file atau folder tertentu dalam kaitannya dengan direktori referensi, gunakan metode resolve() dengan meneruskan FileSystemHandle sebagai argumen. Untuk mendapatkan jalur lengkap file atau folder dalam sistem file pribadi asal, gunakan direktori root sebagai direktori referensi yang diperoleh menggunakan navigator.storage.getDirectory().

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

Memeriksa apakah dua handle file atau folder mengarah ke file atau folder yang sama

Terkadang Anda memiliki dua handle dan tidak tahu apakah keduanya mengarah ke file atau folder yang sama. Untuk memeriksa apakah hal ini terjadi, gunakan metode isSameEntry().

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

Mencantumkan konten folder

FileSystemDirectoryHandle adalah iterator asinkron yang Anda iterasi dengan loop for await... of. Sebagai iterator asinkron, iterator ini juga mendukung metode entries(), values(), dan keys(), yang dapat Anda pilih bergantung pada informasi yang Anda butuhkan:

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()) {}

Mencantumkan konten folder dan semua subfolder secara rekursif

Menangani loop dan fungsi asinkron yang dipasangkan dengan rekursi mudah salah. Fungsi berikut dapat berfungsi sebagai titik awal untuk mencantumkan konten folder dan semua subfoldernya, termasuk semua file dan ukurannya. Anda dapat menyederhanakan fungsi jika tidak memerlukan ukuran file dengan, di mana tertulis directoryEntryPromises.push, tidak mendorong promise handle.getFile(), tetapi handle secara langsung.

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

Menggunakan sistem file pribadi asal di Web Worker

Seperti yang diuraikan sebelumnya, Web Worker tidak dapat memblokir thread utama, itulah sebabnya dalam konteks ini metode sinkron diizinkan.

Mendapatkan handle akses sinkron

Titik entri ke operasi file secepat mungkin adalah FileSystemSyncAccessHandle, yang diperoleh dari FileSystemFileHandle biasa dengan memanggil createSyncAccessHandle().

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

Metode file di tempat sinkron

Setelah memiliki handle akses sinkron, Anda akan mendapatkan akses ke metode file di tempat yang cepat yang semuanya sinkron.

  • getSize(): Menampilkan ukuran file dalam byte.
  • write(): Menulis konten buffer ke dalam file, secara opsional pada offset tertentu, dan menampilkan jumlah byte yang ditulis. Memeriksa jumlah byte yang ditulis yang ditampilkan memungkinkan pemanggil mendeteksi dan menangani error serta penulisan sebagian.
  • read(): Membaca konten file ke dalam buffer, secara opsional pada offset tertentu.
  • truncate(): Mengubah ukuran file ke ukuran yang ditentukan.
  • flush(): Memastikan bahwa konten file berisi semua modifikasi yang dilakukan melalui write().
  • close(): Menutup handle akses.

Berikut adalah contoh yang menggunakan setiap metode.

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

Menyalin file dari sistem file pribadi asal ke sistem file yang dapat dilihat pengguna

Seperti yang disebutkan di atas, memindahkan file dari sistem file pribadi asal ke sistem file yang terlihat pengguna tidak dapat dilakukan, tetapi Anda dapat menyalin file. Karena showSaveFilePicker() hanya diekspos di thread utama, tetapi tidak di thread Worker, pastikan untuk menjalankan kode di sana.

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

Men-debug sistem file pribadi origin

Hingga dukungan DevTools bawaan ditambahkan (lihat crbug/1284595), gunakan ekstensi Chrome OPFS Explorer untuk men-debug sistem file pribadi origin. Screenshot dari bagian Membuat file dan folder baru diambil langsung dari ekstensi.

Ekstensi OPFS Explorer Chrome DevTools di Chrome Web Store.

Setelah menginstal ekstensi, buka Chrome DevTools, pilih tab OPFS Explorer, dan Anda siap memeriksa hierarki file. Simpan file dari sistem file pribadi asal ke sistem file yang dapat dilihat pengguna dengan mengklik nama file dan hapus file serta folder dengan mengklik ikon tempat sampah.

Demo

Lihat cara kerja sistem file pribadi origin (jika Anda menginstal ekstensi OPFS Explorer) dalam demo yang menggunakannya sebagai backend untuk database SQLite yang dikompilasi ke WebAssembly. Pastikan untuk melihat kode sumber di GitHub. Perhatikan bagaimana versi tersemat tidak menggunakan backend sistem file pribadi origin (karena iframe adalah lintas origin), tetapi saat Anda membuka demo di tab terpisah, versi tersemat akan menggunakannya.

Kesimpulan

Sistem file pribadi origin, sebagaimana ditentukan oleh WHATWG, telah membentuk cara kita menggunakan dan berinteraksi dengan file di web. Hal ini telah memungkinkan kasus penggunaan baru yang tidak mungkin dicapai dengan sistem file yang terlihat oleh pengguna. Semua vendor browser utama—Apple, Mozilla, dan Google—telah bergabung dan memiliki visi yang sama. Pengembangan sistem file pribadi origin merupakan upaya kolaboratif, dan masukan dari developer serta pengguna sangat penting untuk kemajuannya.

Seiring kami terus menyempurnakan dan meningkatkan kualitas standar, kami menerima masukan terkait repositori whatwg/fs dalam bentuk Masalah atau Permintaan Pull.

Ucapan terima kasih

Dokumen ini ditinjau oleh Austin Sully, Etienne Noël, dan Rachel Andrew.