Standard File System wprowadza prywatny system plików pochodzenia (OPFS) jako punkt końcowy pamięci masowej prywatny dla pochodzenia strony i niewidoczny dla użytkownika, który zapewnia opcjonalny dostęp do specjalnego rodzaju pliku zoptymalizowanego pod kątem wydajności.
Obsługa przeglądarek
Prywatny system plików pochodzenia jest obsługiwany przez nowoczesne przeglądarki i jest standaryzowany przez grupę roboczą Web Hypertext Application Technology Working Group (WHATWG) w standardzie File System Living Standard.
Motywacja
Gdy myślisz o plikach na komputerze, prawdopodobnie masz na myśli hierarchię plików: pliki uporządkowane w folderach, które możesz przeglądać za pomocą eksploratora plików systemu operacyjnego. Na przykład w systemie Windows lista zadań użytkownika o nazwie Tom może znajdować się w pliku C:\Users\Tom\Documents\ToDo.txt. W tym przykładzie ToDo.txt to nazwa pliku, a Users, Tom i Documents to nazwy folderów. `C:` w systemie Windows oznacza katalog główny dysku.
Typowy sposób pracy z plikami w internecie
Aby edytować listę zadań w aplikacji internetowej, zwykle wykonujesz te czynności:
- Użytkownik przesyła plik na serwer lub otwiera go na kliencie za pomocą elementu
<input type="file">. - Użytkownik wprowadza zmiany, a następnie pobiera plik wynikowy za pomocą
wstrzykniętego elementu
<a download="ToDo.txt>, który programowoclick()za pomocą JavaScript. - Aby otworzyć foldery, użyj specjalnego atrybutu w
<input type="file" webkitdirectory>, który pomimo zastrzeżonej nazwy jest praktycznie uniwersalnie obsługiwany przez przeglądarki.
Nowoczesny sposób pracy z plikami w internecie
Ten proces nie odzwierciedla sposobu, w jaki użytkownicy myślą o edytowaniu plików, i oznacza, że użytkownicy otrzymują kopie pobranych plików wejściowych. Dlatego interfejs File System Access API wprowadził 3 metody wyboru:
Umożliwiają one wykonanie tych czynności:
- Otwórz
ToDo.txtza pomocąshowOpenFilePicker(), i uzyskaj obiektFileSystemFileHandle. - Z obiektu
FileSystemFileHandleuzyskajFile, wywołując metodęgetFile()uchwytu pliku. - Zmodyfikuj plik, a następnie wywołaj
requestPermission({mode: 'readwrite'})w uchwycie. - Jeśli użytkownik zaakceptuje prośbę o uprawnienia, zapisz zmiany w oryginalnym pliku.
- Możesz też wywołać
showSaveFilePicker()i pozwolić użytkownikowi wybrać nowy plik. (Jeśli użytkownik wybierze wcześniej otwarty plik, jego zawartość zostanie zastąpiona). W przypadku powtarzających się zapisów możesz zachować uchwyt pliku, aby nie trzeba było ponownie wyświetlać okna zapisu pliku.
Ograniczenia dotyczące pracy z plikami w internecie
Pliki i foldery, do których można uzyskać dostęp za pomocą tych metod, znajdują się w systemie plików, który można nazwać widocznym dla użytkownika. Pliki zapisane z internetu, a w szczególności pliki wykonywalne, są oznaczane znakiem internetu, dzięki czemu system operacyjny może wyświetlić dodatkowe ostrzeżenie przed wykonaniem potencjalnie niebezpiecznego pliku. Dodatkowo pliki pobrane z internetu są chronione przez Bezpieczne przeglądanie, które w uproszczeniu i w kontekście tego dokumentu można traktować jako skanowanie antywirusowe w chmurze. Gdy zapisujesz dane w pliku za pomocą interfejsu File System Access API, zapisy nie są wykonywane w miejscu, ale używają pliku tymczasowego. Sam plik nie jest modyfikowany, dopóki nie przejdzie wszystkich tych kontroli bezpieczeństwa.
Ta praca sprawia, że operacje na plikach są stosunkowo wolne, mimo że w miarę możliwości wprowadzane są ulepszenia, np. w macOS.
Każde wywołanie write()
jest jednak samodzielne, więc w tle otwiera plik, wyszukuje dany
przesunięcie i w końcu zapisuje dane.
Pliki jako podstawa przetwarzania
Jednocześnie pliki są doskonałym sposobem na zapisywanie danych. Na przykład, SQLite przechowuje całe bazy danych w jednym pliku. Innym przykładem są mapy mipmap używane w przetwarzaniu obrazów. Mapy mipmap to wstępnie obliczone, zoptymalizowane sekwencje obrazów, z których każdy jest reprezentacją poprzedniego obrazu o coraz niższej rozdzielczości, co przyspiesza wiele operacji, takich jak powiększanie. Jak więc aplikacje internetowe mogą korzystać z plików bez obniżania wydajności przetwarzania plików w internecie? Odpowiedzią jest prywatny system plików pochodzenia.
System plików widoczny dla użytkownika a prywatny system plików pochodzenia
W przeciwieństwie do systemu plików widocznego dla użytkownika, który można przeglądać za pomocą eksploratora plików systemu operacyjnego, z plikami i folderami, które można odczytywać, zapisywać, przenosić i zmieniać ich nazwy, prywatny system plików pochodzenia nie jest przeznaczony do wyświetlania przez użytkowników. Pliki i foldery
w prywatnym systemie plików pochodzenia, jak sama nazwa wskazuje, są prywatne, a konkretnie – prywatne dla pochodzenia
witryny. Aby sprawdzić pochodzenie strony, wpisz
location.origin
w konsoli Narzędzi deweloperskich. Na przykład pochodzenie strony
https://developer.chrome.com/articles/ to https://developer.chrome.com.
Więcej informacji o teorii pochodzenia znajdziesz w artykule
Understanding "same-site" and "same-origin".
Wszystkie strony, które mają to samo pochodzenie, mogą wyświetlać te same dane prywatnego systemu plików pochodzenia, więc https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ może wyświetlać te same szczegóły co w poprzednim przykładzie. Każde pochodzenie ma swój własny
niezależny prywatny system plików pochodzenia, co oznacza, że prywatny system plików
pochodzenia https://developer.chrome.com jest zupełnie inny niż np. https://web.dev. W systemie Windows katalogiem głównym systemu plików widocznego dla użytkownika jest C:\\.
Odpowiednikiem prywatnego systemu plików pochodzenia jest początkowo pusty katalog główny
na pochodzenie, do którego można uzyskać dostęp, wywołując asynchroniczną metodę
navigator.storage.getDirectory().
Porównanie systemu plików widocznego dla użytkownika i prywatnego systemu plików pochodzenia znajdziesz na tym diagramie. Diagram pokazuje, że oprócz katalogu głównego wszystko inne jest koncepcyjnie takie samo – hierarchia plików i folderów, które można uporządkować i ułożyć zgodnie z potrzebami w zakresie danych i pamięci masowej.
navigator.storage.getDirectory.Szczegóły prywatnego systemu plików pochodzenia
Podobnie jak inne mechanizmy przechowywania danych w przeglądarce (np. localStorage lub IndexedDB), prywatny system plików pochodzenia podlega ograniczeniom przydziału w przeglądarce. Gdy użytkownik wyczyści wszystkie dane przeglądania lub wszystkie dane witryny, prywatny system plików pochodzenia również zostanie usunięty.
Wywołaj navigator.storage.estimate()
i w obiekcie odpowiedzi zobacz
usage
wpis, aby sprawdzić, ile miejsca na dane zajmuje już Twoja aplikacja. Jest on podzielony według
mechanizmu przechowywania w obiekcie usageDetails
, w którym należy sprawdzić wpis fileSystem . Ponieważ prywatny system plików pochodzenia nie jest widoczny dla użytkownika, nie ma żadnych próśb o uprawnienia ani kontroli Bezpiecznego przeglądania.
Uzyskiwanie dostępu do katalogu głównego
Aby uzyskać dostęp do katalogu głównego, uruchom to polecenie. Otrzymasz
pusty uchwyt katalogu, a dokładniej –
FileSystemDirectoryHandle.
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
Wątek główny lub Web Worker
Prywatny system plików pochodzenia można używać na 2 sposoby: w wątku głównym lub w Web Worker. Web Workers nie mogą blokować wątku głównego, co oznacza, że w tym kontekście interfejsy API mogą być synchroniczne, co jest ogólnie niedozwolone w wątku głównym. Synchroniczne interfejsy API mogą być szybsze, ponieważ nie muszą obsługiwać obietnic, a operacje na plikach są zwykle synchroniczne w językach takich jak C, które można skompilować do WebAssembly.
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);
Jeśli potrzebujesz najszybszych możliwych operacji na plikach lub pracujesz z WebAssembly, przejdź do sekcji Używanie prywatnego systemu plików pochodzenia w Web Worker.
Używanie prywatnego systemu plików pochodzenia w wątku głównym
Tworzenie nowych plików i folderów
Gdy masz folder główny, utwórz pliki i foldery za pomocą metod
getFileHandle()
i getDirectoryHandle(). Jeśli przekażesz
{create: true},
plik lub folder zostanie utworzony, jeśli nie istnieje. Utwórz hierarchię plików, wywołując te funkcje, używając nowo utworzonego katalogu jako punktu początkowego.
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});

