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.

Obsługa przeglądarek

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

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 chodzi Ci o 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 dla użytkownika Tomek jego lista zadań może znajdować się w tym 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 reprezentuje katalog główny dysku.

Tradycyjny sposób pracy z plikami w internecie

Aby edytować listę zadań w aplikacji internetowej, postępuj w następujący sposób:

  1. Użytkownik przesyła plik na serwer lub otwiera go na kliencie przy użyciu <input type="file">.
  2. Użytkownik wprowadza zmiany, a potem pobiera wynikowy plik z wstrzykniętym elementem <a download="ToDo.txt>, który automatycznie click() wykorzystujesz do obsługi JavaScriptu.
  3. Do otwierania folderów używasz atrybutu specjalnego w <input type="file" webkitdirectory>, który, mimo jego zastrzeżonej nazwy, ma praktycznie uniwersalną obsługę przeglądarek.

Nowoczesny sposób pracy z plikami w internecie

Ten proces nie jest reprezentatywny dla tego, jak użytkownicy postrzegają edytowanie plików i oznacza, że otrzymują pobrane kopie swoich plików wejściowych. W związku z tym w interfejsie File System Access API wprowadziliśmy 3 metody selektora – showOpenFilePicker(), showSaveFilePicker() i showDirectoryPicker() – które działają dokładnie tak, jak sugeruje to nazwa. Umożliwiają one następujący przepływ:

  1. Otwórz aplikację ToDo.txt w aplikacji showOpenFilePicker() i pobierz obiekt FileSystemFileHandle.
  2. Z obiektu FileSystemFileHandle uzyskaj File, wywołując metodę getFile() uchwytu pliku.
  3. Zmodyfikuj plik, a potem wywołaj requestPermission({mode: 'readwrite'}) w nicku.
  4. Jeśli użytkownik zaakceptuje prośbę o uprawnienia, 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 kolejnych zapisów możesz zachować uchwyt pliku, aby nie trzeba było ponownie wyświetlać okna zapisywania pliku.

Ograniczenia dotyczące pracy z plikami w internecie

Pliki i foldery dostępne przy użyciu 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. Jako dodatkową funkcję zabezpieczeń pliki uzyskane z internetu są również chronione przez funkcję Bezpieczne przeglądanie, która dla uproszczenia w kontekście tego artykułu może być traktowana jako skanowanie w poszukiwaniu wirusów działające w chmurze. 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. Możesz sobie wyobrazić, że ta praca sprawia, że operacje na plikach są stosunkowo powolne, pomimo ulepszeń wprowadzonych tam, gdzie to możliwe, na przykład w macOS. Mimo to każde wywołanie write() jest niezależne, więc umożliwia otwarcie pliku, przeszukanie danego przesunięcia i zapisanie danych.

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ą mipmapy używane do przetwarzania obrazów. Mipmapy to wstępnie wyliczane, zoptymalizowane sekwencje obrazów, z których każda w stopniowo zmniejsza się rozdzielczością poprzedniego obrazu, co przyspiesza wykonywanie wielu operacji, takich jak powiększanie. W jaki sposób aplikacje internetowe mogą korzystać z zalet plików, ale bez kosztów związanych z wydajnością przetwarzania plików w internecie? Wynik to prywatny system plików pierwotny.

System plików widocznych dla użytkowników a prywatny system plików punktu początkowego

