跳转到内容

网站访问用户文件夹

刀刀

1/8/2025

0 字

0 分钟

在以前,网页是无法获取用户的文件夹隐私,但是现在已经有相关的 API 了。下面通过几步来实现。

读取文件夹内全部文件

选择文件夹

通过 showDirectoryPicker API 打开选择文件夹对话框,调用该方法就会弹窗对话框,在对话框内可以选择文件夹。

选择完毕后需要用户确认给予相关权限,确认后就能生效。代码如下:

js
btn.onclick= function() {
  console.log('click me')
  showDirectoryPicker()
}

得到文件夹内的文件、子文件夹

showDirectoryPicker API 的返回值就是得到的文件或文件夹的句柄,返回如下:

js
{
  kind: 'directory',
  name:'xxx'
}

其中,kind 表示得到的类型,directory 表示文件夹,file 表示文件。name 为该文件夹或文件的名称。

获取有可能会失败,可能用户会拒绝访问文件夹,解决方法为通过 try...catch 捕获错误。

拿到了文件夹时,可以通过 .entries() 方法获取文件夹内所有的内容,返回值是一个异得到的是步迭代器,通过 for await of 循环都可以拿到子文件句柄。句柄返回的内容如下:

js
[
  'foldr-request',
  {
    kind: 'directory',
    name: 'folder-request'
  }
],
[
  'demo.html',
  {
    kind: 'file',
    name: 'demo.html'
  }
]

由此可见能拿到文件夹或文件,可以通过递归的形式一直去循环遍历获取句柄,直到全部都获取到文件为止。代码如下:

查看代码
js
btn.onclick = async function() {
  try {
    const handle = await showDirectoryPicker()
    const root = await processHandle(handle)
  } catch(err) {
    // ...
  }
}

async function processHandle(handle) {
  // 添加判断,终止递归,返回文件
  if(handle.kind === 'file') {
    return handle
  }
  
  handle.children = []
  const iter = await handle.entries() // 获取文件夹中所有内容
  for await (const info of iter) {
    const subHandle = await processHandle(info[1]) // 返回的是一个数组,返回的内容格式如上所述。通过递归的思想一直获取文件夹内的内容
    handle.children.push(subHandle)
  }
  
  return handle
}

现在 root 能够拿到一个树状结构的句柄。

读取文件

通过 getFile() 可以获取文件内容,使用 FileReader() 类中的 readAsText 方法就能读取文件内容,该读取操作是异步操作,通过 onload 函数能够最终获取结果。代码如下:

js
btn.onclick = async function() {
  try {
    const handle = await showDirectoryPicker()
    const root = await processHandle(handle)
    
    // 获取内容
    const file = await root.children[1].getFile()
    const reader = new FileReader()
    reader.onload = e => {
      console.log(e.target.result)
    }
  } catch(err) {
    // ...
  }
}

// ...

其他操作

文件夹多选

要从多个文件中读取,我们需要传入一个 options 对象给 showOpenFilePicker() ,其中,默认情况下,multiple 属性设置为 false ,想要多选需要修改为 true

js
let fileHandles;
const options = { multiple: true, }; 
const pickFile  = async () => {
  fileHandles = await window.showOpenFilePicker(options); 
};

其他选项可用于指示可以选择的文件类型。例如,只想接收 .png 文件,选项对象将包括以下内容

js
const options = { 
  types: [ 
    {
      description: "Images", 
      accept: { "image/png": ".png", },
    }, 
  ],
  excludeAcceptAllOption: true,
};

多选后返回的结果是一个包含多个文件的数组,因此获取它们的内容将通过以下方式完成:

js
const pickFile = async () => { 
  const fileList = await window.showOpenFilePicker({ multiple: true });
  const allContent = await Promise.all( fileList.map(async (fileItem) => { 
      const file = await fileItem.getFile();
      const content = await file.text(); 
      return content; 
    })
  );
  console.log(allContent);
};

写文件

写文件的操作分为以下几步:

  1. 通过 createWritable() 返回一个 FileSystemWritableFileStream 对象
  2. 调用 write() 将需要传递内容写入此流
  3. 调用 close() 关闭流
js
btn.onclick = async function() {
  try {
    const handle = await showDirectoryPicker({ 
      types: [ 
        { 
          description: "Test files",
          accept: { 
            "text/plain": [".txt"], 
          },
        }, 
      ],
    })
        
    // 写文件
    const writable = await handle.createWritable()
    await writable.write('hello world')
    await writable.close()
  } catch(err) {
    // ...
  }
}

这样就能写一个 html 内容。

编辑文件内容

编辑文件的本质是先通过 showOpenFilePicker()getFile() 方法读取文件,然后使用createWritable() , write()close() 写入同一个文件内,实现编辑操作。

js
let fileHandle;
const writeFile = async () => { 
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile(); 
  const writable = await fileHandle.createWritable(); 
  await writable.write("这是新写入的内容"); 
  await writable.close(); 
};

删除文件

调用 remove() 方法可以删除全部文件或文件夹;传入对应文件名称可以指定删除的文件或文件夹。

js
const removeDirectory =  async () => { 
  const directoryHandle = await window.showDirectoryPicker();
  await directoryHandle.remove(); 
};

// 删除选定文件夹中名为data.txt的单个文件
const removeSelfFile = async () => { 
  const directoryHandle = await window.showDirectoryPicker();
  await directoryHandle.removeEntry("data.txt"); 
};

// 递归删除名为“data”的文件夹
const removeAllDirectory = async () => {
  const directoryHandle = await window.showDirectoryPicker();
  await directoryHandle.removeEntry('data', { recursive: true }); 
};

浏览器兼容性

can i use版本

总体效果