Prywatny system plików źródła

Standard systemu plików wprowadza prywatny system plików pochodzenia (OPFS) jako punkt końcowy pamięci, który jest prywatny dla pochodzenia strony i niewidoczny dla użytkownika. Zapewnia on opcjonalny dostęp do specjalnego rodzaju pliku, który jest w wysokim stopniu zoptymalizowany pod kątem wydajności.

Obsługa przeglądarek

Prywatny system plików pochodzenia jest obsługiwany przez nowoczesne przeglądarki i jest standardem File System Living Standard zdefiniowanym przez Web Hypertext Application Technology Working Group (WHATWG).

Obsługa przeglądarek

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

Źródło

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ć w eksploratorze plików systemu operacyjnego. Na przykład w systemie Windows użytkownik o imieniu Tomasz może mieć listę zadań 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. W systemie Windows litera „C” oznacza katalog główny dysku.

tradycyjny sposób pracy z plikami w internecie;

Aby edytować listę zadań w aplikacji internetowej, wykonaj te czynności:

  1. Użytkownik przesyła plik na serwer lub otwiera go na kliencie za pomocą <input type="file">.
  2. Użytkownik wprowadza zmiany, a następnie pobiera utworzony plik z wstrzykniętym <a download="ToDo.txt>, który został dodany programowo click() za pomocą kodu JavaScript.
  3. Aby otwierać foldery, używasz specjalnego atrybutu w <input type="file" webkitdirectory>, który pomimo swojej zastrzeżonej nazwy jest praktycznie obsługiwany przez wszystkie przeglądarki.

Nowoczesny sposób pracy z plikami w internecie

Ten proces nie odzwierciedla tego, jak użytkownicy postrzegają edycję plików, i oznacza, że użytkownicy pobierają kopie plików wejściowych. Dlatego w interfejsie File System Access API wprowadzono 3 metody selektora: showOpenFilePicker(), showSaveFilePicker()showDirectoryPicker(), które działają zgodnie z nazwą. Umożliwiają one przepływ w taki sposób:

  1. Otwórz ToDo.txt za pomocą showOpenFilePicker() i pobierz obiekt FileSystemFileHandle.
  2. Z obiektu FileSystemFileHandle pobierz File, wywołując metodę getFile() uchwytu pliku.
  3. Zmodyfikuj plik, a następnie wywołaj requestPermission({mode: 'readwrite'}).
  4. Jeśli użytkownik zaakceptuje prośbę o przyznanie uprawnień, zapisz zmiany w pierwotnym pliku.
  5. Możesz też zadzwonić showSaveFilePicker() i pozwolić użytkownikowi wybrać nowy plik. (jeśli użytkownik wybierze wcześniej otwarty plik, jego zawartość zostanie zastąpiona). W przypadku powtarzanego zapisywania możesz zachować uchwyt pliku, aby nie wyświetlać ponownie okna zapisywania.

Ograniczenia dotyczące pracy z plikami w internecie

Pliki i foldery dostępne za pomocą tych metod znajdują się w systemie plików, który można nazwać widocznym dla użytkownika. Pliki zapisane z sieci, a w szczególności pliki wykonywalne, są oznaczone oznaczeniem sieci, więc system operacyjny może wyświetlić dodatkowe ostrzeżenie przed wykonaniem potencjalnie niebezpiecznego pliku. Dodatkowo pliki pobierane z internetu są chronione przez Bezpieczne przeglądanie, które w ramach uproszczenia i w kontekście tego artykułu można traktować jako skanowanie w chmurze pod kątem wirusów. Podczas zapisywania danych do pliku za pomocą interfejsu File System Access API zapisywanie nie odbywa się na miejscu, ale w pliku tymczasowym. Plik nie jest modyfikowany, chyba że przejdzie wszystkie te kontrole bezpieczeństwa. Jak możesz sobie wyobrazić, powoduje to stosunkowo wolne działanie operacji na plikach, mimo że w miarę możliwości stosujemy ulepszenia, na przykład w systemie macOS. Mimo to każde wywołanie write() jest samodzielne, więc pod spodem otwiera plik, przeszukuje go do podanego przesunięcia i w końcu zapisuje dane.

