Prywatny system plików źródła

Standard systemu plików wprowadza prywatny system plików źródła (OPFS) jako punkt końcowy pamięci masowej, który jest prywatny dla źródła strony i nie jest widoczny dla użytkownika. Zapewnia opcjonalny dostęp do specjalnego rodzaju pliku, który jest wysoce zoptymalizowany pod kątem wydajności.

Obsługiwane przeglądarki

Prywatny system plików źródła jest obsługiwany przez nowoczesne przeglądarki i ustandaryzowany przez Web Hypertext Application Technology Technology Group (WhatWG) w raporcie File System Living Standard.

Obsługa przeglądarek

  • 86
  • 86
  • 111
  • 15.2

Źródło

Motywacja

Gdy myślisz o plikach na komputerze, prawdopodobnie chodzi Ci o hierarchię plików, czyli uporządkowane w folderach foldery, które można przeglądać za pomocą eksploratora plików systemu operacyjnego. Na przykład w systemie Windows użytkownika o imieniu Tomek może pojawić się lista Do zrobienia w języku: 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ę Zadania do wykonania w aplikacji internetowej, wygląda to w następujący sposób:

  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 wynikowy plik z wstrzykniętym elementem <a download="ToDo.txt>, który możesz programować click() przy użyciu JavaScriptu.
  3. Do otwierania folderów służy atrybut specjalny w <input type="file" webkitdirectory>, który pomimo zastrzeżonej nazwy jest praktycznie uniwersalną obsługą przeglądarek.

Nowoczesny sposób pracy z plikami w internecie

Ten proces nie odzwierciedla sposobu, w jaki użytkownicy myślą o edytowaniu plików, ponieważ oznacza, że użytkownicy otrzymują pobrane kopie plików wejściowych. W związku z tym w interfejsie File System Access API wprowadzono 3 metody selektora – showOpenFilePicker(), showSaveFilePicker() i showDirectoryPicker(), które działają dokładnie tak, jak wskazuje na to ich nazwa. Włączają one przepływ w taki sposób:

  1. Otwórz ToDo.txt w narzędziu 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'}) na nicku.
  4. Jeśli użytkownik zaakceptuje prośbę o przyznanie uprawnień, zapisz zmiany w pierwotnym pliku.
  5. 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 powtarzających się operacji zapisywania możesz zachować uchwyt pliku, dzięki czemu nie trzeba będzie ponownie wyświetlać okna zapisywania.

Ograniczenia 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 widocznym dla użytkowników. Pliki zapisane z internetu, a w szczególności pliki wykonywalne, są oznaczone znacznikiem z internetu, dlatego przed wykonaniem potencjalnie niebezpiecznego pliku może pojawić się dodatkowe ostrzeżenie. Pliki pobierane z internetu są dodatkowo chronione przez funkcję Bezpieczne przeglądanie, która – dla uproszczenia – w kontekście tego artykułu – można ją traktować jako skanowanie w poszukiwaniu wirusów działające w chmurze. Gdy zapisujesz dane w pliku przy użyciu interfejsu File System Access API, operacje zapisu nie są w danym miejscu, ale są używane pliki tymczasowe. Sam plik nie jest modyfikowany, chyba że przejdzie wszystkie te kontrole zabezpieczeń. Jak można sobie wyobrazić, sposób pracy na plikach jest stosunkowo powolny, mimo wprowadzenia ulepszeń tam, gdzie jest to możliwe, na przykład w macOS. Nadal każde wywołanie write() jest samodzielne, więc pod oknem otwiera plik, wyszukuje określone przesunięcie, a na koniec zapisuje dane.

Pliki jako podstawę przetwarzania

