Check if file has changed using HTML5 File API
Asked Answered
S

3

17

Okay, so I have a program which outputs some specific data to a tab separated variable file.

I had been using Excel to open and view the file contents, however I found excel's insistence on locking every file it opens to be incredibly annoying as my program would crash if I left the file open in Excel... but I would really like the data to neatly update after each run of the program, so I don't have to close and re-open the file all the time.

So, I decided it would be easiest to use Javascript to parse the file and display it in a html table, and it was. I knocked something together in no time. Now my program doesn't crash if I leave the file on display, however, it still doesn't update... and I have to open the newly generated file each time.

So, I was wondering if there was a mechanism by which my Javascript could be somehow notified of a change to the file by another process? I know this is unlikely, but I would like to avoid simply polling the file for obvious reasons.

I am very familiar with JS, but HTML5 and the new APIs are all new to me.

She answered 11/1, 2013 at 18:9 Comment(0)
P
13

I don't believe the File API has any event for the file changing, just progress events and the like.

Update August 2020: The alternative below no longer works, and the specification specifically disallows it by saying that the File object's information must reflect the state of the underlying file as of when it was selected. From the spec:

User agents should endeavor to have a File object’s snapshot state set to the state of the underlying storage on disk at the time the reference is taken. If the file is modified on disk following the time a reference has been taken, the File's snapshot state will differ from the state of the underlying storage.


You could use polling. Remember the lastModifiedDate of the File, and then when your polling function fires, get a new File instance for the input and see if the lastModifiedDate has changed.

This works for me on Chrome, for instance: Live Copy | Source

(function() {
  var input;
  var lastMod;

  document.getElementById('btnStart').onclick = function() {
    startWatching();
  };
    function startWatching() {
        var file;

        if (typeof window.FileReader !== 'function') {
            display("The file API isn't supported on this browser yet.");
            return;
        }

        input = document.getElementById('filename');
        if (!input) {
            display("Um, couldn't find the filename element.");
        }
        else if (!input.files) {
            display("This browser doesn't seem to support the `files` property of file inputs.");
        }
        else if (!input.files[0]) {
            display("Please select a file before clicking 'Show Size'");
        }
        else {
            file = input.files[0];
      lastMod = file.lastModifiedDate;
            display("Last modified date: " + lastMod);
      display("Change the file");
      setInterval(tick, 250);
        }
    }

  function tick() {
    var file = input.files && input.files[0];
    if (file && lastMod && file.lastModifiedDate.getTime() !== lastMod.getTime()) {
      lastMod = file.lastModifiedDate;
            display("File changed: " + lastMod);
    }
  }

  function display(msg) {
    var p = document.createElement('p');
    p.innerHTML = msg;
    document.body.appendChild(p);
  }
})();
<input type='file' id='filename'>
<input type='button' id='btnStart' value='Start'>
Paranymph answered 11/1, 2013 at 18:14 Comment(5)
I have implemented this, through finding time amongst other work. I am just testing it out now. Looks like it should be great though. Thanks!She
Firefox seems to cache the file, it produces the same date every time.Skellum
@TomášZato: Hmmm, sadly I can replicate your results on Firefox. :-| The characters "cach" (e.g., "cache", "caching", etc.) don't appear in the specification at all, so I guess it's allowed to do what it wants. Could use a "refresh" method on File or some such.Paranymph
This no longer works in any major browser (nor should it, according to the spec). See comment here: #62247032Fusillade
Thanks @leo! Verified, found the relevant part of the spec, and updated the answer.Paranymph
W
7

There is two solutions to this problem, and <input type="file"> is not one of them. according to the spec, it creates a "snapshot" of the file.


Native File System

This api is experimental and requires flags to be enabled in blink (aka chromium browsers). The idea is that you get a file handle and when you need the file then you call the async "getFile" function to retrieve the actual file.

This feature is a "power feature" and require your site to be secure, and it can't work in sandboxed iframes.

So without testing here is some "code in the dark":

// triggerd on click
async function pickFile () {
  const handle = showOpenFilePicker()
  let lastModificationTime = 0
  
  async function compare () {
    const file = await handle.getFile()
    if (file.lastModified > lastModificationTime) {
      lastModificationTime = +file.lastModified
      console.log(await file.text())
    }
  }
  
  setInterval(compare, 1000)
}

Get Entry from Drag and drop

Similar to the native file system you can also retrieve a file handle and do the same thing, but this feature works in all browsers today. but this code snippet don't work in stackoverflow since it use some sandboxing do making it incompatible, so here is a fiddle with few comments

function drop(event) {
  event.stopPropagation();
  event.preventDefault();
    
  // get the file as an fileEntry (aka file handle)
  const fileEntry = event.dataTransfer.items[0].webkitGetAsEntry()
  let lastModificationTime = 0
  
  async function read (file) {
    // use the new async read method on blobs.
    console.log(await file.text())
  }
  
  function compare (meta) {
    if (meta.modificationTime > lastModificationTime) {
      lastModificationTime = meta.modificationTime
      fileEntry.file(read)
    }
  }
  
  setInterval(fileEntry.getMetadata.bind(fileEntry, compare), 1000)
}

Edit: there is now also a way to get drag and dropped files as a FileSystemFileHandle that is much nicer to work with

elem.addEventListener('dragover', evt => {
  // Prevent navigation.
  evt.preventDefault()
})
elem.addEventListener('drop', async evt => {
  // Prevent navigation.
  evt.preventDefault()

  // Process all of the items.
  for (const item of evt.dataTransfer.items) {
    // kind will be 'file' for file/directory entries.
    if (item.kind === 'file') {
      const entry = await item.getAsFileSystemHandle();
      if (entry.kind === 'file') {
        // use same solution as the first Native File System solution
      }
    }
  }
})
Wastepaper answered 29/5, 2020 at 20:20 Comment(1)
Looks like this API is enabled by default at the moment, thanks for this info! I will look into it. chromestatus.com/feature/6284708426022912Helminthic
T
4

While T.J. Crowder's answer is correct, Chrome's implementation appears to break the spec.

Each Blob must have an internal snapshot state, which must be initially set to the state of the underlying storage, if any such underlying storage exists, and must be preserved through structured clone. Further normative definition of snapshot state can be found for files.

When a file is selected the input has a snapshot of the contents at that point. Local changes on disk don't update the snapshot.

Tureen answered 17/2, 2017 at 12:1 Comment(4)
Is it possible that just Chrome on Windows breaks the spec? Linux file system might make implementing this easier than on Windows...Semicolon
This is the observed behavior on macOS as well.Tureen
It looks like this part of the spec may have been relaxed. In particular, for files, it is explicitly a "should" instead of a "must" due to the implementation difficulty. See the "File Interface" part of the specManville
Based on my testing, Chrome's implementation now aligns with the spec, and doesn't update the snapshot.Semicolon

© 2022 - 2024 — McMap. All rights reserved.