Is it possible to append to an existing file with Chrome's File System Access API

Viewed 513

Using the new File System Access APIit is possible to read and write to files and folders on the user's device:

const newHandle = await window.showSaveFilePicker();
const writableStream = await newHandle.createWritable();
await writableStream.write("Hello World")
await writableStream.close();

The above code will write "Hello World" to the chosen file. If the file already exists, it will be truncated and overwritten with the new content.

Is it possible to instead append to an existing file without reading the whole file and writing it again? A good example could be writing to a log file.

2 Answers

You can get the current file size and set the writing position to the end:

const newHandle = await window.showSaveFilePicker();
const writableStream = await newHandle.createWritable();

// Get the current file size.
const size = (await fileHandle.getFile()).size;

await writableStream.write({
  type: "write",
  data: "Hello World",
  position: size // Set the position to the current file size.
})
await writableStream.close();

More info:

https://developer.mozilla.org/en-US/docs/Web/API/FileSystemWritableFileStream/write https://developer.mozilla.org/en-US/docs/Web/API/IDBMutableFile/getFile

Joe Duncan is correct (and thanks for the tip!)

But you need to open the writeable file in 'append' mode.

I have need to close/flush the file after every line is written, and then re-open, seek and append to that file (but only 1 click to the SaveFile button)

import {} from "wicg-file-system-access"

/**
 * Supply a button-id in HTML, when user clicks the file is opened for write-append.
 * 
 * Other code can: new LogWriter().writeLine("First line...") 
 * to queue writes before user clicks the button.
 * 
 * file is flushed/closed & re-opened after every writeLine.
 * (so log is already saved if browser crashes...)
 */
export class LogWriter {
  fileHandle: FileSystemFileHandle;
  writeablePromise: Promise<FileSystemWritableFileStream> = this.newWriterPromise;
  writerReady: (value: FileSystemWritableFileStream | PromiseLike<FileSystemWritableFileStream>) => void 
  writerFailed: (reason?: any) => void
  contents: string
  get newOpenPromise () { return new Promise<FileSystemWritableFileStream>((fil, rej)=>{
    this.writerReady = fil; 
    this.writerFailed = rej
  })}
  constructor(name = 'logFile', public buttonId = "fsOpenFileButton") {
    const options = {
      id: 'logWriter',
      startIn: 'downloads', // documents, desktop, music, pictures, videos
      suggestedName: name,
      types: [{
          description: 'Text Files',
          accept: { 'text/plain': ['.txt'], },
        }, ],
    };
    this.setButton('showSaveFilePicker', options, (value) => {
      this.fileHandle = value as FileSystemFileHandle
      this.openWriteable()
    })
  }
  async openWriteable(fileHandle: FileSystemFileHandle = this.fileHandle,
    options: FileSystemCreateWritableOptions = { keepExistingData: true }) {
    let writeable = await fileHandle.createWritable(options)
    let offset = (await fileHandle.getFile()).size
    writeable.seek(offset)
    this.writerReady(writeable)
  }
  async writeLine(text: string) {
    try {
      let line = `${text}\n`
      let stream = (await this.openPromise) // indicates writeable is ready
      await stream.seek((await this.fileHandle.getFile()).size)
      await stream.write({type: 'write', data: line});
      let closePromise = this.closeFile()       // flush to real-file
      this.openPromise = this.newOpenPromise    // new Promise for next cycle:
      await closePromise
      while (!this.openPromise.value) await this.openWriteable()
    } catch (err) {
      console.warn(stime(this, `.writeLine failed:`), err)
      throw err
    }
  }
  async closeFile() {
    try {
      return (await this.writeablePromise).close();
    } catch (err) {
      console.warn(stime(this, `.closeFile failed:`), err)
      throw err
    }
  }
  /** multi-purpose picker button: (callback arg-type changes) */
  setButton(method: 'showOpenFilePicker' | 'showSaveFilePicker' | 'showDirectoryPicker',
    options: OpenFilePickerOptions & { multiple?: false; } & SaveFilePickerOptions & DirectoryPickerOptions,
    cb: (fileHandleAry: any) => void) {
    const picker = window[method]  // showSaveFilePicker showDirectoryPicker
    const fsOpenButton = document.getElementById(this.buttonId)
    fsOpenButton.innerText = method.substring(4, method.length - 6)
    fsOpenButton.onclick = () => {
      picker(options).then((value: any) => cb(value), (rej: any) => {
        console.warn(`showOpenFilePicker failed: `, rej)
      });
    }
    return fsOpenButton
  }
}
Related