Jednocześnie pliki to doskonały sposób na rejestrowanie danych. Na przykład SQLite przechowuje całe bazy danych w jednym pliku. Innym przykładem są mipmapy używane do przetwarzania obrazów. Mipmaps to wstępnie obliczone, zoptymalizowane sekwencje obrazów, z których każdy stanowi reprezentację poprzedniej wersji o stopniowo niższej rozdzielczości, dzięki czemu wiele operacji, takich jak powiększanie, jest szybsze. W jaki sposób aplikacje internetowe mogą skorzystać z zalet plików, ale bez kosztów związanych z wydajnością internetowego przetwarzania plików? Odpowiedź to prywatny system plików origin.

Porównanie systemu plików widocznego dla użytkownika z prywatnym systemem plików źródła

W przeciwieństwie do widocznego dla użytkowników systemu plików przeglądanego za pomocą eksploratora plików systemu operacyjnego, w którym pliki i foldery można odczytywać, zapisywać, przenosić i zmieniać, system plików origin nie jest więc widoczny dla użytkowników. Pliki i foldery w prywatnym systemie plików origin, jak sugeruje to nazwa, są prywatne i konkretniej chronione w źródle witryny. Poznaj pochodzenie strony, wpisując w konsoli Narzędzi deweloperskich location.origin. Na przykład pochodzenie strony https://developer.chrome.com/articles/ to https://developer.chrome.com (czyli część /articles nie jest częścią źródła). Teorię pochodzenia możesz dowiedzieć się z artykułu „Ta sama strona” i „ta sama strona”. Wszystkie strony o tym samym źródle mogą widzieć te same dane prywatnego systemu plików źródła, więc https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ widzi te same szczegóły co w poprzednim przykładzie. Każde źródło ma własny, niezależny system plików prywatnych, co oznacza, że system plików źródła prywatnego w https://developer.chrome.com jest zupełnie inny niż system, na przykład https://web.dev. W systemie Windows katalog główny systemu plików widocznego dla użytkowników to C:\\. Odpowiednikiem dla prywatnego systemu plików źródła jest początkowo pusty katalog główny dla każdego źródła, do którego dostęp jest uzyskiwany przez wywołanie metody asynchronicznej navigator.storage.getDirectory(). Na diagramie poniżej znajdziesz porównanie systemu plików widocznego dla użytkownika i prywatnego systemu plików źródła. Na diagramie widać, że poza katalogiem głównym wszystkie inne elementy są koncepcyjnie takie same, z hierarchią plików i folderów, które można uporządkować i uporządkować zgodnie z potrzebami dotyczącymi danych i miejsca na dane.

Schemat systemu plików widocznego dla użytkowników i prywatnego systemu plików źródła z 2 przykładowymi hierarchiami plików. Punktem wejścia dla systemu plików widocznego dla użytkowników jest symboliczny dysk twardy, a punkt wejścia dla prywatnego systemu plików źródła jest wywoływany metodą „navigator.storage.getDirectory”.

Szczegółowe informacje o prywatnym systemie plików źródła

Tak jak inne mechanizmy pamięci masowej w przeglądarce (np. localStorage czy IndexedDB), prywatny system plików źródła podlega ograniczeniom dla przeglądarki. Gdy użytkownik wyczyści wszystkie dane przeglądania lub dane witryn, prywatny system plików także zostanie usunięty. Wywołaj metodę navigator.storage.estimate() i w wywołanym obiekcie odpowiedzi zobacz wpis usage, aby zobaczyć, ile miejsca używa już aplikacja z podziałem według mechanizmu pamięci masowej w obiekcie usageDetails, gdzie chcesz przyjrzeć się wpisowi fileSystem. Prywatny system plików źródła nie jest widoczny dla użytkownika, dlatego nie pojawiają się prośby o przyznanie uprawnień ani kontrole Bezpiecznego przeglądania.

Uzyskiwanie dostępu do katalogu głównego

Aby uzyskać dostęp do katalogu głównego, uruchom następujące polecenie. W rezultacie uzyskasz pusty uchwyt katalogu, a konkretnie 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 instancja robocza

