File System Access API: 로컬 파일 액세스 간소화

File System Access API를 사용하면 웹 앱이 사용자 기기의 파일 및 폴더에 변경사항을 직접 읽거나 저장할 수 있습니다.

File System Access API란 무엇인가요?

File System Access API (이전 명칭: Native File System API, 이전에는 Writeable Files API라고 함)는 개발자가 IDE, 사진 및 동영상 편집기, 텍스트 편집기 등 사용자의 로컬 기기에 있는 파일과 상호작용하는 강력한 웹 앱을 빌드할 수 있습니다. 사용자가 웹 앱에 액세스 권한을 부여하면 이 API를 통해 사용자 기기의 파일 및 폴더에 변경사항을 직접 읽거나 저장할 수 있습니다. File System Access API는 파일 읽기 및 쓰기 외에도 디렉터리를 열고 콘텐츠를 열거하는 기능을 제공합니다.

이전에 파일을 읽고 쓰는 작업을 해본 적이 있다면 제가 공유할 내용은 대부분 익숙할 것입니다. 모든 시스템이 같은 것은 아니기 때문에 꼭 읽어 보시기 바랍니다.

File System Access API는 현재 Windows, macOS, ChromeOS, Linux에 설치된 대부분의 Chromium 브라우저에서 지원됩니다. 주목할 만한 예외는 현재 플래그 뒤에만 사용할 수 있는 Brave입니다. Android는 Chromium 109부터 API의 원본 비공개 파일 시스템 부분을 지원합니다. 현재 선택 도구 메서드에 관한 계획은 없지만 crbug.com/1011535에 별표표시하여 잠재적인 진행 상황을 추적할 수 있습니다.

File System Access API 사용

File System Access API의 성능과 유용성을 입증하기 위해 저는 단일 파일 텍스트 편집기를 작성했습니다. 이를 통해 텍스트 파일을 열거나 수정하거나 변경사항을 디스크에 다시 저장하거나 새 파일을 시작하고 변경사항을 디스크에 저장할 수 있습니다. 복잡하지는 않지만 개념을 이해하는 데 도움이 될 만큼 충분히 제공합니다.

브라우저 지원

브라우저 지원

  • 86
  • 86
  • x
  • x

소스

직접 해 보기

텍스트 편집기 데모에서 File System Access API의 작동 방식을 확인하세요.

로컬 파일 시스템에서 파일 읽기

첫 번째로 다룰 사용 사례는 사용자에게 파일을 선택하도록 요청한 다음 디스크에서 파일을 열고 읽는 것입니다.

사용자에게 읽을 파일을 선택하라고 요청

File System Access API의 진입점은 window.showOpenFilePicker()입니다. 호출하면 파일 선택 도구 대화상자가 표시되고 사용자에게 파일을 선택하라는 메시지가 표시됩니다. 사용자가 파일을 선택하면 API는 파일 핸들 배열을 반환합니다. 선택사항인 options 매개변수를 사용하면 사용자가 여러 파일이나 디렉터리, 다른 파일 형식을 선택하도록 하여 파일 선택 도구의 동작에 영향을 미칠 수 있습니다. 지정된 옵션이 없으면 파일 선택 도구를 사용하면 사용자가 단일 파일을 선택할 수 있습니다. 이는 텍스트 편집기에 적합합니다.

다른 여러 강력한 API와 마찬가지로 showOpenFilePicker() 호출은 보안 컨텍스트에서 실행해야 하며 사용자 동작 내에서 호출해야 합니다.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  // Destructure the one-element array.
  [fileHandle] = await window.showOpenFilePicker();
  // Do something with the file handle.
});

사용자가 파일을 선택하면 showOpenFilePicker()가 핸들 배열을 반환합니다. 이 경우 파일과 상호작용하는 데 필요한 속성과 메서드를 포함하는 하나의 FileSystemFileHandle가 있는 요소 하나 배열입니다.

나중에 사용할 수 있도록 파일 핸들 참조를 보관하는 것이 좋습니다. 파일에 변경사항을 저장하거나 다른 파일 작업을 실행하기 위해 필요합니다.

