10

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)
DarkKnight
  • 5,651
  • 2
  • 24
  • 36
moggers
  • 503
  • 4
  • 8
  • One issue is that you are calling `resolve` function immediately at `traverse(list, path, i + 1, depth).then(resolve(0))` instead of referencing `resolve` function. Not certain what expected result is? Add all dropped files to single `.zip` folder? – guest271314 Sep 16 '16 at 01:36

1 Answers1

4

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.

DarkKnight
  • 5,651
  • 2
  • 24
  • 36
  • >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. – moggers Sep 13 '16 at 15:34
  • (Accidentally pressed enter instead of shift+enter) >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. That's what I've tried to do with putting traverse() inside the success callback of writer.add >JsZip I was using JsZip before. But it was causing memory usage to balloon up to 2GB and I found a bunch of github issues describing it as not very good for large filesets. And zip.js has web workers so it seems all around better. I'll play around with your code snippet for a bit. – moggers Sep 13 '16 at 15:45