Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

HTML5 ondrop event returns before zip.js can finish operations

The crux of my issue is that I need to use a datatransferitemlist asynchronously which is at odds with the functionality described in the specs, which is that you are locked out of the dataTransfer.items collection once the event ends.

https://bugs.chromium.org/p/chromium/issues/detail?id=137231 http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#drag-data-store

The case offender is the following. With a more detailed description of my problem and thoughts below it.

drophandler: function(event) {
    event.stopPropagation();
    event.preventDefault();
    event.dataTransfer.dropEffect = 'copy';
    zip.workerScriptsPath = "../bower_components/zip.js/WebContent/";
    zip.useWebWorkers = false; // Disabled because it just makes life more complicated
    // Check if files contains just a zip
    if (event.dataTransfer.files[0].name.match(/(?:\.([^.]+))?$/) == 'zip') {
        var reader = new FileReader();
        that = this;
        reader.onload = function(e) {
            that.fire('zipuploaded', e.target.result.split(',')[1]);
        }
        reader.readAsDataURL(event.dataTransfer.files[0]);
        // Rev up that in browser zipping
    } else {
        var that = this;
        var items = event.dataTransfer.items;
        // Async operation, execution falls through from here
        zip.createWriter(new zip.Data64URIWriter(), function(writer) {
            (function traverse(list, path, i, depth) {
                return new Promise(function(resolve, reject) {
                    var item;
                    if (depth == 0) {
                        if (i == list.length) {
                            writer.close(function(uri) {
                                that.fire('zipuploaded', uri.split(',')[1]); // Just the base64, please
                                fulfill(1);
                                return;
                            });
                        } else {
                            console.log(i);
                            console.log(list);
                            var item = list[i].webkitGetAsEntry();
                        }
                    } else {
                        if (i == list.length) {
                            resolve(0);
                            return;
                        } else {
                            item = list[i];
                        }
                    }
                    if (item.isFile) {
                        item.file(function(file) {
                            // Zipping operations done asynchronously, it'll fail by roughly the second operation
                            writer.add(path + file.name, zip.BlobReader(file), function() {
                                traverse(list, path, i + 1, depth).then(resolve(0)); // Next item
                            });
                        });
                    } else if (item.isDirectory) {
                        var dirReader = item.createDirReader();
                        dirReader.readEntries(function(entries) {
                            // Operate on child folder then the next item at this level
                            traverse(entries, path + item.name + "/", 0, depth + 1).then(function() {
                                traverse(list, path, i + 1, depth).then(resolve(0));
                            });
                        });
                    }
                });
            })(items, "", 0, 0); // Begin with datatransferitemlist, 0th element, depth 0
        });
        this.$.uploadarea.classList.remove('highlightdrag');
    }
    // When we exit it kills the event.dataTransfer.items
},

I am using zip.js which is asynchronous with the HTML5 DnD API. The ondrop event ends before the asynchronous zip.createWriter/writer.add operations finish. I can think of four ways to solve this although I don't know how to implement any of them and would like some advice.

  1. Block until createWriter is done. (Blocking javascript? Uhoh)
  2. Prevent the ondrop from locking me out of dataTransfer.items (It seems to be for security so unlikely)
  3. Synchronously copy out the contents of dataTransfer.items first (Probably very slow)
  4. Do everything synchronously (Don't think zip.js allows this, JsZip does, but I moved away from that due to it having its own limitations with large file sets)
like image 750
moggers Avatar asked Sep 07 '16 12:09

moggers


1 Answers

HTML5 DnD works as expected. The problem is, when adding multiple files, if you add a file before previous finish, zip.js breaks silently. This can be fixed by calling writer.add in series.

The snippet might not work, see this pen instead.

This example flats the structure of dropped files, then add it to zip in series.

function mes(it) {
  const m = document.querySelector('#mes')
  return m.textContent = it + '\n' + m.textContent
}

function read(items) {
  return Promise.all(items.map(item => {
    if (item.isFile) return [item]
    return new Promise(resolve => item.createReader().readEntries(resolve))
    .then(entries => {
      entries.forEach(it => it.path = item.path + '/' + it.name)
      return read(entries)
    })
  })).then(entries => entries.reduce((a, b) => a.concat(b)))
}

function handleResult(blob){
  const res = document.querySelector('#result')
  res.download = 'files.zip'
  res.href = window.URL.createObjectURL(blob)
  res.textContent = 'download zipped file'
}

function handleItems(items){
  mes(items.length)
  items.forEach(item => item.path = item.name)
  const initZip = new Promise(resolve =>
    zip.createWriter(new zip.BlobWriter, resolve)
  )
  const getFiles = read(items).then(entries => {
    return Promise.all(entries.map(entry =>
      new Promise(resolve =>
        entry.file(file => {
          file.path = entry.path
          resolve(file)
        })
      )
    ))
  })
  return Promise.all([getFiles, initZip]).then(([files, writer]) =>
    files.reduce((current, next) =>
      current.then(() =>
        new Promise(resolve => {
          mes(next.path)
          writer.add(next.path, new zip.BlobReader(next), resolve)
        })
      )
    , Promise.resolve())
    .then(() => writer.close(handleResult))
  )
}

zip.useWebWorkers = false
const drop = document.querySelector('#drop');

['dragover', 'drop'].forEach(name =>
  drop.addEventListener(name, ev => ev.preventDefault())
)
drop.addEventListener('drop', ev => {
  const items = [].slice.call(ev.dataTransfer.items)
  .map(item => item.webkitGetAsEntry())
  return handleItems(items)
})
html, body, #drop {
  height: 100%;
  width: 100%;
}
<script src="http://gildas-lormeau.github.io/zip.js/demos/zip.js"></script>
<script src="http://gildas-lormeau.github.io/zip.js/demos/deflate.js"></script>


<div id="drop">
  Drop here!
  <br>
  <a id="result"></a>
</div>
<pre id="mes"></pre>

jszip is much easier than this, you might want to give it a try.

like image 113
DarkKnight Avatar answered Oct 27 '22 01:10

DarkKnight