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 asal didukung oleh browser modern dan distandarisasi oleh Web Hypertext Application Technology Working Group (WhatWG) dalam File System Living Standard.
Motivasi
Ketika Anda memikirkan file di komputer, Anda mungkin memikirkan hierarki file: file yang diatur dalam folder yang dapat Anda jelajahi dengan file explorer sistem operasi Anda. Misalnya, di Windows, untuk pengguna bernama Tomi, daftar Tugasnya mungkin berada 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 mewakili direktori utama drive.
Cara tradisional bekerja dengan file di web
Untuk mengedit daftar Tugas di aplikasi web, berikut adalah alur yang biasa:
- Pengguna mengupload file ke server atau membukanya di klien dengan
<input type="file">
. - Pengguna membuat perubahan, lalu mendownload file yang dihasilkan dengan memasukkan
<a download="ToDo.txt>
yang secara terprogram Andaclick()
melalui JavaScript. - Untuk membuka folder, Anda menggunakan atribut khusus di
<input type="file" webkitdirectory>
, yang, terlepas dari nama eksklusifnya, memiliki dukungan browser yang universal.
Cara modern bekerja dengan file di web
Alur ini tidak merepresentasikan pemikiran pengguna tentang pengeditan file, yang berarti pengguna mendapatkan salinan file input yang telah didownload. Oleh karena itu, File System Access API memperkenalkan tiga metode pemilih—showOpenFilePicker()
, showSaveFilePicker()
, dan showDirectoryPicker()
—yang sesuai dengan namanya. Metode tersebut memungkinkan alur sebagai berikut:
- Buka
ToDo.txt
denganshowOpenFilePicker()
, dan dapatkan objekFileSystemFileHandle
. - Dari objek
FileSystemFileHandle
, dapatkanFile
dengan memanggil metodegetFile()
handle file. - Ubah file, lalu panggil
requestPermission({mode: 'readwrite'})
pada handle. - Jika pengguna menyetujui permintaan izin, simpan perubahan kembali ke file asli.
- Atau, panggil
showSaveFilePicker()
dan biarkan pengguna memilih file baru. (Jika pengguna memilih file yang dibuka sebelumnya, isinya akan ditimpa.) Untuk penyimpanan berulang, Anda dapat menyimpan handle file, sehingga Anda tidak perlu menampilkan dialog penyimpanan file lagi.
Batasan dalam bekerja dengan file di web
File dan folder yang dapat diakses melalui metode ini berada di sistem file yang terlihat oleh pengguna. File yang disimpan dari web, dan khususnya file yang dapat dieksekusi, ditandai dengan tanda web, sehingga ada peringatan tambahan yang dapat ditampilkan oleh sistem operasi sebelum file yang berpotensi berbahaya dieksekusi. Sebagai fitur keamanan tambahan, file yang diperoleh dari web juga dilindungi oleh Safe Browsing, yang demi kemudahan dan dalam konteks artikel ini, Anda dapat menganggapnya sebagai pemindaian virus berbasis cloud. Saat Anda menulis data ke file menggunakan File System Access API, operasi tulis tidak dilakukan, tetapi menggunakan file sementara. File itu sendiri tidak diubah kecuali jika lulus semua pemeriksaan keamanan ini. Seperti yang bisa Anda bayangkan, pekerjaan ini membuat operasi file relatif lambat, meskipun peningkatan diterapkan jika memungkinkan, misalnya, di macOS. Tetap setiap panggilan write()
akan bersifat mandiri, sehingga di balik layar akan membuka file, mencari offset yang diberikan, dan terakhir menulis data.
File sebagai dasar pemrosesan
Pada saat yang sama, file adalah cara terbaik untuk merekam data. Misalnya, SQLite menyimpan seluruh database dalam satu file. Contoh lain adalah mipmaps yang digunakan dalam pemrosesan gambar. Mipmap adalah urutan gambar yang telah dihitung sebelumnya dan dioptimalkan, yang masing-masing merupakan representasi resolusi yang lebih rendah secara progresif dari gambar sebelumnya, yang membuat banyak operasi seperti zoom lebih cepat. Jadi bagaimana aplikasi web bisa mendapatkan manfaat dari file, tetapi tanpa biaya kinerja pemrosesan file berbasis web? Jawabannya adalah sistem file pribadi asal.
Sistem file pribadi yang terlihat oleh pengguna versus sistem file pribadi asal
Tidak seperti sistem file yang dapat dilihat pengguna yang dijelajahi menggunakan file explorer sistem operasi, dengan file dan folder yang dapat dibaca, ditulis, dipindahkan, dan diganti namanya, sistem file pribadi asal tidak dimaksudkan untuk dilihat oleh pengguna. File dan folder dalam sistem file pribadi asal, seperti namanya, bersifat pribadi, dan lebih konkret, bersifat pribadi untuk asal situs. Temukan asal halaman dengan mengetik location.origin
di DevTools Console. Misalnya, asal halaman https://developer.chrome.com/articles/
adalah https://developer.chrome.com
(yaitu, bagian /articles
bukan bagian dari asal). Anda dapat membaca selengkapnya tentang teori asal dalam 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 asal yang independen, yang berarti sistem file pribadi asal https://developer.chrome.com
benar-benar berbeda dari salah satu, misalnya, https://web.dev
. Di Windows, direktori utama dari sistem file yang terlihat oleh pengguna adalah C:\\
.
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 asal, lihat diagram berikut. Diagram menunjukkan bahwa selain direktori {i>root<i}, yang lainnya secara konseptual sama, dengan hierarki file dan folder untuk mengatur dan mengatur sesuai kebutuhan data dan penyimpanan Anda.
Detail sistem file pribadi origin
Sama seperti mekanisme penyimpanan lainnya di browser (contohnya, localStorage atau IndexedDB), sistem file pribadi asal terkena 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 berapa banyak penyimpanan yang telah digunakan aplikasi Anda, yang diperinci berdasarkan mekanisme penyimpanan di objek usageDetails
, tempat Anda ingin melihat entri fileSystem
secara khusus. Karena sistem file pribadi asal tidak terlihat oleh pengguna, tidak ada dialog izin dan pemeriksaan Safe Browsing.
Mendapatkan akses ke direktori root
Untuk mendapatkan akses ke direktori root, jalankan perintah berikut. Anda akan mendapatkan handle direktori kosong, lebih khusus lagi, 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, yaitu pola yang umumnya tidak diizinkan di thread utama. API sinkron dapat lebih cepat karena menghindari keharusan 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 membutuhkan operasi file secepat mungkin, atau menangani WebAssembly, lanjutkan ke bagian Menggunakan sistem file pribadi asal di Web Worker. Selain itu, Anda dapat terus membaca.
Menggunakan sistem file pribadi origin di thread utama
Membuat file dan folder baru
Setelah Anda memiliki folder root, buat file dan folder menggunakan metode getFileHandle()
dan getDirectoryHandle()
. Dengan meneruskan {create: true}
, file atau folder akan dibuat jika tidak ada. Bangun 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});
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 membaca
FileSystemFileHandle
mewakili file pada sistem file. Untuk mendapatkan File
terkait, gunakan metode getFile()
. Objek File
adalah jenis Blob
tertentu, dan dapat digunakan dalam konteks apa pun yang dapat digunakan Blob
. Khususnya, FileReader
, URL.createObjectURL()
, createImageBitmap()
, dan XMLHttpRequest.send()
menyetujui Blobs
dan Files
. Jika Anda mau, mendapatkan File
dari FileSystemFileHandle
"gratis" 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
Alirkan data ke file dengan memanggil createWritable()
yang membuat FileSystemWritableFileStream
ke file tersebut, lalu write()
kontennya. Di bagian akhir, Anda perlu close()
streaming.
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()
khusus dari handle direktori atau file. Untuk menghapus folder yang berisi 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 di direktori, gunakan metode removeEntry()
.
directoryHandle.removeEntry('my first nested file');
Memindahkan dan mengganti nama file dan folder
Ganti nama serta pindahkan file dan folder menggunakan metode move()
. Pemindahan dan penggantian nama dapat terjadi bersamaan atau secara 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 mengetahui lokasi file atau folder tertentu dalam kaitannya dengan direktori referensi, gunakan metode resolve()
, dengan meneruskan FileSystemHandle
sebagai argumen. Untuk mendapatkan jalur lengkap dari file atau folder dalam sistem file pribadi asal, gunakan direktori utama sebagai direktori referensi yang diperoleh melalui navigator.storage.getDirectory()
.
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
Periksa apakah dua handle file atau folder mengarah ke file atau folder yang sama
Terkadang Anda memiliki dua handle dan tidak tahu apakah keduanya mengarah pada file atau folder yang sama. Untuk memeriksa apakah hal ini terjadi, gunakan metode isSameEntry()
.
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
Menampilkan daftar isi folder
FileSystemDirectoryHandle
adalah iterator asinkron yang Anda iterasikan dengan loop for await…of
. Sebagai iterator asinkron, metode ini juga mendukung metode entries()
, values()
, dan keys()
, yang dapat Anda pilih sesuai dengan informasi yang Anda perlukan:
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()) {}
Menampilkan daftar isi folder dan semua subfolder secara rekursif
Menangani fungsi dan loop asinkron yang dipasangkan dengan rekursi mudah terjadi. Fungsi di bawah ini dapat berfungsi sebagai titik awal untuk membuat daftar isi folder dan semua subfoldernya, termasuk semua file dan ukurannya. Anda dapat menyederhanakan fungsi jika tidak memerlukan ukuran file, yang bertuliskan directoryEntryPromises.push
, bukan mengirimkan 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, Pekerja Web tidak dapat memblokir thread utama, itulah sebabnya dalam konteks ini metode sinkron diizinkan.
Mendapatkan handle akses sinkron
Titik entri ke operasi file tercepat adalah FileSystemSyncAccessHandle
, yang diperoleh dari FileSystemFileHandle
reguler dengan memanggil createSyncAccessHandle()
.
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
Metode file di tempat yang sinkron
Setelah memiliki handle akses sinkron, Anda akan mendapatkan akses ke metode file in-tempat 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 tertulis. Memeriksa jumlah byte tertulis yang ditampilkan memungkinkan pemanggil untuk mendeteksi dan menangani error dan penulisan parsial.read()
: Membaca konten file ke dalam buffer, secara opsional pada offset tertentu.truncate()
: Mengubah ukuran file ke ukuran yang ditentukan.flush()
: Memastikan konten file berisi semua perubahan yang dilakukan melaluiwrite()
.close()
: Menutup tuas akses.
Berikut adalah contoh yang menggunakan semua metode yang disebutkan di atas.
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 terlihat oleh pengguna
Seperti yang disebutkan di atas, memindahkan file dari sistem file pribadi asal ke sistem file yang terlihat oleh pengguna tidak mungkin dilakukan, tetapi Anda dapat menyalin file. Karena showSaveFilePicker()
hanya ditampilkan di thread utama, tetapi tidak di thread Pekerja, 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 asal. Screenshot di atas dari bagian Membuat file dan folder baru diambil langsung dari ekstensi.
Setelah menginstal ekstensi, buka Chrome DevTools, pilih tab OPFS Explorer, lalu Anda siap memeriksa hierarki file. Simpan file dari sistem file pribadi asal ke sistem file yang terlihat oleh pengguna dengan mengklik nama file dan hapus file dan folder dengan mengklik ikon tempat sampah.
Demo
Lihat cara kerja sistem file pribadi asal (jika Anda menginstal ekstensi OPFS Explorer) dalam demo yang menggunakannya sebagai backend untuk database SQLite yang dikompilasi ke WebAssembly. Pastikan untuk memeriksa kode sumber di Glitch. Perhatikan bahwa versi yang disematkan di bawah tidak menggunakan backend sistem file pribadi asal (karena iframe lintas origin), tetapi saat Anda membuka demo di tab terpisah, cara tersebut menggunakannya.
Kesimpulan
Sistem file pribadi asal, seperti yang 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—melibatkan diri dan memiliki visi yang sama. Pengembangan sistem file pribadi asal merupakan upaya kolaboratif yang sangat penting, dan masukan dari developer serta pengguna sangat penting untuk progresnya. Seiring kami terus meningkatkan dan meningkatkan standar kami, kami sangat mengharapkan masukan Anda ke repositori whatwg/fs dalam bentuk Masalah atau Permintaan Pull.
Link terkait
- Spesifikasi Standar Sistem File
- Repositori Standar Sistem File
- File System API dengan postingan WebKit Origin Private File System
- Ekstensi OPFS Explorer
Ucapan terima kasih
Artikel ini diulas oleh Austin Sully, Etienne Noël, dan Rachel Andrew. Banner besar oleh Christina Rumpf di Unsplash.