파일 시스템에서 파일 읽기

이제 파일에 대한 핸들을 갖게 되었으므로 파일 속성을 가져오거나 파일 자체에 액세스할 수 있습니다. 지금은 콘텐츠를 그냥 읽어 보겠습니다. handle.getFile()를 호출하면 blob이 포함된 File 객체가 반환됩니다. blob에서 데이터를 가져오려면 메서드 중 하나(slice(), stream(), text() 또는 arrayBuffer())를 호출합니다.

const file = await fileHandle.getFile();
const contents = await file.text();

FileSystemFileHandle.getFile()에서 반환된 File 객체는 디스크의 기본 파일이 변경되지 않는 경우에만 읽을 수 있습니다. 디스크의 파일이 수정되면 File 객체를 읽을 수 없게 되며 getFile()를 다시 호출하여 변경된 데이터를 읽을 새 File 객체를 가져와야 합니다.

요약

사용자가 '열기' 버튼을 클릭하면 브라우저에 파일 선택 도구가 표시됩니다. 사용자가 파일을 선택하면 앱은 콘텐츠를 읽고 <textarea>에 저장합니다.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const contents = await file.text();
  textArea.value = contents;
});

로컬 파일 시스템에 파일 쓰기

텍스트 편집기에서 파일을 저장하는 방법에는 저장다른 이름으로 저장의 두 가지 방법이 있습니다. Save는 앞서 검색한 파일 핸들을 사용하여 변경사항을 원본 파일에 다시 씁니다. 그러나 다른 이름으로 저장을 사용하면 새 파일이 생성되므로 새 파일 핸들이 필요합니다.

새 파일 만들기

파일을 저장하려면 '저장' 모드로 파일 선택 도구를 표시하는 showSaveFilePicker()를 호출합니다. 그러면 사용자가 저장에 사용할 새 파일을 선택할 수 있습니다. 텍스트 편집기의 경우 자동으로 .txt 확장 프로그램을 추가하기 위해 몇 가지 추가 매개변수를 제공했습니다.

async function getNewFileHandle() {
  const options = {
    types: [
      {
        description: 'Text Files',
        accept: {
          'text/plain': ['.txt'],
        },
      },
    ],
  };
  const handle = await window.showSaveFilePicker(options);
  return handle;
}

디스크에 변경사항 저장

GitHub텍스트 편집기 데모에서 파일에 변경사항을 저장하는 데 필요한 모든 코드를 확인할 수 있습니다. 핵심 파일 시스템 상호작용은 fs-helpers.js에 있습니다. 가장 간단한 프로세스는 아래 코드와 같습니다. 각 단계를 단계별로 설명하겠습니다.

// fileHandle is an instance of FileSystemFileHandle..
async function writeFile(fileHandle, contents) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write(contents);
  // Close the file and write the contents to disk.
  await writable.close();
}

디스크에 데이터를 쓸 때는 WritableStream의 서브클래스인 FileSystemWritableFileStream 객체를 사용합니다. 파일 핸들 객체에서 createWritable()를 호출하여 스트림을 만듭니다. createWritable()가 호출되면 브라우저는 먼저 사용자가 파일에 쓰기 권한을 부여했는지 확인합니다. 쓰기 권한이 부여되지 않으면 브라우저에서 사용자에게 권한을 요청하는 메시지를 표시합니다. 권한이 부여되지 않으면 createWritable()에서 DOMException이 발생하고 앱이 파일에 쓸 수 없습니다. 텍스트 편집기에서 DOMException 객체는 saveFile() 메서드에서 처리됩니다.

write() 메서드는 텍스트 편집기에 필요한 문자열을 사용합니다. BufferSourceBlob도 사용할 수 있습니다. 예를 들어 스트림을 직접 파이핑할 수 있습니다.

async function writeURLToFile(fileHandle, url) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Make an HTTP request for the contents.
  const response = await fetch(url);
  // Stream the response into the file.
  await response.body.pipeTo(writable);
  // pipeTo() closes the destination pipe by default, no need to close it.
}

스트림 내에서 seek() 또는 truncate()를 사용하여 특정 위치의 파일을 업데이트하거나 파일 크기를 조정할 수도 있습니다.