W przeciwieństwie do systemu plików widocznego dla użytkowników przeglądanego w eksploratorze plików systemu operacyjnego, który umożliwia odczytywanie, zapisywanie, przenoszenie i zmienianie nazw plików oraz zmiana ich nazwy, użytkownicy nie widzą prywatnego systemu plików źródłowego. Jak sama nazwa wskazuje, pliki i foldery w prywatnym systemie plików źródła są prywatne i konkretnie należą do źródła witryny. Aby poznać pochodzenie strony, wpisz location.origin w konsoli Narzędzi deweloperskich. Na przykład źródło strony https://developer.chrome.com/articles/ jest https://developer.chrome.com (tzn. część /articles nie jest jej częścią). Więcej informacji o teorii źródeł znajdziesz w artykule Pojęcie „tej samej witryny” i „same-origin”. Wszystkie strony mające to samo źródło mogą wyświetlać te same dane prywatnego systemu plików punktu początkowego, 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żde źródło ma własny, niezależny system plików w źródle, co oznacza, że prywatny system plików źródła w systemie 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 widocznych dla użytkowników to C:\\. Odpowiednikiem prywatnego systemu plików punktu początkowego jest początkowo pusty katalog główny dla każdego punktu początkowego, do którego uzyskano dostęp przez wywołanie metody asynchronicznej navigator.storage.getDirectory() Porównanie systemu plików widocznego dla użytkowników z prywatnym systemem plików punktu początkowego znajdziesz na diagramie poniżej. Schemat pokazuje, że oprócz katalogu głównego wszystkie pozostałe elementy są koncepcyjnie takie same, a dodatkowo zawiera hierarchię plików i folderów, które można uporządkować i uporządkować odpowiednio do potrzeb danych i miejsca na dane.

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

Szczegóły dotyczące prywatnego systemu plików punktu początkowego

Tak jak inne mechanizmy pamięci masowej w przeglądarce (np. localStorage lub IndexedDB), prywatny system plików punktu początkowego podlega ograniczeniom limitów przeglądarki. Gdy użytkownik wyczyści wszystkie dane przeglądania lub wszystkie dane witryn, prywatny system plików punktu początkowego również zostanie usunięty. Wywołaj navigator.storage.estimate(), a w wywoływanym obiekcie odpowiedzi zobaczysz wpis usage, aby sprawdzić, ile miejsca na dane zużywa już aplikacja. Tabela jest podzielona według mechanizmu pamięci masowej w obiekcie usageDetails, gdzie chcesz przyjrzeć się temu wpisowi 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. 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 narzędzie Web Worker

Z prywatnego systemu plików punktu początkowego można używać na 2 sposoby: w wątku głównym lub w narzędziu Web Worker. Skrypty Web Workers nie mogą blokować wątku głównego, co oznacza, że w tym kontekście interfejsy API mogą być synchroniczne, czyli zasadniczo niedozwolony 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 możliwych operacji na plikach lub korzystasz z technologii WebAssembly, przejdź od razu do sekcji Używanie źródłowego systemu plików w instancji roboczej. W przeciwnym razie możesz czytać dalej.

Użyj prywatnego systemu plików punktu początkowego w wątku głównym

Tworzenie nowych plików i folderów

Gdy masz już folder główny, 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 przy użyciu 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 wcześniejszego przykładowego 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 reprezentuje plik w systemie plików. Aby uzyskać powiązaną właściwość File, użyj metody getFile(). Obiekt File to konkretny rodzaj obiektu Blob i można go użyć w dowolnym kontekście, w którym może zostać użyty Blob. W szczególności FileReader, URL.createObjectURL(), createImageBitmap() i XMLHttpRequest.send() akceptują zarówno te Blobs, jak i Files. Jeśli tak, uzyskanie File z „bezpłatnych” nagród FileSystemFileHandle danych, dzięki czemu możesz uzyskać do nich dostęp i udostępnić je w systemie plików widocznym dla użytkowników.

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

Zapisz w pliku przez strumieniowanie

Przesyłaj dane strumieniowo do pliku, wywołując metodę createWritable(). W ten sposób powstaje FileSystemWritableFileStream, a następnie write() jej zawartość. Na koniec musisz close() włączyć transmisję.

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

Usuń pliki i foldery, wywołując określoną metodę remove() odpowiadającą im plikowi lub uchwytowi katalogu. Aby usunąć folder ze wszystkimi podfolderami, przekaż 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 plików i folderów oraz zmienianie ich nazw

