Manipulação persistente de arquivos com a File System Access API
Os mecanismos tradicionais de upload de arquivos há muito tempo são limitados pelo sandbox do
navegador. A abordagem típica com <input type="file"> obriga você a selecionar os arquivos de novo após
atualizar a página e não expõe caminhos diretos do sistema de arquivos. A File System Access API
oferece uma solução moderna ao permitir handles de arquivo persistentes que os aplicativos podem
salvar no IndexedDB. Depois que a permissão é concedida, um usuário que volta ao site pode reabrir o
mesmo arquivo local sem selecioná-lo novamente. Retomar um upload também exige um protocolo de
servidor com suporte a retomada e o estado do upload salvo; um handle de arquivo sozinho não oferece
isso. Como o suporte dos navegadores varia, mantenha uma alternativa padrão de seleção de arquivos.
Suporte dos navegadores e aprimoramento progressivo
A biblioteca browser-fs-access recorre a um input de arquivo quando os seletores nativos não estão disponíveis. Verifique cada método de seletor em vez de presumir que todas as APIs de sistema de arquivos têm suporte em conjunto. Consulte a tabela de compatibilidade dos seletores para os navegadores que você quer atender. A seleção de arquivos alternativa não fornece um handle nativo reutilizável.
Em um projeto JavaScript com bundler, instale browser-fs-access e idb-keyval. Este último é
usado mais abaixo para armazenar handles no IndexedDB.
yarn add browser-fs-access idb-keyval
import { fileOpen, supported } from 'browser-fs-access'
async function handleFileAccess() {
if (supported) {
console.log('Using native File System Access API')
} else {
console.log('Using legacy fallback via browser-fs-access')
}
try {
const blob = await fileOpen({
mimeTypes: ['image/*'],
multiple: false,
})
return blob
} catch (err) {
if (err.name === 'AbortError') {
console.log('User cancelled file selection')
} else {
console.error('Error accessing file:', err)
}
return null
}
}
Considerações de segurança
A File System Access API aplica várias medidas de segurança para proteger os dados do usuário:
- As permissões são vinculadas à origem. Handles salvos podem durar mais que uma permissão concedida, então verifique o acesso novamente.
- Um contexto seguro (HTTPS) é obrigatório para uso em produção.
- Operações de escrita pedem consentimento explícito do usuário antes de modificar arquivos.
- O acesso é limitado a arquivos ou diretórios aprovados pelo usuário; alguns locais sensíveis são bloqueados.
Faça as solicitações de permissão a partir de uma ação do usuário, como o clique em um botão. Se o
acesso de escrita for necessário, use { mode: 'readwrite' } em vez de { mode: 'read' }.
async function verifyPermissions(handle) {
const options = { mode: 'read' }
try {
if ((await handle.queryPermission(options)) === 'granted') {
return true
}
const permission = await handle.requestPermission(options)
return permission === 'granted'
} catch (err) {
console.error('Permission error:', err)
return false
}
}
Para persistir um handle nativo, armazene-o usando o suporte a structured clone do IndexedDB, e não
JSON ou localStorage. Carregue o handle salvo antes de habilitar o botão de reabrir, para que as
solicitações de permissão possam ser executadas diretamente a partir do clique. Conecte as funções
a seguir aos botões da sua página depois de importar o helper verifyPermissions anterior no mesmo módulo:
import { get, set } from 'idb-keyval'
let savedHandle = null
async function loadSavedHandle() {
savedHandle = await get('selected-upload-file') ?? null
return savedHandle !== null
}
async function choosePersistentFile() {
if (typeof window.showOpenFilePicker !== 'function') {
return handleFileAccess()
}
const [handle] = await window.showOpenFilePicker({ multiple: false })
await set('selected-upload-file', handle)
savedHandle = handle
return handle.getFile()
}
async function reopenSavedFile() {
if (!savedHandle) return null
if (!(await verifyPermissions(savedHandle))) return null
return savedHandle.getFile()
}
Ao carregar a página, aguarde loadSavedHandle() e habilite o botão de reabrir somente quando ele retornar
true. Chame choosePersistentFile() ou reopenSavedFile() a partir dos respectivos handlers de clique. Capture
erros de armazenamento, de cancelamento do seletor e de acesso a arquivos nesses handlers e mostre
um status útil. Os usuários podem apagar o armazenamento do navegador, revogar a permissão ou mover
o arquivo, então mantenha sempre o botão de escolher arquivo. Antes de retomar um upload, verifique
se o arquivo reaberto ainda corresponde ao upload original.
Manipulação de diretórios
Habilitar uploads de diretórios permite processar hierarquias inteiras de arquivos. O exemplo a seguir coleta recursivamente os arquivos de um diretório selecionado pelo usuário:
async function handleDirectoryUpload() {
if (typeof window.showDirectoryPicker !== 'function') {
throw new Error('Directory selection is unavailable in this browser')
}
try {
const dirHandle = await window.showDirectoryPicker()
const files = []
async function* getFilesRecursively(entry) {
for await (const handle of entry.values()) {
if (handle.kind === 'file') {
const file = await handle.getFile()
if (file) yield file
} else if (handle.kind === 'directory') {
yield* getFilesRecursively(handle)
}
}
}
for await (const file of getFilesRecursively(dirHandle)) {
files.push(file)
}
return files
} catch (err) {
if (err.name === 'AbortError') {
console.log('User cancelled directory selection')
} else if (err.name === 'SecurityError') {
console.error('Permission denied')
} else {
console.error('Error accessing directory:', err)
}
return []
}
}
Integração com arrastar e soltar
Integre arrastar e soltar para melhorar a experiência de seleção de arquivos. O exemplo abaixo demonstra a verificação de tipo dos itens soltos e processa tanto handles de arquivo quanto objetos de arquivo padrão:
function setupDragAndDrop(dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
dropZone.classList.add('drag-active')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-active')
})
dropZone.addEventListener('drop', async (e) => {
e.preventDefault()
dropZone.classList.remove('drag-active')
const items = Array.from(e.dataTransfer.items).filter((item) => item.kind === 'file')
try {
const handles = await Promise.all(
items.map((item) => item.getAsFileSystemHandle?.() ?? item.getAsFile()),
)
for (const handle of handles) {
if (!handle) continue
if (handle instanceof File) {
// Process a regular file dropped
console.log('Processing dropped file:', handle.name)
} else if (handle.kind === 'file') {
const file = await handle.getFile()
console.log('Processing file handle:', file.name)
} else if (handle.kind === 'directory') {
console.log('Processing directory:', handle.name)
}
}
} catch (err) {
console.error('Error processing dropped items:', err)
}
})
}
Manipulação de arquivos grandes
Para manter o uso de memória limitado, consuma cada chunk antes de ler o próximo; não junte os
chunks em um novo Blob. Forneça um callback assíncrono consumeChunk que termine de processar ou
enviar cada chunk antes de ser resolvido. Isso controla o buffering da aplicação, não os buffers
internos do navegador.
async function handleLargeFile(fileHandle, consumeChunk) {
const file = await fileHandle.getFile()
const reader = file.stream().getReader()
let totalSize = 0
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
await consumeChunk(value)
totalSize += value.length
// Report progress
const progress = (totalSize / file.size) * 100
console.log(`Processing: ${progress.toFixed(2)}%`)
}
return totalSize
} catch (err) {
await reader.cancel(err).catch(() => {})
throw err
} finally {
reader.releaseLock()
}
}
Estratégias de tratamento de erros
Garanta que suas operações com arquivos sejam robustas com um tratamento de erros abrangente. A estratégia abaixo verifica permissões, valida o acesso aos arquivos e trata casos de erro comuns:
async function safeFileOperation(handle) {
if (!handle) {
throw new Error('No file handle provided')
}
try {
const permissionGranted = await verifyPermissions(handle)
if (!permissionGranted) {
throw new DOMException('Permission denied', 'NotAllowedError')
}
const file = await handle.getFile()
if (!file) {
throw new Error('Could not access file')
}
return file
} catch (err) {
switch (err.name) {
case 'NotFoundError':
console.error('File no longer exists')
break
case 'SecurityError':
console.error('Permission denied')
break
case 'NotAllowedError':
console.error('User denied permission')
break
default:
console.error('Unexpected error:', err)
}
throw err
}
}
A File System Access API transforma a manipulação de arquivos na web com integração direta ao sistema de arquivos local. Embora o suporte dos navegadores seja limitado a certos ambientes, implementar aprimoramento progressivo garante que seus usuários tenham uma experiência confiável em todas as plataformas.
Para implementações avançadas de upload de arquivos, considere explorar ferramentas como Uppy e tus para ter uploads resilientes e retomáveis.