Z prywatnego systemu plików źródła można korzystać na 2 sposoby: w wątku głównym lub w instancji Web Worker. Instancje robocze nie mogą zablokować wątku głównego, co oznacza, że w tym kontekście interfejsy API mogą być synchroniczne, czyli wzorce zwykle niedozwolone w wątku głównym. Synchroniczne interfejsy API mogą być szybsze, ponieważ nie muszą zajmować się obietnicami. 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 najszybszego wykonywania operacji na plikach lub korzystasz z komponentu WebAssembly, przejdź od razu do sekcji Używanie prywatnego systemu plików źródła w panelu Web Worker. W przeciwnym razie możesz czytać dalej.

Używaj prywatnego systemu plików origin w wątku głównym

Tworzenie nowych plików i folderów

Gdy będziesz już mieć folder główny, utwórz pliki i foldery odpowiednio za pomocą metod getFileHandle() i getDirectoryHandle(). Dzięki przekazowi {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});

Wynikowa hierarchia plików z poprzedniego przykładowego 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 metodę 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');

Pobieram plik powiązany z nickiem pliku do odczytu

FileSystemFileHandle reprezentuje plik w systemie plików. Aby uzyskać powiązany File, użyj metody getFile(). Obiekt File to konkretny rodzaj obiektu Blob. Można go używać w dowolnym kontekście dostępnym dla Blob. W szczególności zasady FileReader, URL.createObjectURL(), createImageBitmap() i XMLHttpRequest.send() akceptują zarówno Blobs, jak i Files. Jeśli tak, uzyskanie File z FileSystemFileHandle „zwolni” dane, aby można było 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 strumieniowanie

Prześlij strumieniowo dane do pliku, wywołując metodę createWritable(), która tworzy FileSystemWritableFileStream, a następnie write() zawartość pliku. Na koniec musisz close() transmitować.

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

usuwać pliki i foldery, wywołując ich konkretną metodę remove(); Aby usunąć folder wraz ze wszystkimi podfolderami, prześlij opcję {recursive: true}.

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

Jeśli znasz nazwę pliku lub folderu do usunięcia w katalogu, możesz użyć metody removeEntry().

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

Przenoszenie plików i folderów oraz zmienianie ich nazw

Korzystając z metody move(), możesz zmieniać nazwy plików i folderów oraz przenosić je. 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 problemu ze ścieżką pliku lub folderu

Aby dowiedzieć się, gdzie znajduje się dany plik lub folder w stosunku do katalogu referencyjnego, użyj metody resolve(), przesyłając jako argument FileSystemHandle. Aby uzyskać pełną ścieżkę pliku lub folderu w prywatnym systemie plików źródła, użyj katalogu głównego jako katalogu referencyjnego uzyskanego za pomocą funkcji navigator.storage.getDirectory().

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

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

Czasami masz 2 nicki i nie wiadomo, czy wskazują one ten sam plik lub folder. Aby sprawdzić, czy tak jest w Twoim przypadku, użyj metody isSameEntry().

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

Wymienia zawartość folderu

FileSystemDirectoryHandle to asynchroniczny iterator, który jest powtarzany za pomocą pętli for await…of. Jako asynchroniczny iterator obsługuje też metody entries(), values() i keys(), spośród których możesz wybierać w zależności od tego, jakie informacje są Ci potrzebne:

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świetlaj zawartość folderu i wszystkich podfolderów