Pliki jako podstawa przetwarzania

Jednocześnie pliki są świetnym sposobem na rejestrowanie danych. Na przykład SQLite przechowuje całe bazy danych w pojedynczym pliku. Innym przykładem są mapy MIPMAP używane do przetwarzania obrazów. Mipmapy to wstępnie obliczone, zoptymalizowane sekwencje obrazów, z których każdy jest reprezentacją poprzedniego obrazu w coraz niższej rozdzielczości, co przyspiesza wiele operacji, np. powiększanie. Jak aplikacje internetowe mogą korzystać z zalet plików, ale bez kosztów wydajności związanych z przetwarzaniem plików w internecie? Odpowiedź to system plików prywatnych źródła.

System plików prywatnych widoczny dla użytkownika a system plików prywatnych źródła

W przeciwieństwie do widocznego dla użytkowników systemu plików, który można przeglądać za pomocą eksploratora plików systemu operacyjnego, a w którym można czytać, zapisywać, przenosić i zmieniać nazwy plików i folderów, prywatny system plików nie jest przeznaczony do wyświetlania przez użytkowników. Jak sama nazwa wskazuje, pliki i foldery w prywatnym systemie plików źródła są prywatne, a dokładniej prywatne dla źródła witryny. Aby dowiedzieć się, skąd pochodzi strona, wpisz location.origin w konsoli DevTools. Na przykład źródłem strony https://developer.chrome.com/articles/ jest https://developer.chrome.com (czyli część /articles nie należy do źródła). Więcej informacji o teorii pochodzenia znajdziesz w artykule „Pojęcie „ta sama witryna” i „ten sam zasób”. Wszystkie strony z tym samym pochodzeniem mogą wyświetlać dane prywatnego systemu plików tego samego pochodzenia, więc https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ może zobaczyć te same szczegóły co w poprzednim przykładzie. Każda usługa ma własny niezależny system plików prywatnych, co oznacza, że system plików prywatnych usługi https://developer.chrome.com jest całkowicie inny niż system plików prywatnych usługi https://web.dev. W systemie Windows katalog główny widocznego dla użytkownika systemu plików to C:\\. Odpowiednikiem prywatnego systemu plików źródła jest początkowo pusty katalog główny dla każdego źródła, do którego można uzyskać dostęp przez wywołanie asynchronicznej metody navigator.storage.getDirectory(). Porównanie widocznego dla użytkownika systemu plików i prywatnego systemu plików pochodzenia przedstawia diagram poniżej. Z diagramu wynika, że poza katalogiem głównym wszystko jest zasadniczo takie samo, z hierarchią plików i folderów, które można porządkować i umieszczać w zależności od potrzeb związanych z danymi i magazynem.

Schemat pokazujący widoczny dla użytkownika system plików i prywatny system plików pochodzenia z dwoma przykładowymi hierarchiami plików. Punkt wejścia do widocznego dla użytkownika systemu plików to symboliczny dysk twardy, a punkt wejścia do prywatnego systemu plików pochodzenia to wywołanie metody „navigator.storage.getDirectory”.

Szczegóły prywatnego systemu plików źródła