추천 파일 이름 및 시작 디렉터리 지정

대부분의 경우 앱에서 기본 파일 이름이나 위치를 제안하도록 할 수 있습니다. 예를 들어 텍스트 편집기는 Untitled 대신 Untitled Text.txt의 기본 파일 이름을 제안할 수 있습니다. 이렇게 하려면 suggestedName 속성을 showSaveFilePicker 옵션의 일부로 전달하면 됩니다.

const fileHandle = await self.showSaveFilePicker({
  suggestedName: 'Untitled Text.txt',
  types: [{
    description: 'Text documents',
    accept: {
      'text/plain': ['.txt'],
    },
  }],
});

기본 시작 디렉터리도 마찬가지입니다. 텍스트 편집기를 빌드하는 경우 기본 documents 폴더에서 파일 저장 또는 파일 열기 대화상자를 시작하는 것이 좋습니다. 반면 이미지 편집기의 경우 기본 pictures 폴더에서 시작하는 것이 좋습니다. 이와 같이 startIn 속성을 showSaveFilePicker, showDirectoryPicker() 또는 showOpenFilePicker 메서드에 전달하여 기본 시작 디렉터리를 제안할 수 있습니다.

const fileHandle = await self.showOpenFilePicker({
  startIn: 'pictures'
});

잘 알려진 시스템 디렉터리의 목록은 다음과 같습니다.

  • desktop: 사용자의 데스크톱 디렉터리(존재하는 경우)
  • documents: 사용자가 만든 문서가 일반적으로 저장되는 디렉터리
  • downloads: 다운로드한 파일이 일반적으로 저장되는 디렉터리입니다.
  • music: 오디오 파일이 일반적으로 저장되는 디렉터리입니다.
  • pictures: 일반적으로 사진 및 기타 스틸 이미지가 저장되는 디렉터리입니다.
  • videos: 일반적으로 동영상/영화가 저장되는 디렉터리입니다.

잘 알려진 시스템 디렉터리 외에도 기존 파일이나 디렉터리 핸들을 startIn 값으로 전달할 수 있습니다. 그러면 동일한 디렉터리에서 대화상자가 열립니다.

// Assume `directoryHandle` is a handle to a previously opened directory.
const fileHandle = await self.showOpenFilePicker({
  startIn: directoryHandle
});

다양한 파일 선택 도구의 용도 지정

애플리케이션의 목적에 따라 선택 도구가 다른 경우도 있습니다. 예를 들어 서식 있는 텍스트 편집기를 사용하면 텍스트 파일을 여는 것뿐 아니라 이미지를 가져올 수도 있습니다. 기본적으로 각 파일 선택기는 마지막으로 기억된 위치에서 열립니다. 각 선택 도구 유형의 id 값을 저장하여 이를 우회할 수 있습니다. id가 지정되면 파일 선택 도구 구현은 id에 마지막으로 사용한 디렉터리를 별도로 기억합니다.

const fileHandle1 = await self.showSaveFilePicker({
  id: 'openText',
});

const fileHandle2 = await self.showSaveFilePicker({
  id: 'importImage',
});

IndexedDB에 파일 핸들 또는 디렉터리 핸들 저장

파일 핸들과 디렉터리 핸들은 직렬화할 수 있습니다. 즉, 파일 또는 디렉터리 핸들을 IndexedDB에 저장하거나 postMessage()를 호출하여 동일한 최상위 출처 간에 전송할 수 있습니다.

파일 또는 디렉터리 핸들을 IndexedDB에 저장하면 상태를 저장하거나 사용자가 작업 중인 파일이나 디렉터리를 기억할 수 있습니다. 이를 통해 최근에 열거나 수정한 파일 목록을 유지하고, 앱이 열릴 때 마지막 파일을 다시 열고, 이전 작업 디렉터리를 복원하는 등의 작업을 할 수 있습니다. 텍스트 편집기에 사용자가 마지막으로 연 파일 5개의 목록을 저장하여 다시 해당 파일에 쉽게 액세스할 수 있도록 합니다.

