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 wysoce zoptymalizowanego pod kątem wydajności.
Obsługa przeglądarek
Pochodny prywatny system plików jest obsługiwany przez nowoczesne przeglądarki i został ustandaryzowany przez grupę roboczą Web Hypertext Application Technology Working Group (WHATWG) w File System Living Standard.
Motywacja
Gdy myślisz o plikach na komputerze, prawdopodobnie wyobrażasz sobie 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 imieniu Tomek może znajdować się w folderze 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.
Tradycyjny 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ą
<input type="file">
. - Użytkownik wprowadza zmiany, a następnie pobiera wynikowy plik z wstrzykniętym
<a download="ToDo.txt>
, który programowoclick()
za pomocą JavaScriptu. - Do otwierania folderów służy specjalny atrybut w
<input type="file" webkitdirectory>
, który mimo zastrzeżonej nazwy jest obsługiwany przez praktycznie wszystkie przeglądarki.
Nowoczesny sposób pracy z plikami w internecie
Ten proces nie odzwierciedla sposobu, w jaki użytkownicy myślą o edycji plików, i oznacza, że użytkownicy otrzymują pobrane kopie plików wejściowych. Dlatego w interfejsie File System Access API wprowadziliśmy 3 metody selektora: showOpenFilePicker()
, showSaveFilePicker()
i showDirectoryPicker()
, które robią dokładnie to, co sugeruje ich nazwa. Umożliwiają one następujący przepływ:
- Otwórz
ToDo.txt
za pomocąshowOpenFilePicker()
i uzyskaj obiektFileSystemFileHandle
. - Z obiektu
FileSystemFileHandle
uzyskajFile
, wywołując metodęgetFile()
uchwytu pliku. - Zmodyfikuj plik, a potem wywołaj
requestPermission({mode: 'readwrite'})
na uchwycie. - Jeśli użytkownik zaakceptuje prośbę o uprawnienia, zapisz zmiany w oryginalnym pliku.
- Możesz też wywołać funkcję
showSaveFilePicker()
i pozwolić użytkownikowi wybrać nowy plik. (Jeśli użytkownik wybierze wcześniej otwarty plik, jego zawartość zostanie zastąpiona). W przypadku wielokrotnego zapisywania możesz zachować uchwyt pliku, aby nie musieć ponownie wyświetlać okna dialogowego zapisywania pliku.
Ograniczenia dotyczące pracy z plikami w internecie
Pliki i foldery dostępne za pomocą tych metod znajdują się w systemie plików 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. Dodatkową funkcją zabezpieczeń jest bezpieczne przeglądanie, które chroni pliki pobrane z internetu. Dla uproszczenia i w kontekście tego artykułu można je traktować jako skanowanie antywirusowe w chmurze. Gdy zapisujesz dane w pliku za pomocą interfejsu File System Access API, zapisywanie nie odbywa się w miejscu docelowym, ale przy użyciu pliku tymczasowego. Sam plik nie jest modyfikowany, chyba że przejdzie wszystkie te kontrole bezpieczeństwa. Jak można się domyślać, takie działanie sprawia, że operacje na plikach są stosunkowo powolne, mimo że w miarę możliwości wprowadzane są ulepszenia, np. w systemie macOS. Każde wywołanie write()
jest niezależne, więc w tle otwiera plik, wyszukuje podane 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 mip są wstępnie obliczonymi, zoptymalizowanymi sekwencjami obrazów, z których każdy jest reprezentacją poprzedniego obrazu o stopniowo niższej rozdzielczości. Dzięki temu wiele operacji, takich jak powiększanie, jest szybszych. Jak więc aplikacje internetowe mogą korzystać z zalet plików bezpłatnie związanych z przetwarzaniem plików w internecie? Odpowiedzią jest prywatny system plików pochodzenia.
Widoczny dla użytkownika system plików a system plików prywatnych 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 i w którym można odczytywać, zapisywać, przenosić i zmieniać nazwy plików i folderów, źródłowy prywatny system plików 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 dokładniej – prywatne dla pochodzenia witryny. Sprawdź pochodzenie strony, wpisując location.origin
w konsoli narzędzi deweloperskich. Na przykład źródłem strony https://developer.chrome.com/articles/
jest https://developer.chrome.com
(czyli część /articles
nie jest częścią źródła). Więcej informacji o teorii pochodzenia znajdziesz w artykule Wyjaśnienie pojęć „ta sama witryna” i „to samo pochodzenie”. Wszystkie strony, które mają to samo pochodzenie, mogą wyświetlać te same dane z 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 źródło ma własny, niezależny prywatny system plików, co oznacza, że prywatny system plików źródła https://developer.chrome.com
jest zupełnie inny niż np. system https://web.dev
. W systemie Windows katalogiem głównym systemu plików widocznego dla użytkownika jest C:\\
.
Odpowiednikiem w przypadku prywatnego systemu plików pochodzenia jest początkowo pusty katalog główny każdego pochodzenia, do którego dostęp uzyskuje się przez wywołanie metody asynchronicznej navigator.storage.getDirectory()
.
Porównanie systemu plików widocznego dla użytkownika i systemu plików prywatnych pochodzenia znajdziesz na tym diagramie. Diagram pokazuje, że poza katalogiem głównym wszystko inne jest koncepcyjnie takie samo, z hierarchią plików i folderów, które można organizować i rozmieszczać zgodnie z potrzebami dotyczącymi danych i pamięci.
Szczegóły dotyczące prywatnego systemu plików źródła
Podobnie jak inne mechanizmy pamięci w przeglądarce (np. localStorage lub IndexedDB), prywatny system plików pochodzenia podlega ograniczeniom dotyczącym limitu przeglądarki. Gdy użytkownik wyczyści wszystkie dane przeglądania lub wszystkie dane witryny, usunięty zostanie również prywatny system plików pochodzenia. Wywołaj navigator.storage.estimate()
i w obiekcie odpowiedzi sprawdź wpis usage
, aby zobaczyć, ile miejsca na dane zajmuje już Twoja aplikacja. Jest to podzielone według mechanizmu przechowywania w obiekcie usageDetails
, w którym należy zwrócić uwagę na wpis fileSystem
. Prywatny system plików pochodzenia nie jest widoczny dla użytkownika, więc nie wyświetlają się prośby o uprawnienia ani nie są przeprowadzane kontrole 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
System plików prywatnych pochodzenia można używać na 2 sposoby: na głównym wątku lub w Web Workerze. Web Workers nie mogą blokować wątku głównego, co oznacza, że w tym kontekście interfejsy API mogą być synchroniczne, co jest zwykle niedozwolone w wątku głównym. Interfejsy API synchroniczne mogą działać szybciej, 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 jak najszybszych operacji na plikach lub pracujesz z WebAssembly, przejdź do sekcji Używanie prywatnego systemu plików pochodzenia w Web Workerze. W przeciwnym razie możesz czytać dalej.
Korzystanie z systemu plików prywatnych pochodzenia w głównym wątku
Tworzenie nowych plików i folderów
Po utworzeniu folderu głównego utwórz pliki i foldery, korzystając odpowiednio z metod getFileHandle()
i getDirectoryHandle()
. Jeśli przekażesz wartość {create: true}
, plik lub folder zostanie utworzony, jeśli nie istnieje. Zbuduj 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 nazwę, możesz uzyskać 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 element File
, użyj metody getFile()
. Obiekt File
to konkretny 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 funkcje 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 za pomocą przesyłania strumieniowego
Przesyłaj strumieniowo dane do pliku, wywołując funkcję createWritable()
, która tworzy obiekt 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 odpowiednią metodę remove()
uchwytu pliku lub katalogu. Aby usunąć folder wraz ze wszystkimi podfolderami, przekaż opcję {recursive: true}
.
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
Jeśli znasz nazwę pliku lub folderu, który chcesz usunąć z katalogu, możesz też użyć metody removeEntry()
.
directoryHandle.removeEntry('my first nested file');
przenoszenia i zmieniania nazw plików i folderów,
Zmieniaj nazwy plików i folderów oraz przenoś je za pomocą metody move()
. Przenoszenie i zmiana nazwy mogą nastąpić razem lub oddzielnie.
// 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 argument FileSystemHandle
. Aby uzyskać pełną ścieżkę do 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 dwa uchwyty plików lub folderów wskazują ten sam plik lub folder
Czasami masz 2 uchwyty i nie wiesz, czy wskazują one ten sam plik lub folder. Aby sprawdzić, czy tak jest, użyj metody isSameEntry()
.
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
Wyświetlanie zawartości folderu
FileSystemDirectoryHandle
to iterator asynchroniczny, po którym iterujesz za pomocą pętli for await…of
. Jako iterator asynchroniczny obsługuje też metody entries()
, values()
i keys()
, które możesz wybrać w zależności od tego, jakich informacji potrzebujesz:
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
Obsługa pętli i funkcji asynchronicznych w połączeniu z rekurencją może być trudna. Poniższa funkcja może posłużyć jako punkt wyjścia do wyświetlania zawartości folderu i wszystkich jego podfolderów, w tym wszystkich plików i ich rozmiarów. Jeśli nie potrzebujesz rozmiarów plików, możesz uprościć funkcję, nie przekazując obietnicy handle.getFile()
, ale bezpośrednio handle
w miejscu, w którym jest napisane directoryEntryPromises.push
.
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;
};
Korzystanie z prywatnego systemu plików pochodzenia w procesie roboczym
Jak wspomnieliśmy wcześniej, Web Workers nie mogą blokować głównego wątku, 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 uzyskasz uchwyt dostępu synchronicznego, będziesz mieć 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 z określonym przesunięciem, i zwraca liczbę zapisanych bajtów. Sprawdzanie 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 z określonym przesunięciem.truncate()
: zmienia rozmiar pliku na podany.flush()
: zapewnia, że zawartość pliku zawiera wszystkie zmiany wprowadzone za pomocąwrite()
.close()
: zamyka uchwyt dostępu.
Oto przykład, w którym użyto wszystkich wymienionych powyżej metod.
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 źródłowych do systemu plików widocznego dla użytkownika nie jest możliwe, ale możesz je kopiować. Interfejs showSaveFilePicker()
jest dostępny tylko w wątku głównym, a nie w wątku roboczym, więc pamiętaj, aby uruchamiać w nim 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. Powyższy zrzut ekranu z sekcji Tworzenie nowych plików i folderów pochodzi bezpośrednio z rozszerzenia.
Po zainstalowaniu rozszerzenia otwórz Narzędzia deweloperskie w Chrome i kliknij kartę OPFS Explorer. Możesz już sprawdzić hierarchię plików. Zapisuj pliki z oryginalnego prywatnego systemu plików w systemie plików widocznym dla użytkownika, klikając nazwę pliku, a usuwaj pliki i foldery, klikając ikonę kosza.
Prezentacja
Zobacz, jak działa prywatny system plików pochodzenia (jeśli zainstalujesz rozszerzenie OPFS Explorer), w demonstracji, która używa go jako backendu bazy danych SQLite skompilowanej do WebAssembly. Zapoznaj się z kodem źródłowym w Glitchu. Zwróć uwagę, że wersja osadzona poniżej nie korzysta z backendu prywatnego systemu plików pochodzenia (ponieważ element iframe jest międzyźródłowy), ale gdy otworzysz wersję demonstracyjną w osobnej karcie, będzie z niego korzystać.
Podsumowanie
Pochodny prywatny system plików, zgodnie ze specyfikacją WHATWG, ukształtował sposób, w jaki używamy plików w internecie i wchodzimy z nimi w interakcje. Umożliwia to nowe przypadki użycia, które były niemożliwe do osiągnięcia w przypadku systemu plików widocznego dla użytkownika. Wszyscy główni dostawcy przeglądarek – Apple, Mozilla i Google – są zaangażowani w ten projekt i mają wspólną wizję. Tworzenie prywatnego systemu plików pochodzenia to w dużej mierze wspólne przedsięwzięcie, a opinie deweloperów i użytkowników są niezbędne do jego rozwoju. Stale ulepszamy ten standard, dlatego zachęcamy do przesyłania opinii na temat repozytorium whatwg/fs w formie zgłoszeń lub próśb o scalenie.
Powiązane artykuły
- Specyfikacja standardu systemu plików
- Standardowe repozytorium systemu plików
- Post na blogu WebKit na temat interfejsu File System API z systemem Origin Private File System
- Rozszerzenie OPFS Explorer
Podziękowania
Ten artykuł został sprawdzony przez Austina Sully’ego, Etienne’a Noëla i Rachel Andrew. Baner powitalny: Christina Rumpf, Unsplash.