Podobnie jak inne mechanizmy przechowywania w przeglądarce (np. localStorage czy 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 witryn, zostanie również usunięty prywatny system plików źródła. Wywołaj funkcję navigator.storage.estimate(), a w otrzymanym obiekcie odpowiedzi sprawdź wpis usage, aby zobaczyć, ile miejsca zajmuje już Twoja aplikacja. Dane te są podzielone według mechanizmu magazynowania w obiekcie usageDetails, w którym należy zwrócić uwagę na wpis fileSystem. Ponieważ prywatny system plików źródła jest niewidoczny dla użytkownika, nie pojawiają się żadne prośby o przyznanie uprawnień ani nie są przeprowadzane żadne kontrole Bezpiecznego przeglądania.

Uzyskiwanie dostępu do katalogu głównego

Aby uzyskać dostęp do katalogu głównego, uruchom to polecenie. W efekcie otrzymujesz pusty identyfikator katalogu, a ściślej mówiąc 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: w głównym wątku lub w Web Workerze. Elementy Web Worker 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. Interfejsy API synchroniczne mogą być szybsze, ponieważ nie trzeba w nich 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 operacji na plikach lub pracujesz z WebAssembly, przejdź do sekcji Używanie prywatnego systemu plików pochodzenia w Web Workerze. W przeciwnym razie czytaj dalej.

Użyj prywatnego systemu plików źródła w wątku głównym.

tworzyć nowe pliki i foldery.

Po utworzeniu folderu głównego utwórz pliki i foldery odpowiednio za pomocą metod getFileHandle() i getDirectoryHandle(). Jeśli podasz parametr {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 wyjścia.

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});

Wynikowa hierarchia plików z poprzedniego przykładu kodu.

Dostęp do istniejących plików i folderów

Jeśli znasz ich nazwy, możesz uzyskać dostęp do wcześniej utworzonych plików i folderów, wywołując metody getFileHandle() lub getDirectoryHandle(), 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 to plik w systemie plików. Aby uzyskać powiązany element File, użyj metody getFile(). Obiekt File jest określonym rodzajem obiektu Blob i może być używany w dowolnym kontekście, w którym można użyć obiektu Blob. W szczególności funkcje FileReader, URL.createObjectURL(), createImageBitmap()XMLHttpRequest.send() akceptują zarówno Blobs, jak i Files. Uzyskanie FileFileSystemFileHandle „uwalnia” dane, dzięki czemu możesz uzyskać do nich dostęp i uzyskać do nich dostęp w widocznym dla użytkownika systemie plików.

const file = await fileHandle.getFile();
console.log(await file.text());

Zapisywanie do pliku przez strumieniowanie

Przesyłanie danych do pliku przez wywołanie funkcji createWritable(), która tworzy FileSystemWritableFileStream, do którego następnie write() zawartość. Na koniec musisz close() przesyłanie strumieniowe.

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

Usuwanie plików i folderów przez wywołanie odpowiedniej metody remove() pliku lub katalogu. Aby usunąć folder wraz ze wszystkimi podfolderami, podaj opcję {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Jeśli znasz nazwę pliku lub folderu, który chcesz usunąć, w katalogu, użyj metody removeEntry().

directoryHandle.removeEntry('my first nested file');

Przenoszenie i zmienianie 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ą być wykonywane 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 referencyjnego, użyj metody resolve(), przekazując jej jako argument wartość FileSystemHandle. Aby uzyskać pełną ścieżkę do pliku lub folderu w prywatnym systemie plików źródłowego, użyj katalogu głównego jako katalogu referencyjnego uzyskanego za pomocą navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Sprawdź, czy dwa uchwyty plików lub folderów wskazują na ten sam plik lub folder

Czasami masz 2 elementy uchwytu i nie wiesz, czy wskazują one ten sam plik lub folder. Aby to sprawdzić, użyj metody isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Wyświetlanie zawartości folderu

FileSystemDirectoryHandle to asynchroniczny iterator, który jest iterowany za pomocą pętli for await…of. Jako iterator asynchroniczny obsługuje on też metody entries(), values()keys(), z których możesz wybierać 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()) {}

Rekursywnie wyświetlać zawartość folderu i wszystkich podfolderów.