아래의 코드 예에서는 파일 핸들과 디렉터리 핸들을 저장하고 검색하는 방법을 보여줍니다. Glitch에서 실제 작동 모습을 확인할 수 있습니다. 간결성을 위해 idb-keyval 라이브러리를 사용합니다.

import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js';

const pre1 = document.querySelector('pre.file');
const pre2 = document.querySelector('pre.directory');
const button1 = document.querySelector('button.file');
const button2 = document.querySelector('button.directory');

// File handle
button1.addEventListener('click', async () => {
  try {
    const fileHandleOrUndefined = await get('file');
    if (fileHandleOrUndefined) {
      pre1.textContent = `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const [fileHandle] = await window.showOpenFilePicker();
    await set('file', fileHandle);
    pre1.textContent = `Stored file handle for "${fileHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

// Directory handle
button2.addEventListener('click', async () => {
  try {
    const directoryHandleOrUndefined = await get('directory');
    if (directoryHandleOrUndefined) {
      pre2.textContent = `Retrieved directroy handle "${directoryHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const directoryHandle = await window.showDirectoryPicker();
    await set('directory', directoryHandle);
    pre2.textContent = `Stored directory handle for "${directoryHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

저장된 파일 또는 디렉터리 핸들 및 권한

권한은 현재 세션 간에 유지되지 않으므로 queryPermission()를 사용하여 사용자가 파일이나 디렉터리에 권한을 부여했는지 확인해야 합니다. 없으면 requestPermission()를 호출하여 다시 요청합니다. 이는 파일 및 디렉터리 핸들에서도 동일하게 작동합니다. fileOrDirectoryHandle.requestPermission(descriptor) 또는 fileOrDirectoryHandle.queryPermission(descriptor)를 각각 실행해야 합니다.

사용자가 이미 권한을 부여했는지 확인하고 필요한 경우 요청하는 verifyPermission() 메서드를 텍스트 편집기에서 만들었습니다.

async function verifyPermission(fileHandle, readWrite) {
  const options = {};
  if (readWrite) {
    options.mode = 'readwrite';
  }
  // Check if permission was already granted. If so, return true.
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  // Request permission. If the user grants permission, return true.
  if ((await fileHandle.requestPermission(options)) === 'granted') {
    return true;
  }
  // The user didn't grant permission, so return false.
  return false;
}

읽기 요청으로 쓰기 권한을 요청하여 권한 프롬프트 수를 줄였습니다. 사용자가 파일을 열 때 프롬프트가 하나만 표시되고 파일에 대한 읽기 및 쓰기 권한을 모두 부여했습니다.

디렉터리 열기 및 콘텐츠 열거

디렉터리의 모든 파일을 열거하려면 showDirectoryPicker()를 호출합니다. 사용자가 선택 도구에서 디렉터리를 선택하면 FileSystemDirectoryHandle이 반환되어 디렉터리의 파일을 열거하고 액세스할 수 있습니다. 기본적으로 디렉터리에 있는 파일에 대한 읽기 액세스 권한이 있지만 쓰기 액세스 권한이 필요한 경우 메서드에 { mode: 'readwrite' }를 전달할 수 있습니다.

const butDir = document.getElementById('butDirectory');
butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.kind, entry.name);
  }
});

예를 들어 개별 파일 크기를 가져오기 위해 getFile()를 통해 각 파일에 추가로 액세스해야 하는 경우 각 결과에 await를 순차적으로 사용하지 말고 Promise.all() 등을 통해 모든 파일을 병렬로 처리합니다.

const butDir = document.getElementById('butDirectory');
butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  const promises = [];
  for await (const entry of dirHandle.values()) {
    if (entry.kind !== 'file') {
      continue;
    }
    promises.push(entry.getFile().then((file) => `${file.name} (${file.size})`));
  }
  console.log(await Promise.all(promises));
});

디렉터리에 파일 및 폴더 만들기 또는 액세스

디렉터리에서 getFileHandle() 또는 각각 getDirectoryHandle() 메서드를 사용하여 파일 및 폴더를 만들거나 액세스할 수 있습니다. create 키와 불리언 값 true 또는 false와 함께 선택적 options 객체를 전달하면 새 파일이나 폴더가 존재하지 않는 경우 만들어야 하는지 여부를 결정할 수 있습니다.

// In an existing directory, create a new directory named "My Documents".
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
  create: true,
});
// In this new directory, create a file named "My Notes.txt".
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });

