Standar Sistem File memperkenalkan sistem file pribadi asal (OPFS) sebagai endpoint penyimpanan yang bersifat pribadi untuk asal halaman dan tidak terlihat oleh pengguna yang memberikan 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
Saat memikirkan file di komputer, Anda mungkin memikirkan hierarki file: file yang diatur dalam folder yang dapat Anda jelajahi dengan file explorer sistem operasi. 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 root drive.
Cara tradisional bekerja dengan file di web
Untuk mengedit daftar Tugas di aplikasi web, berikut adalah alur biasanya:
- Pengguna mengupload file ke server atau membukanya di klien dengan
<input type="file">
. - Pengguna membuat perubahan, lalu mendownload file yang dihasilkan dengan
<a download="ToDo.txt>
yang dimasukkan yang Andaclick()
secara terprogram melalui JavaScript. - Untuk membuka folder, Anda menggunakan atribut khusus di
<input type="file" webkitdirectory>
, yang, meskipun memiliki nama eksklusif, memiliki dukungan browser yang praktis universal.
Cara modern untuk menggunakan file di web
Alur ini tidak mewakili cara pengguna mengedit file, dan berarti pengguna akan mendapatkan salinan file input yang didownload. Oleh karena itu, File System Access API memperkenalkan tiga metode pemilih—showOpenFilePicker()
, showSaveFilePicker()
, dan showDirectoryPicker()
—yang melakukan persis seperti yang disarankan namanya. Semua ini mengaktifkan flow 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 nama sebutan channel. - Jika pengguna menyetujui permintaan izin, simpan perubahan kembali ke file asli.
- Atau, panggil
showSaveFilePicker()
dan izinkan 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.
Pembatasan penggunaan 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 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 Penjelajahan Aman, yang, demi kemudahan dan dalam konteks artikel 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. Seperti yang dapat Anda bayangkan, pekerjaan ini membuat operasi file relatif lambat, meskipun peningkatan diterapkan jika memungkinkan, misalnya, di macOS. Namun, setiap panggilan write()
bersifat mandiri, sehingga di balik layar, panggilan tersebut akan membuka file, mencari offset yang diberikan, dan akhirnya 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 lainnya adalah mipmap yang digunakan dalam pemrosesan gambar. Mipmap adalah urutan gambar yang dioptimalkan dan dihitung sebelumnya, yang masing-masing merupakan representasi resolusi yang lebih rendah secara progresif dari yang sebelumnya, sehingga banyak operasi seperti zoom menjadi lebih cepat. Jadi, bagaimana aplikasi web bisa mendapatkan manfaat file, tetapi tanpa biaya performa pemrosesan file berbasis web? Jawabannya adalah sistem file pribadi asal.
Sistem file pribadi yang terlihat pengguna versus sistem file pribadi origin
Tidak seperti sistem file yang terlihat 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 dalam sistem file pribadi origin, seperti namanya, bersifat pribadi, dan lebih konkret, 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
(yaitu, bagian /articles
bukan bagian dari asal). Anda dapat membaca selengkapnya tentang teori origin 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, misalnya, https://web.dev
. Di Windows, direktori root sistem file yang terlihat pengguna adalah C:\\
.
Hal 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 asal, lihat diagram berikut. Diagram menunjukkan bahwa selain direktori utama, semuanya secara konseptual sama, dengan hierarki file dan folder untuk mengatur dan menata sesuai kebutuhan data dan penyimpanan Anda.
Spesifikasi sistem file pribadi asal
Sama seperti mekanisme penyimpanan lainnya di browser (misalnya, localStorage atau IndexedDB), sistem file pribadi origin tunduk pada batasan kuota browser. Saat pengguna menghapus semua data penjelajahan atau semua data situs, sistem file pribadi asal juga akan dihapus. Panggil navigator.storage.estimate()
dan lihat entri usage
di objek respons yang dihasilkan untuk melihat jumlah penyimpanan yang telah digunakan aplikasi Anda, yang dikelompokkan menurut mekanisme penyimpanan di objek usageDetails
, tempat Anda ingin melihat entri fileSystem
secara khusus. Karena sistem file pribadi asal tidak terlihat oleh pengguna, tidak ada permintaan izin dan tidak ada pemeriksaan Safe Browsing.
Mendapatkan akses ke direktori utama
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, 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 tercepat, atau Anda menangani WebAssembly, lanjutkan ke Menggunakan sistem file pribadi origin di Web Worker. Jika tidak, Anda dapat melanjutkan membaca.
Menggunakan sistem file pribadi origin 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 tidak 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});
Mengakses file dan folder yang ada
Jika Anda mengetahui namanya, akses file dan folder yang dibuat sebelumnya dengan memanggil metode getFileHandle()
atau getDirectoryHandle()
, yang 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 digunakan Blob
. Secara khusus, FileReader
, URL.createObjectURL()
, createImageBitmap()
, dan XMLHttpRequest.send()
menerima Blobs
dan Files
. Jika Anda mau, mendapatkan File
dari FileSystemFileHandle
akan "membebaskan" data, sehingga Anda dapat mengaksesnya dan menyediakannya ke sistem file yang terlihat 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
, lalu Anda write()
kontennya. Pada akhirnya, 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()
tertentu untuk file atau handle direktorinya. 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 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 dilakukan secara 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');
Me-resolve jalur file atau folder
Untuk mempelajari lokasi file atau folder tertentu sehubungan 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']`.
Memeriksa apakah dua nama sebutan file atau folder mengarah ke file atau folder yang sama
Terkadang Anda memiliki dua nama sebutan channel 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`.
Menampilkan daftar isi folder
FileSystemDirectoryHandle
adalah iterator asinkron yang Anda iterasi dengan loop for await…of
. Sebagai iterator asinkron, metode 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()) {}
Menampilkan daftar isi folder dan semua subfolder secara rekursif
Menangani loop dan fungsi asinkron yang disambungkan dengan rekursi mudah salah. 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 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 origin 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 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 in-place sinkron
Setelah memiliki handle akses sinkron, Anda akan mendapatkan akses ke metode file in-place 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 buffering, secara opsional pada offset tertentu.truncate()
: Mengubah ukuran file ke ukuran yang ditentukan.flush()
: Memastikan bahwa konten file berisi semua modifikasi yang dilakukan melaluiwrite()
.close()
: Menutup handle 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 pengguna
Seperti yang disebutkan di atas, Anda tidak dapat memindahkan file dari sistem file pribadi asal ke sistem file yang terlihat oleh pengguna, 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 asal
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 melihat 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 dapat 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 sangatlah merupakan upaya kolaboratif, dan masukan dari developer dan pengguna sangatlah penting untuk progresnya. Seiring upaya kami untuk terus meningkatkan kualitas dan menyempurnakan standar, masukan tentang repositori whatwg/fs dalam bentuk Masalah atau Permintaan Pull akan diterima.
Link terkait
- Spesifikasi Standar Sistem File
- Repositori Standar Sistem File
- Postingan File System API dengan Origin Private File System WebKit
- Ekstensi OPFS Explorer
Ucapan terima kasih
Artikel ini ditinjau oleh Austin Sully, Etienne Noël, dan Rachel Andrew. Gambar hero oleh Christina Rumpf di Unsplash.