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

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

File System Access API란 무엇인가요?

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

파일 읽기 및 쓰기를 사용해 본 적이 있다면 공유할 내용의 대부분이 익숙할 것입니다. 모든 시스템이 동일하지는 않으므로 가이드 내용을 읽어보시기 바랍니다.

File System Access API는 Windows, macOS, ChromeOS, Linux의 대부분의 Chromium 브라우저에서 지원됩니다. 주목할 만한 예외는 현재 플래그 뒤에만 사용할 수 있는 Brave입니다. Android 지원은 crbug.com/1011535의 맥락에서 진행되고 있습니다.

File System Access API 사용

File System Access API의 성능과 유용성을 자랑하기 위해 단일 파일 텍스트 편집기를 작성했습니다. 텍스트 파일을 열거나, 수정하거나, 변경사항을 디스크에 다시 저장하거나, 새 파일을 시작하고 변경사항을 디스크에 저장할 수 있습니다. 특별한 것은 아니지만 개념을 이해하는 데 도움이 될 만큼 충분합니다.

브라우저 지원

브라우저 지원

  • Chrome: 86
  • Edge: 86.
  • Firefox: 지원되지 않음
  • Safari: 지원되지 않음

소스

기능 감지

File System Access API가 지원되는지 확인하려면 관심 있는 선택 도구 메서드가 있는지 확인합니다.

if ('showOpenFilePicker' in self) {
  // The `showOpenFilePicker()` method of the File System Access API is supported.
}

직접 해 보기

텍스트 편집기 데모에서 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가 하나 있는 1개 요소 배열입니다.

나중에 사용할 수 있도록 파일 핸들에 대한 참조를 유지하는 것이 좋습니다. 파일에 변경사항을 저장하거나 다른 파일 작업을 실행해야 합니다.

파일 시스템에서 파일 읽기

이제 파일 핸들이 있으므로 파일의 속성을 가져오거나 파일 자체에 액세스할 수 있습니다. 지금은 콘텐츠를 읽어 보겠습니다. 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;
});

파일을 로컬 파일 시스템에 씁니다.

텍스트 편집기에서 파일을 저장하는 방법에는 저장다른 이름으로 저장 두 가지가 있습니다. 저장은 이전에 가져온 파일 핸들을 사용하여 변경사항을 원본 파일에 다시 씁니다. 하지만 다른 이름으로 저장을 사용하면 새 파일이 생성되므로 새 파일 핸들이 필요합니다.

새 파일 만들기

파일을 저장하려면 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() 메서드는 텍스트 편집기에 필요한 문자열을 사용합니다. 하지만 BufferSource 또는 Blob을 사용할 수도 있습니다. 예를 들어 스트림을 이 위치로 직접 파이핑할 수 있습니다.

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' }를 전달할 수 있습니다.

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()를 사용하여 모든 파일을 동시에 처리하세요.

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

브라우저 지원

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

소스

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

출처 비공개 파일 시스템은 파일 콘텐츠에 대한 인플레이스 및 독점 쓰기 액세스를 제공하는 등 성능에 최적화된 특수한 종류의 파일에 대한 선택적 액세스를 제공합니다. 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와 같이 여러 파일을 수정하는 웹 앱에서도 열 때 변경사항을 저장할 권한을 요청할 수 있습니다.

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

투명성

검색주소창 아이콘
사용자가 웹사이트에 로컬 파일에 저장할 권한을 부여했음을 나타내는 주소 표시줄 아이콘입니다.

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

권한 유지

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

의견

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가 작성했습니다.