디렉터리의 항목 경로 확인

디렉터리에서 파일이나 폴더로 작업할 때 해당 항목의 경로를 해결하는 것이 유용할 수 있습니다. 적절한 이름의 resolve() 메서드를 사용하면 됩니다. 해결을 위해 항목은 디렉터리의 직접 또는 간접 하위 요소일 수 있습니다.

// Resolve the path of the previously created file called "My Notes.txt".
const path = await newDirectoryHandle.resolve(newFileHandle);
// `path` is now ["My Documents", "My Notes.txt"]

디렉터리에서 파일 및 폴더 삭제

디렉터리에 대한 액세스 권한이 있으면 removeEntry() 메서드를 사용하여 포함된 파일과 폴더를 삭제할 수 있습니다. 폴더의 경우 삭제는 선택적으로 재귀적일 수 있으며 모든 하위 폴더 및 그 안에 포함된 파일을 포함합니다.

// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });

파일 또는 폴더 직접 삭제

파일이나 디렉터리 핸들에 액세스할 수 있는 경우 FileSystemFileHandle 또는 FileSystemDirectoryHandle에서 remove()를 호출하여 삭제합니다.

// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();

파일과 폴더 이름 바꾸기

FileSystemHandle 인터페이스에서 move()를 호출하여 파일과 폴더를 이름을 바꾸거나 새 위치로 이동할 수 있습니다. FileSystemHandle에는 하위 인터페이스 FileSystemFileHandleFileSystemDirectoryHandle가 있습니다. move() 메서드는 하나 또는 두 개의 매개변수를 사용합니다. 첫 번째는 새 이름이 있는 문자열 또는 대상 폴더에 대한 FileSystemDirectoryHandle일 수 있습니다. 후자의 경우 선택사항인 두 번째 매개변수는 새 이름이 포함된 문자열이므로 이동 및 이름 변경이 한 번에 발생할 수 있습니다.

// Rename the file.
await file.move('new_name');
// Move the file to a new directory.
await file.move(directory);
// Move the file to a new directory and rename it.
await file.move(directory, 'newer_name');

드래그 앤 드롭 통합

HTML 드래그 앤 드롭 인터페이스를 사용하면 웹 애플리케이션이 웹페이지에서 드래그 앤 드롭한 파일을 허용할 수 있습니다. 드래그 앤 드롭 작업 중에 드래그된 파일 및 디렉터리 항목은 각각 파일 항목 및 디렉터리 항목과 연결됩니다. DataTransferItem.getAsFileSystemHandle() 메서드는 드래그된 항목이 파일인 경우 FileSystemFileHandle 객체와 함께 프로미스를 반환하고 드래그된 항목이 디렉터리인 경우 FileSystemDirectoryHandle 객체와 함께 프로미스를 반환합니다. 아래 목록은 실제 사례를 보여줍니다. 드래그 앤 드롭 인터페이스의 DataTransferItem.kind는 파일 디렉터리에 모두 "file"인 반면, File System Access API의 FileSystemHandle.kind는 파일의 경우 "file", 디렉터리의 경우 "directory"입니다.

elem.addEventListener('dragover', (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener('drop', async (e) => {
  e.preventDefault();

  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFileSystemHandle());

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === 'directory') {
      console.log(`Directory: ${handle.name}`);
    } else {
      console.log(`File: ${handle.name}`);
    }
  }
});

원본 비공개 파일 시스템에 액세스