Radzenie sobie z pętlami asynchronicznymi i funkcjami połączonymi z rekurencją łatwo jest popełnić błąd. Poniższa funkcja może służyć jako punkt wyjścia dla 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ę, w miejscu oznaczonym jako directoryEntryPromises.push (bez przekazywania obietnicy handle.getFile(), a tylko na 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 origin w instancji Web Worker

Jak już wspomnieliśmy, instancje robocze nie mogą blokować wątku głównego, dlatego w tym kontekście metody synchroniczne są dozwolone.

Uzyskiwanie synchronicznego uchwytu dostępu

Punktem wejścia do najszybszych możliwych operacji na pliku jest FileSystemSyncAccessHandle. Pozyskany ze zwykłego FileSystemFileHandle przez wywołanie createSyncAccessHandle().

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

Synchroniczne metody przechowywania plików w miejscu

Dzięki synchronicznemu uchwytowi dostępu masz dostęp do szybkich, działających na miejscu metod przesyłania plików, które są synchroniczne.

  • getSize(): zwraca rozmiar pliku w bajtach.
  • write(): zapisuje zawartość bufora w pliku (opcjonalnie przy danym przesunięciu) i zwraca liczbę zapisanych bajtów. Sprawdzanie zwróconej liczby zapisanych bajtów umożliwia obiektom 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 rozmiar.
  • flush(): zawartość pliku zawiera wszystkie zmiany wprowadzone za pomocą write().
  • close(): zamyka uchwyt dostępu.

Oto przykład, w którym zastosowano wszystkie wymienione powyż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);

Skopiuj plik z prywatnego systemu plików origin do systemu plików widocznego dla użytkowników

Jak wspomnieliśmy powyżej, przeniesienie plików z prywatnego systemu plików źródła do systemu plików widocznego dla użytkowników jest niemożliwe, ale możesz kopiować pliki. Ponieważ etykieta showSaveFilePicker() jest widoczna tylko w wątku głównym, a nie w wątku instancji roboczej, pamiętaj o uruchomieniu kodu właśnie 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);
}

Debuguj prywatny system plików źródła

Dopóki nie dodasz wbudowanej obsługi Narzędzi deweloperskich (zobacz crbug/1284595), używaj rozszerzenia do Chrome OPFS Explorer, aby debugować prywatny system plików źródła. Powyższy zrzut ekranu z sekcji Tworzenie nowych plików i folderów pochodzi bezpośrednio z rozszerzenia.

Rozszerzenie OPFS Explorer do Chrome DevTools w Chrome Web Store.

Po zainstalowaniu rozszerzenia otwórz Narzędzia deweloperskie w Chrome, wybierz kartę Eksplorator OPFS, aby sprawdzić hierarchię plików. Zapisuj pliki z prywatnego systemu plików origin w widocznym dla użytkowników systemie plików, klikając nazwę pliku, a następnie usuwając pliki i foldery, klikając ikonę kosza.

Pokaz

Zobacz, jak działa prywatny system plików źródła (jeśli zainstalujesz rozszerzenie OPFS Explorer) w prezentacji, która używa go jako backendu dla bazy danych SQLite skompilowanej w WebAssembly. Zapoznaj się z kodem źródłowym w Glitch. Zwróć uwagę, że przedstawiona poniżej wersja nie korzysta z backendu prywatnego systemu plików źródła (ponieważ element iframe jest z innej domeny), ale gdy otworzysz wersję demonstracyjną w osobnej karcie, tak się stanie.

Podsumowanie

Prywatny system plików origin zdefiniowany przez organizację WhatWG ukształtował sposób, w jaki wykorzystujemy pliki w internecie i wchodzimy z nimi w interakcję. Udostępniła też nowe przypadki użycia, które nie były możliwe w przypadku systemu plików widocznego dla użytkowników. Wszyscy najwięksi dostawcy przeglądarek – Apple, Mozilla i Google – dołączają do programu i współtworzą wspólną wizję. Opracowanie prywatnego systemu plików origin w dużym stopniu wymaga współpracy, a opinie programistów i użytkowników mają kluczowe znaczenie dla jego postępu. W miarę ulepszania i ulepszania tego standardu zachęcamy do przesyłania opinii o repozytoriumwhatwg/fs w postaci problemów lub żądań pull.

Podziękowania

Ten artykuł napisali Austin Sully, Etienne Noël i Rachel Andrew. Baner powitalny od Christiny Rumpf w aplikacji Unsplash.