Uzyskiwanie dostępu do istniejących plików i folderów
Jeśli znasz ich nazwę, uzyskaj dostęp do wcześniej utworzonych plików i folderów, wywołując metody getFileHandle() lub getDirectoryHandle() i przekazując nazwę pliku lub folderu.
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder');
Pobieranie pliku powiązanego z uchwytem pliku do odczytu
FileSystemFileHandle reprezentuje plik w systemie plików. Aby uzyskać powiązany File, użyj metody getFile(). Obiekt File to szczególny rodzaj
Blob, którego można używać w
każdym kontekście, w którym można używać Blob.
W szczególności FileReader,
URL.createObjectURL(),
createImageBitmap(),
i XMLHttpRequest.send()
akceptują zarówno Blobs jak i Files. Uzyskanie File z
FileSystemFileHandle „uwalnia” dane, dzięki czemu możesz uzyskać do nich dostęp i udostępnić je
w systemie plików widocznym dla użytkownika.
const file = await fileHandle.getFile();
console.log(await file.text());
Zapisywanie w pliku przez przesyłanie strumieniowe
Przesyłaj strumieniowo dane do pliku, wywołując
createWritable()
które tworzy FileSystemWritableFileStream
do którego następnie write()
zawartość. Na koniec musisz
close()
strumień.
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();
Usuwanie plików i folderów
Usuwaj pliki i foldery, wywołując metodę
remove()
ich uchwytu pliku lub katalogu. Aby usunąć folder wraz ze wszystkimi podfolderami, przekaż opcję
{recursive: true}.
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
Alternatywnie, jeśli znasz nazwę pliku lub folderu, który ma zostać usunięty w katalogu, użyj metody removeEntry().
directoryHandle.removeEntry('my first nested file');
Przenoszenie i zmienianie nazw plików i folderów
Zmieniaj nazwy i przenoś pliki i foldery za pomocą metody
move(). Przenoszenie i zmienianie nazw może odbywać się razem lub osobno.
// 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');
Rozwiązywanie ścieżki pliku lub folderu
Aby dowiedzieć się, gdzie znajduje się dany plik lub folder w stosunku do katalogu odniesienia
, użyj metody resolve()
, przekazując jej FileSystemHandle jako argument. Aby uzyskać pełną ścieżkę pliku lub folderu w prywatnym systemie plików pochodzenia, użyj katalogu głównego jako katalogu odniesienia uzyskanego za pomocą navigator.storage.getDirectory().
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
Sprawdzanie, czy 2 uchwyty plików lub folderów wskazują ten sam plik lub folder
Czasami masz 2 uchwyty i nie wiesz, czy wskazują ten sam plik lub folder. Aby sprawdzić, czy tak jest, użyj metody
isSameEntry().
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
Wyświetlanie listy zawartości folderu
FileSystemDirectoryHandle to
asynchroniczny iterator
, po którym można iterować za pomocą pętli
for await... of. Jako asynchroniczny iterator obsługuje też metody
entries(),
values(),
i keys()
, z których możesz wybrać w zależności od potrzebnych informacji:
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()) {}
Rekurencyjne wyświetlanie listy zawartości folderu i wszystkich podfolderów
Praca z asynchronicznymi pętlami i funkcjami połączonymi z rekurencją jest łatwa do popełnienia błędu. Ta funkcja może służyć jako punkt wyjścia do wyświetlania listy zawartości folderu i wszystkich jego podfolderów, w tym wszystkich plików i ich rozmiarów. Możesz uprościć funkcję, jeśli nie potrzebujesz rozmiarów plików, w których
jest napisane directoryEntryPromises.push, nie przekazując handle.getFile()
obietnicy, ale bezpośrednio 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;
};
Używanie prywatnego systemu plików pochodzenia w Web Worker
Jak wspomnieliśmy wcześniej, Web Workers nie mogą blokować wątku głównego, dlatego w tym kontekście dozwolone są metody synchroniczne.
Uzyskiwanie synchronicznego uchwytu dostępu
Punktem wejścia do najszybszych możliwych operacji na plikach jest
FileSystemSyncAccessHandle,
który można uzyskać z regularnego
FileSystemFileHandle
wywołując createSyncAccessHandle().
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
Synchroniczne metody plików w miejscu
Gdy masz synchroniczny uchwyt dostępu, uzyskujesz dostęp do szybkich metod plików w miejscu, które są synchroniczne.
getSize(): zwraca rozmiar pliku w bajtach.write(): zapisuje zawartość bufora w pliku, opcjonalnie w danym przesunięciu, i zwraca liczbę zapisanych bajtów. Sprawdzenie zwróconej liczby zapisanych bajtów umożliwia wywołującym wykrywanie i obsługę błędów oraz częściowych zapisów.read(): odczytuje zawartość pliku do bufora, opcjonalnie w danym przesunięciu.truncate(): zmienia rozmiar pliku na podany rozmiar.flush(): zapewnia, że zawartość pliku zawiera wszystkie modyfikacje wprowadzone za pomocąwrite().close(): zamyka uchwyt dostępu.
Oto przykład użycia każdej metody.
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);
Kopiowanie pliku z prywatnego systemu plików pochodzenia do systemu plików widocznego dla użytkownika
Jak wspomnieliśmy powyżej, przenoszenie plików z prywatnego systemu plików pochodzenia do systemu plików widocznego dla użytkownika nie jest możliwe, ale możesz je kopiować. Ponieważ showSaveFilePicker() jest udostępniany tylko w wątku głównym, a nie w wątku roboczym, pamiętaj, aby uruchomić tam kod.
// 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);
}
Debugowanie prywatnego systemu plików pochodzenia
Do czasu dodania wbudowanej obsługi Narzędzi deweloperskich (patrz crbug/1284595), używaj rozszerzenia do Chrome OPFS Explorer, aby debugować prywatny system plików pochodzenia. Zrzut ekranu z sekcji Tworzenie nowych plików i folderów pochodzi z tego rozszerzenia.

