0

I want a cleaner UI than what

<input type="file" ...> 

gives me.

I can use window.showOpenFilePicker() to get the user gesture security context and get a "fileSystemHandle" from that API but I can't figure out how to read that file using the FileReader object. (or any other way)

Here is my failing code:

/**
 * Used to import via a simple button.
 * The caller must call window.showOpenFilePicker() and give the
 * fileHandle to this API.
 * @param {*} fileHandle 
 * @param {*} fnImportToUI 
 */
fio.importFromFileButton = async function(fileHandle, fnImportToUI) {
  if (Array.isArray(fileHandle)) {
    fileHandle = fileHandle[0];
  }
  var blob;
  const reader = new FileReader();
  reader.addEventListener('loadend', (event) => {
    blob = new Blob([new Uint8Array(event.target.result)], {type: fileHandle.type })
    fnImportToUI(fileHandle, event.target.result)
  });
  reader.readAsArrayBuffer(blob);
}

The problem being that the fileSystemHandle is not a Blob (I get an error if I call

reader.readAsArrayBuffer(fileHandle);
or
reader.readAsText(fileHandle);

or using any of the ReadFile read APIs.)

From what I can tell, I can't convert the fileSystemHandle to a Blob until I have read the file to get the data - a chicken and egg issue.

4
  • Note that this doesn't work at all in either Firefox or Safari and doesn't work in any mobile browser (not even Chrome on Android), so most users aren't going see any UI at all... Commented Aug 24, 2023 at 15:40
  • That being said, if you can't resist the Siren's song, I believe you want to call getFile on the FileSystemFileHandle in the Promise returned by showOpenFilePicker and then await that Promise to get the File object (which N.B. is already a Blob). See this and this and this for more detail. Commented Aug 24, 2023 at 15:47
  • 1
    As @JaredSmith said with much reason, one should not use this cool FileSystem API without putting in place a fallback if ever the API is not available. Check this article where the author demonstrates a complete example of how you could use this. link Commented Mar 19, 2024 at 9:48
  • This API is for actual read/write filesystem file access, i.e. you're writing an application that's going to actually work with files, not just "loads in some data because that's all it needs". For that, use a file input with a FileReader and parse the result either as text, json, or byte array. Use the File System API if you need something like a filesystem backed live-updated sqlite3 database that literally saves itself back to disk on each operation, or a webview-based text editor that has to save changes back to disk. I.e. applications that need to work with files, not just data. Commented Jul 8 at 22:21

3 Answers 3

0

I'm not sure where that code came from, but the correct way to use that API looks like this. Assuming you have a file foo.txt with the contents "Hello, world!" in it:

// Returns a Promise of a single File object
async function pickSingleFile() {
  const [fileHandle] = await window.showOpenFilePicker();
  return await fileHandle.getFile();
}

// Takes a File object, returns a Promise of a string
async function readFile(file) {
  return await file.text();
}

pickSingleFile() // user picks foo.txt
  .then(readFile)
  .then(console.log) // prints "Hello, world!"
  .catch(console.error); // in case of oops

You can actually test this here, on this page, by creating a text file, pasting that code into your browser console (assuming you're on desktop Chrome or Edge) and navigating to the correct file in the file picker that opens.

Note as I said in the comments this feature has at the time of this writing has abysmal support and works only on desktop versions of Chromium-based browsers, no Safari, no FireFox, and no mobile (not even Chrome on Android), so think carefully before using.

Sign up to request clarification or add additional context in comments.

Comments

0

What ended up working for me was:

fio.importFromFileOpenPicker = async function(fileHandle, fnImportToUI) {
  let file = await fileHandle.getFile();
  const reader = new FileReader();
  reader.addEventListener('loadend', (event) => {
    fnImportToUI(file, event.target.result)
  });
  reader.readAsText(file);
}

Thank you all for the help!

1 Comment

Just for clarity, fileHandle is what is directly returned from window.showOpenFilePicker(); You can't do that within this async function as it violates the user gesture security context.
-1

There are several mistakes in your snippet, I have fixed based on my assumptions. Code is pretty much self explanatory.

/**
 * convert a File object into base64 dataurl or plain text, returns null if its aborted
 * @param {File} file
 * @returns {Promise<string|null>}
 * @param {'plain'|'base64'} mode
 */
 
const convertFileToDataURLOrString = (file, mode) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.addEventListener('load', () => resolve('' + reader.result))
    reader.addEventListener('abort', () => resolve(null))
    reader.addEventListener('error', () => reject(reader.error))
    if (mode === 'plain') reader.readAsText(file)
    else reader.readAsDataURL(file)
  })

fio.importFromFileButton = async function (fileHandle, fnImportToUI) {
  if (Array.isArray(fileHandle)) {
    fileHandle = fileHandle[0]
  }
  const datastr = convertFileToDataURLOrString(fileHandle, 'plain')
  // do something with datastr
}

1 Comment

I was confused about getFile() called directly on the returned fileSystemHandle vs fileSystemHandle[0]. Thanks.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.