2

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.

marlar
  • 3,858
  • 6
  • 37
  • 60
  • 1
    reading the [documentation](https://wicg.github.io/file-system-access/#api-filesystemfilehandle-createwritable) shows that `newHandle.createWritable({keepExistingData:true});` is *possibly* what you need to look for – Jaromanda X Jun 21 '21 at 13:57
  • @JaromandaX Thanks, looks promising. Combined with seek it might work. – marlar Jun 21 '21 at 14:00
  • 1
    @JaromandaX It doesn't seem to work. The moment you invoke window.showSaveFilePicker(), the file gets truncated. So even if you use keepExistingData:true, the file's content is already lost. – marlar Jun 29 '21 at 09:51
  • Seems Chrome hasn't implemented that flag in that case – Jaromanda X Jun 29 '21 at 09:59

2 Answers2

2

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
  }
}
Jack Punt
  • 342
  • 1
  • 14
0

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
  • 366
  • 2
  • 7
  • 3
    This doesn't work. window.showSaveFilePicker immediately truncates the opened file to zero so even if you are appending data, you are appending from the beginning. I tried with showOpenFilePicker instead, but the returned handle does not have a createWritable method. – marlar Jun 25 '21 at 14:56