Prywatny system plików źródła

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.

Browser Support

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

Source

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:

  1. Użytkownik przesyła plik na serwer lub otwiera go na kliencie za pomocą elementu <input type="file">.
  2. Użytkownik wprowadza zmiany, a następnie pobiera plik wynikowy za pomocą wstrzykniętego elementu <a download="ToDo.txt> , który programowo click() za pomocą JavaScript.
  3. 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:

  1. Otwórz ToDo.txt za pomocą showOpenFilePicker(), i uzyskaj obiekt FileSystemFileHandle.
  2. Z obiektu FileSystemFileHandle uzyskaj File, wywołując metodę getFile() uchwytu pliku.
  3. Zmodyfikuj plik, a następnie wywołaj requestPermission({mode: 'readwrite'}) w uchwycie.
  4. Jeśli użytkownik zaakceptuje prośbę o uprawnienia, zapisz zmiany w oryginalnym pliku.
  5. 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.

Diagram przedstawiający system plików widoczny dla użytkownika i system plików prywatnych pochodzenia z 2 przykładowymi hierarchiami plików.
Punktem wejścia do systemu plików widocznego dla użytkownika jest symboliczny dysk twardy, a punktem wejścia do prywatnego systemu plików pochodzenia jest wywołanie metody 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});

Hierarchia plików wynikająca z wcześniejszego przykładowego kodu.

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.

Rozszerzenie OPFS Explorer do narzędzi deweloperskich w Chrome w Chrome Web Store.

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.

Podziękowania

Ten dokument został sprawdzony przez Austina Sully'ego, Etienne'a Noëla i Rachel Andrew.