Zmienianie nazw i przenoszenie plików oraz folderów przy użyciu metody move() Przenoszenie i zmienianie nazw może odbywać się 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');

Rozpoznawanie ścieżki pliku lub folderu

Aby dowiedzieć się, gdzie znajduje się dany plik lub folder w odniesieniu do katalogu referencyjnego, użyj metody resolve(), przekazując mu FileSystemHandle jako argument. Aby uzyskać pełną ścieżkę do pliku lub folderu w prywatnym systemie plików punktu początkowego, 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ą ten sam plik lub folder

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

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

Wyświetlanie listy zawartości folderu

FileSystemDirectoryHandle to asynchroniczny iterator, który można powtarzać za pomocą pętli for await…of. Jako iterator asynchroniczny obsługuje również metody entries(), values() i 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;

W przypadku pętli i funkcji asynchronicznych w połączeniu z rekurencją łatwo jest się pomylić. Poniższa funkcja może sł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ę, stosując directoryEntryPromises.push, nie wypychając obietnicy handle.getFile(), a handle bezpośrednio.

  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 punktu początkowego w instancji roboczej

Jak wspomnieliśmy wcześniej, 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 najszybszej możliwej operacji na pliku jest FileSystemSyncAccessHandle uzyskany 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 lokalne metody tworzenia plików

Dzięki synchronicznemu uchwytowi dostępu możesz korzystać z szybkich, synchronicznych metod obsługi plików.

  • 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 obiektom wywołującym wykrywanie i obsługę błędów oraz częściowych zapisów.
  • read(): odczytuje zawartość pliku w buforze, opcjonalnie z określonym przesunięciem.
  • truncate(): zmienia rozmiar pliku do podanego rozmiaru.
  • flush(): gwarantuje, że zawartość pliku zawiera wszystkie zmiany wprowadzone za pomocą narzędzia 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 źródłowego do systemu plików widocznego dla użytkowników

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 punktu początkowego

Dopóki nie dodasz wbudowanej obsługi Narzędzi deweloperskich (patrz crbug/1284595), używaj rozszerzenia do Chrome OPFS Explorer, aby debugować prywatny system plików źródła. 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 do Chrome w Chrome Web Store.

Po zainstalowaniu rozszerzenia otwórz Narzędzia deweloperskie w Chrome, wybierz kartę OPFS Explorer. Teraz możesz przejrzeć hierarchię plików. Aby zapisać pliki z prywatnego systemu plików źródłowego w systemie plików widocznym dla użytkowników, kliknij nazwę pliku, a następnie usuń pliki i foldery, klikając ikonę kosza.

Prezentacja

Zobacz, jak działa prywatny system plików origin (jeśli zainstalujesz rozszerzenie OPFS Explorer) w wersji demonstracyjnej, która używa go jako backendu bazy danych SQLite skompilowanej do WebAssembly. Koniecznie zapoznaj się z kodem źródłowym w Glitch. Zwróć uwagę, że niżej umieszczona wersja nie korzysta z backendu prywatnego systemu plików punktu początkowego (ponieważ element iframe jest z innej domeny), ale po otwarciu wersji demonstracyjnej na osobnej karcie korzysta z niego.

Podsumowanie

Prywatny system plików źródła, zgodnie z zasadą WhatWG, ukształtował sposób, w jaki wykorzystujemy pliki w internecie i wchodzimy z nimi w interakcje. Zapewnia on nowe przypadki użycia, których nie dało się osiągnąć z użyciem systemu plików widocznego dla użytkowników. Wszyscy najwięksi dostawcy przeglądarek – Apple, Mozilla i Google – uczestniczą w programie i mają wspólną wizję. Rozwój prywatnego systemu plików origin wymaga współpracy, a opinie deweloperów i użytkowników są niezbędne. 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, którego autorem jest Christina Rumpf, kanał Unsplash.