원본 비공개 파일 시스템은 이름에서 알 수 있듯이 페이지 출처에만 공개되는 스토리지 엔드포인트입니다. 브라우저는 일반적으로 이 출처 비공개 파일 시스템의 콘텐츠를 어딘가에 디스크에 유지하여 이를 구현하지만, 사용자가 콘텐츠에 쉽게 액세스할 수 있는 것은 아닙니다. 마찬가지로 원본 비공개 파일 시스템의 하위 요소 이름과 이름이 일치하는 파일이나 디렉터리가 존재할 것으로 예상하지 않습니다. 브라우저는 원본 비공개 파일 시스템이므로 내부적으로는 파일이 있는 것처럼 보일 수 있지만 브라우저에서는 이러한 '파일'을 데이터베이스나 다른 데이터 구조에 저장할 수 있습니다. 기본적으로 이 API를 사용하는 경우 생성된 파일이 하드 디스크의 어딘가에서 일대일로 일치한다는 것을 기대하지 마세요. 루트 FileSystemDirectoryHandle에 액세스하면 원본 비공개 파일 시스템에서 평소와 같이 작동할 수 있습니다.

const root = await navigator.storage.getDirectory();
// Create a new file handle.
const fileHandle = await root.getFileHandle('Untitled.txt', { create: true });
// Create a new directory handle.
const dirHandle = await root.getDirectoryHandle('New Folder', { create: true });
// Recursively remove a directory.
await root.removeEntry('Old Stuff', { recursive: true });

브라우저 지원

  • 86
  • 86
  • 111
  • 15.2

소스

원본 비공개 파일 시스템에서 성능에 최적화된 파일에 액세스

원본 비공개 파일 시스템은 파일 콘텐츠에 대한 인플레이스(In-Place) 및 배타적인 쓰기 액세스를 제공하는 등 성능에 고도로 최적화된 특별한 종류의 파일에 대한 선택적 액세스를 제공합니다. Chromium 102 이상에서는 원본 비공개 파일 시스템에 파일 액세스를 간소화하기 위한 추가 메서드인 createSyncAccessHandle() (동기식 읽기 및 쓰기 작업용)가 있습니다. FileSystemFileHandle에서 노출되지만 웹 워커에서만 노출됩니다.

// (Read and write operations are synchronous,
// but obtaining the handle is asynchronous.)
// Synchronous access exclusively in Worker contexts.
const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.read(buffer, { at: 1 });

폴리필

File System Access API 메서드를 완전히 폴리필할 수는 없습니다.

  • showOpenFilePicker() 메서드는 <input type="file"> 요소를 사용하여 근사할 수 있습니다.
  • showSaveFilePicker() 메서드는 프로그래매틱 다운로드를 트리거하고 기존 파일을 덮어쓸 수 없지만 <a download="file_name"> 요소를 사용하여 시뮬레이션할 수 있습니다.
  • showDirectoryPicker() 메서드는 비표준 <input type="file" webkitdirectory> 요소를 사용하여 다소 에뮬레이션될 수 있습니다.

Google은 가능한 경우 File System Access API를 사용하고 다른 모든 경우에는 차선의 옵션으로 대체하는 browser-fs-access라는 라이브러리를 개발했습니다.

보안 및 권한

Chrome팀은 사용자 제어 및 투명성, 사용자 인체공학 등 강력한 웹 플랫폼 기능에 대한 액세스 제어에 정의된 핵심 원칙을 사용하여 File System Access API를 설계하고 구현했습니다.

파일 열기 또는 새 파일 저장

읽을 파일을 여는 파일 선택 도구
기존 파일을 열어 읽을 때 사용하는 파일 선택 도구입니다.

파일을 열 때 사용자는 파일 선택기를 통해 파일이나 디렉터리를 읽을 수 있는 권한을 제공합니다. 파일 열기 선택 도구는 보안 컨텍스트에서 제공될 때 사용자 동작을 통해서만 표시할 수 있습니다. 사용자가 마음이 바뀌면 파일 선택기에서 선택을 취소할 수 있으며 사이트에서는 이 항목에 액세스할 수 없습니다. 이는 <input type="file"> 요소와 동일한 동작입니다.

디스크에 파일을 저장하는 파일 선택 도구
디스크에 파일을 저장하는 데 사용되는 파일 선택 도구입니다.

