Prywatny system plików źródła

W ramach standardu systemu plików wprowadza prywatny system plików punktu początkowego (OPFS) jako punkt końcowy pamięci masowej, który jest prywatny względem źródła strony i niewidoczny dla użytkownika. Zapewnia opcjonalny dostęp do specjalnego rodzaju plików, które są wysoce zoptymalizowane pod kątem wydajności.

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, czyli 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. „C:” w systemie Windows reprezentuje 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 sposobu, w jaki użytkownicy myślą o edytowaniu 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 widocznym dla użytkowników. Pliki zapisane z internetu, a w szczególności pliki wykonywalne, są oznaczone znakiem sieci, dlatego system operacyjny może zobaczyć dodatkowe ostrzeżenie przed wykonaniem potencjalnie niebezpiecznego pliku. Dodatkową funkcją zabezpieczeń jest to, że pliki pobierane z Internetu są również chronione przez Bezpieczne przeglądanie, które w celu uproszczenia i w kontekście tego artykułu można traktować jako skanowanie w chmurze pod kątem wirusów. Gdy zapisujesz dane w pliku przy użyciu interfejsu File System Access API, zapisy nie są w miejscu, tylko korzystają z pliku tymczasowego. Sam plik nie jest modyfikowany, dopóki nie przejdzie wszystkich tych kontroli bezpieczeństwa. Jak łatwo się domyślić, operacje na plikach są dość powolne, pomimo ulepszeń wprowadzonych tam, gdzie to możliwe, na przykład w 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 jednym 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.

Widok systemu plików prywatnych źródła w porównaniu z widokiem użytkownika

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 konkretnie prywatne dla źródła witryny. Aby dowiedzieć się, skąd pochodzi strona, wpisz location.origin w konsoli DevTools. Na przykład źródło strony https://developer.chrome.com/articles/ to 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 element”. 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żdej usługi, 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. Prywatny system plików źródła nie jest widoczny dla użytkownika, dlatego nie pojawia się prośba o przyznanie uprawnień ani 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. Synchroniczne interfejsy API mogą być szybsze, ponieważ nie wymagają przetwarzania obietnic. 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 plik lub folder nie istnieje, wartość {create: true} zostanie utworzona. 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 nazwę pliku lub folderu, możesz uzyskać dostęp do wcześniej utworzonych plików i folderów, wywołując metodę 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 odczytu

FileSystemFileHandle to plik w systemie plików. Aby uzyskać powiązany identyfikator 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 FileReader, URL.createObjectURL(), createImageBitmap() i XMLHttpRequest.send() akceptują zarówno te 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 systemie plików widocznym dla użytkownika.

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

Zapisz do pliku przez strumieniowanie

Przesyłaj dane strumieniowo do pliku, wywołując metodę createWritable(). Tworzy ona FileSystemWritableFileStream, a następnie write() jej 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

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 w katalogu znasz nazwę pliku lub folderu do usunięcia, możesz użyć metody removeEntry().

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

Przenoszenie i zmienianie nazw plików i folderów

Zmień nazwy plików i folderów oraz przenieś je, korzystając z 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()) {}

cyklicznie wyświetlanie zawartości folderu i wszystkich podfolderów;

Praca z asynchronicznymi pętlami i funkcjami w połączeniu z rekursją jest podatna na błędy. Poniżej podana 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().

  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ć wątku głównego, dlatego w tym kontekście dozwolone są metody synchroniczne.

Uzyskiwanie synchronicznego uchwytu dostępu

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

Synchroniczne lokalne metody tworzenia plików

Gdy masz uchwyt dostępu synchronicznego, uzyskujesz dostęp do szybkich metod plików na 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 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 na system plików widoczny dla użytkownika

Jak wspomnieliśmy powyżej, przeniesienie plików z prywatnego systemu plików źródłowego do systemu plików widocznych dla użytkowników nie jest możliwe, ale możesz je skopiować. Ponieważ interfejs showSaveFilePicker() jest widoczny tylko w wątku głównym, ale nie w wątku instancji roboczej, 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 źródłowego

Dopóki nie dodasz wbudowanej obsługi Narzędzi deweloperskich (patrz crbug/1284595), debuguj prywatny system plików źródła za pomocą rozszerzenia do Chrome OPFS Explorer. Zrzut ekranu powyżej z sekcji Tworzenie nowych plików i folderów został wykonany 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

Zobacz, jak działa prywatny system plików źródła (jeśli zainstalujesz rozszerzenie OPFS Explorer) w wersji demonstracyjnej, która używa go jako backendu 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 backendowego prywatnego systemu plików ź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. Stale udoskonalamy i ulepszamy standard, dlatego zachęcamy do przesyłania opinii o problemach i żądaniach pull w repozytorium Whatwg/fs.

Podziękowania

Ten artykuł napisali Austin Sully, Etienne Noël i Rachel Andrew. Baner powitalny autorstwa Christina RumpfUnsplash.