Po zainstalowaniu rozszerzenia otwórz Narzędzia deweloperskie w Chrome, wybierz kartę OPFS Explorer i możesz już sprawdzać hierarchię plików. Zapisuj pliki z prywatnego systemu plików pochodzenia w systemie plików widocznym dla użytkownika, klikając nazwę pliku, a usuwaj pliki i foldery, klikając ikonę kosza.
Prezentacja
Zobacz prywatny system plików pochodzenia w działaniu (jeśli zainstalujesz rozszerzenie OPFS Explorer ) w prezentacji , która używa go jako backendu bazy danych SQLite skompilowanej do WebAssembly. Sprawdź kod źródłowy w GitHubie. Zwróć uwagę, że wersja osadzona nie używa backendu prywatnego systemu plików pochodzenia (ponieważ iframe jest między domenami), ale gdy otworzysz prezentację w osobnej karcie, będzie go używać.
Podsumowanie
Prywatny system plików pochodzenia, zgodnie ze specyfikacją WHATWG, zmienił sposób, w jaki używamy plików w internecie i wchodzimy z nimi w interakcję. Umożliwił nowe przypadki użycia, które były niemożliwe do osiągnięcia w systemie plików widocznym dla użytkownika. Wszyscy główni dostawcy przeglądarek – Apple, Mozilla i Google – są na pokładzie i mają wspólną wizję. Rozwój prywatnego systemu plików pochodzenia jest w dużej mierze wynikiem współpracy, a opinie deweloperów i użytkowników są niezbędne do jego rozwoju.
W miarę udoskonalania i ulepszania standardu zachęcamy do przesyłania opinii na temat repozytorium whatwg/fs w postaci zgłoszeń lub próśb o scalenie.
Powiązane artykuły
- Specyfikacja standardu File System
- Repozytorium standardu File System
- Post w WebKit na temat interfejsu File System API z prywatnym systemem plików pochodzenia
- Rozszerzenie OPFS Explorer
Podziękowania
Ten dokument został sprawdzony przez Austina Sully'ego, Etienne'a Noëla i Rachel Andrew.