마찬가지로 웹 앱이 새 파일을 저장하려고 하면 브라우저에 저장 파일 선택 도구가 표시되어 사용자가 새 파일의 이름과 위치를 지정할 수 있습니다. 파일 선택 도구는 기존 파일을 덮어쓰는 것과는 달리 새 파일을 기기에 저장하므로 파일에 쓸 수 있는 권한을 앱에 부여합니다.

제한된 폴더

사용자 및 사용자 데이터를 보호하기 위해 브라우저에서는 Windows, macOS 라이브러리 폴더와 같은 핵심 운영체제 폴더와 같은 특정 폴더에 사용자가 저장하는 기능을 제한할 수 있습니다. 이 경우 브라우저에 메시지가 표시되고 사용자에게 다른 폴더를 선택하라는 메시지가 표시됩니다.

기존 파일 또는 디렉터리 수정

웹 앱은 사용자에게 명시적인 권한을 받지 않고 디스크에 있는 파일을 수정할 수 없습니다.

권한 메시지

이전에 읽기 액세스 권한을 부여한 파일에 변경사항을 저장하려는 경우 브라우저에 권한 메시지가 표시되어 사이트에 변경사항을 디스크에 쓸 수 있도록 권한을 요청하는 메시지가 표시됩니다. 권한 요청은 저장 버튼 클릭과 같은 사용자 동작에 의해서만 트리거될 수 있습니다.

파일을 저장하기 전에 표시되는 권한 메시지
브라우저에 기존 파일에 대한 쓰기 권한이 부여되기 전에 사용자에게 메시지가 표시됩니다.

또는 IDE와 같이 여러 파일을 수정하는 웹 앱은 열 때 변경사항을 저장할 권한을 요청할 수도 있습니다.

사용자가 취소를 선택하고 쓰기 액세스 권한을 부여하지 않으면 웹 앱은 변경사항을 로컬 파일에 저장할 수 없습니다. 예를 들어 파일을 '다운로드'하거나 클라우드에 데이터를 저장하는 등의 방법으로 사용자가 데이터를 저장할 수 있는 대체 방법을 제공해야 합니다.

투명성

검색주소창 아이콘
사용자가 웹사이트에 로컬 파일에 저장할 수 있는 권한을 부여했음을 나타내는 검색주소창 아이콘

사용자가 웹 앱에 로컬 파일을 저장할 권한을 부여하면 브라우저의 URL 표시줄에 아이콘이 표시됩니다. 아이콘을 클릭하면 사용자가 액세스 권한을 부여한 파일 목록을 보여주는 팝오버가 열립니다. 사용자는 원하는 경우 액세스 권한을 쉽게 취소할 수 있습니다.

권한 지속성

웹 앱은 출처의 모든 탭을 닫을 때까지 메시지를 표시하지 않고 파일의 변경사항을 계속 저장할 수 있습니다. 탭이 닫히면 사이트에서 모든 액세스 권한을 잃게 됩니다. 다음에 사용자가 웹 앱을 사용할 때 파일에 액세스하라는 메시지가 다시 표시됩니다.

의견

File System Access API 사용 경험에 관한 의견을 듣고자 합니다.

API 설계에 대해 알려주세요.

API에서 예상한 대로 작동하지 않는 부분이 있나요? 아니면 아이디어를 구현하는 데 필요한 메서드나 속성이 누락되었나요? 보안 모델에 대한 질문이나 의견이 있으신가요?

구현에 문제가 있나요?

Chrome 구현에서 버그를 발견하셨나요? 아니면 구현이 사양과 다른가요?

  • https://new.crbug.com에서 버그를 신고합니다. 가능한 한 많은 세부정보와 간단한 재현 안내를 포함하고 구성요소Blink>Storage>FileSystem로 설정합니다. Glitch는 쉽고 빠른 재현을 공유하는 데 효과적입니다.

API를 사용할 계획이신가요?

사이트에서 File System Access API를 사용할 계획이신가요? 공개 지원은 Google에서 기능의 우선순위를 정하는 데 도움이 되며 다른 브라우저 공급업체에 이러한 기능을 지원하는 것이 얼마나 중요한지 보여줍니다.

유용한 링크

감사의 말

File System Access API 사양은 Marijn Kruisselbrink가 작성했습니다.