Send a batch of files in sequence. The session is left open
afterward, which allows for more files to be sent if desired.

@param {Zmodem.Session} session - The send session

@param {FileList|Array} files - A list of File objects

@param {Object} [options]
@param {Function} [options.on_offer_response] - Called when an
offer response arrives. Arguments are:

- (File) - The File object that corresponds to the offer.
- (Transfer|undefined) - If the receiver accepts the offer, then
this is a Transfer object; otherwise it’s undefined.

@param {Function} [options.on_progress] - Called immediately
after a chunk of a file is sent. Arguments are:

- (File) - The File object that corresponds to the file.
- (Transfer) - The Transfer object for the current transfer.
- (Uint8Array) - The chunk of data that was just loaded from disk
and sent to the receiver.

@param {Function} [options.on_file_complete] - Called immediately
after the last file packet is sent. Arguments are:

- (File) - The File object that corresponds to the file.
- (Transfer) - The Transfer object for the now-completed transfer.

@return {Promise} A Promise that fulfills when the batch is done.
Note that skipped files are not considered an error condition.
function send_files(session, files, options) {
        if (!options) options = {};

        //Populate the batch in reverse order to simplify sending
        //the remaining files/bytes components.
        var batch = [];
        var total_size = 0;
        for (var f=files.length - 1; f>=0; f--) {
            var fobj = files[f];
            total_size += fobj.size;
            batch[f] = {
                obj: fobj,
                name: fobj.name,
                size: fobj.size,
                mtime: new Date(fobj.lastModified),
                files_remaining: files.length - f,
                bytes_remaining: total_size,
            };
        }

        var file_idx = 0;
        function promise_callback() {
            var cur_b = batch[file_idx];

            if (!cur_b) {
                return Promise.resolve(); //batch done!
            }

            file_idx++;

            return session.send_offer(cur_b).then( function after_send_offer(xfer) {
                if (options.on_offer_response) {
                    options.on_offer_response(cur_b.obj, xfer);
                }

                if (xfer === undefined) {
                    return promise_callback();   //skipped
                }

                return new Promise( function(res) {
                    var reader = new FileReader();

                    //This really shouldn’t happen … so let’s
                    //blow up if it does.
                    reader.onerror = function reader_onerror(e) {
                        console.error("file read error", e);
                        throw("File read error: " + e);
                    };

                    var piece;
                    reader.onprogress = function reader_onprogress(e) {

                        //Some browsers (e.g., Chrome) give partial returns,
                        //while others (e.g., Firefox) don’t.
                        if (e.target.result) {
                            piece = new Uint8Array(e.target.result, xfer.get_offset())

                            _check_aborted(session);

                            xfer.send(piece);

                            if (options.on_progress) {
                                options.on_progress(cur_b.obj, xfer, piece);
                            }
                        }
                    };

                    reader.onload = function reader_onload(e) {
                        piece = new Uint8Array(e.target.result, xfer, piece)

                        _check_aborted(session);

                        xfer.end(piece).then( function() {
                            if (options.on_progress && piece.length) {
                                options.on_progress(cur_b.obj, xfer, piece);
                            }

                            if (options.on_file_complete) {
                                options.on_file_complete(cur_b.obj, xfer);
                            }

                            //Resolve the current file-send promise with
                            //another promise. That promise resolves immediately
                            //if we’re done, or with another file-send promise
                            //if there’s more to send.
                            res( promise_callback() );
                        } );
                    };

                    reader.readAsArrayBuffer(cur_b.obj);
                } );
            } );
        }

        return promise_callback();
    }