Praca z asynchronicznymi pętlami i funkcjami w połączeniu z rekursją jest podatna na błędy. Podana niżej funkcja może posłużyć jako punkt wyjścia do wyświetlenia 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ę, tak aby zamiast directoryEntryPromises.push pojawiło się handle.getFile(), a nie handle.getFile().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 już wspomnieliśmy, zadania wątekowe nie mogą blokować głównego wątku, dlatego w tym kontekście dozwolone są metody synchroniczne.

Pobieranie uchwytu dostępu synchronicznego

Punkt wejścia do najszybszych możliwych operacji na plikach to FileSystemSyncAccessHandle, uzyskany z normalnego FileSystemFileHandle przez wywołanie createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Metody synchroniczne na miejscu dotyczące plików

Gdy masz uchwyt synchronicznego dostępu, możesz korzystać z szybkich metod synchronicznego zapisu plików, 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. 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 określony.
  • flush(): zapewnia, że zawartość pliku zawiera wszystkie modyfikacje wprowadzone za pomocą write().
  • close(): zamyka uchwyt dostępu.

Oto przykład, który wykorzystuje wszystkie wymienione wyżej 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 źródłowego do widocznego dla użytkownika systemu plików

Jak wspomnieliśmy wcześniej, przenoszenie plików z prywatnego systemu plików źródłowego do widocznego dla użytkownika systemu plików nie jest możliwe, ale możesz je kopiować. Funkcja showSaveFilePicker() jest dostępna tylko w wątku głównym, a nie w wątku roboczym, więc pamiętaj, aby uruchomić kod w tym wątku.

// 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 źródłowego

Dopóki nie zostanie dodana obsługa wbudowanych DevTools (patrz crbug/1284595), do debugowania prywatnego systemu plików origin używaj rozszerzenia OPFS Explorer do Chrome. Zrzut ekranu z sekcji Tworzenie nowych plików i folderów pochodzi bezpośrednio z rozszerzenia.

Rozszerzenie OPFS Explorer w Narzędziach deweloperskich w Chrome w Chrome Web Store.

Po zainstalowaniu rozszerzenia otwórz Narzędzia deweloperskie w Chrome i wybierz kartę OPFS Explorer, aby przejrzeć hierarchię plików. Aby zapisać pliki z prywatnego systemu plików źródłowego w systemie plików widocznym dla użytkownika, kliknij nazwę pliku, a aby usunąć pliki i foldery, kliknij ikonę kosza.

Prezentacja

Aby zobaczyć, jak działa prywatny system plików origin (jeśli zainstalujesz rozszerzenie OPFS Explorer), możesz obejrzeć prezentację, w której jest on używany jako backend dla bazy danych SQLite skompilowanej do WebAssembly. Sprawdź kod źródłowy na Glitch. Poniżej widać, że wersja umieszczona w ramce nie korzysta z systemu plików prywatnych źródła (ponieważ element iframe jest między domenami), ale gdy otworzysz wersję demonstracyjną na osobnej karcie, będzie ona z niego korzystać.

Podsumowanie

Pierwotny prywatny system plików określony przez WHATWG wpłynął na sposób, w jaki korzystamy z plików i interagujemy z nimi w internecie. Umożliwiło to nowe przypadki użycia, które były niemożliwe do osiągnięcia przy użyciu widocznego dla użytkownika systemu plików. Wszyscy główni dostawcy przeglądarek: Apple, Mozilla i Google, są zaangażowani w ten projekt i dzielą wspólną wizję. Tworzenie prywatnego systemu plików pochodzenia to praca zespołowa, a opinie programistów i użytkowników są niezbędne do jego rozwoju. Cały czas ulepszamy standard, dlatego prosimy o przesyłanie opinii na temat repozytorium whatwg/fs w postaci zgłoszeń o błędach lub pull requestów.

Podziękowania

Ten artykuł został sprawdzony przez Austina Sully, Etienne Noëla i Rachel Andrew. Baner powitalny autorstwa Christina RumpfUnsplash.