Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chrome's FileSystem API has a bug reading files from dropped folders or input dialog's selected folder #301

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ Most of the magic for Flow.js happens in the user's browser, but files still nee

To handle the state of upload chunks, a number of extra parameters are sent along with all requests:

* `flowChunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here).
* `flowTotalChunks`: The total number of chunks.
* `flowChunkSize`: The general chunk size. Using this value and `flowTotalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `flowChunkSize` of this for the last chunk for a file.
* `flowTotalSize`: The total file size.
* `flowIdentifier`: A unique identifier for the file contained in the request.
* `flowFilename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts).
* `flowRelativePath`: The file's relative path when selecting a directory (defaults to file name in all browsers except Chrome).
* `chunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here).
* `totalChunks`: The total number of chunks.
* `chunkSize`: The general chunk size. Using this value and `totalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `chunkSize` of this for the last chunk for a file.
* `totalSize`: The total file size.
* `requestId`: A unique identifier for the file contained in the request.
* `filename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts).
* `relativePath`: The file's relative path when selecting a directory (defaults to file name in all browsers except Chrome).

You should allow for the same chunk to be uploaded more than once; this isn't standard behaviour, but on an unstable network environment it could happen, and this case is exactly what Flow.js is designed for.

Expand Down Expand Up @@ -195,6 +195,7 @@ added.
* `.fileRetry(file, chunk)` Something went wrong during upload of a specific file, uploading is being
retried.
* `.fileError(file, message, chunk)` An error occurred during upload of a specific file.
* `.readErrors(files, folders, event)` This event fires before fileAdded or filesAdded events only if errors occur while reading files or folders. First argument `files` is collection of files read errors, second argument `folders` is collection of folder read errors.
* `.uploadStart()` Upload has been started on the Flow object.
* `.complete()` Uploading completed.
* `.progress()` Uploading progress.
Expand Down
236 changes: 169 additions & 67 deletions dist/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,55 +230,109 @@
*/
webkitReadDataTransfer: function (event) {
var $ = this;
var queue = event.dataTransfer.items.length;
var files = [];
each(event.dataTransfer.items, function (item) {
var entry = item.webkitGetAsEntry();
if (!entry) {
decrement();
return ;
getEntries(event.dataTransfer.items).then(function (result) {
getFiles(result.files).then(function (entries) {
var files = [];
var errors = [];
each(entries, function (entry) {
if (entry.error) {
errors.push(entry);
} else {
files.push(entry);
}
});
if (result.errors.length || errors.length) {
$.fire('readErrors', errors, result.errors, event);
}
if (files.length) {
$.addFiles(files, event);
}
});
});
function getEntries(items) {
var files = [];
var errors = [];
var promises = [];

function readEntry(entry, promises) {
if (entry.isFile) {
files.push(entry);
} else if (entry.isDirectory) {
promises.push(readDirectory(entry));
}
}
if (entry.isFile) {
// due to a bug in Chrome's File System API impl - #149735
fileReadSuccess(item.getAsFile(), entry.fullPath);
} else {
readDirectory(entry.createReader());

function readDirectory(entry) {
var reader = entry.createReader();
return new Promise(function (resolve, reject) {
var promises = [];
readEntries(entry, reader, promises, resolve);
});
}
});
function readDirectory(reader) {
reader.readEntries(function (entries) {
if (entries.length) {
queue += entries.length;
each(entries, function(entry) {
if (entry.isFile) {
var fullPath = entry.fullPath;
entry.file(function (file) {
fileReadSuccess(file, fullPath);
}, readError);
} else if (entry.isDirectory) {
readDirectory(entry.createReader());
}

function readEntries(entry, reader, promises, resolve) {
reader.readEntries(function (entries) {
if (entries.length) {
var promises2 = [];
each(entries, function (entry2) {
readEntry(entry2, promises2);
});
promises.push(Promise.all(promises2));
readEntries(entry, reader, promises, resolve);
return;
}
resolve(Promise.all(promises));
}, function (error) {
errors.push({
path: entry.fullPath,
error: error
});
readDirectory(reader);
} else {
decrement();
resolve(promises);
});
}

each(items, function (item) {
var entry = item.webkitGetAsEntry();
if (!entry) {
return;
}
}, readError);
if (entry.isFile) {
// due to a bug in Chrome's File System API impl - #149735
files.push(getFile(item.getAsFile(), entry.fullPath));
return;
}
readEntry(entry, promises);
});

return new Promise(function (resolve, reject) {
return Promise.all(promises).then(function () {
resolve({ files: files, errors: errors });
});
});
}
function fileReadSuccess(file, fullPath) {
function getFiles(entries) {
return Promise.all(entries.map(function (entry) {
return new Promise(function (resolve, reject) {
if (entry.file) {
var fullPath = entry.fullPath;
entry.file(function (file) {
resolve(getFile(file, fullPath));
}, function (file) {
resolve({
path: entry.fullPath,
error: file
});
});
} else {
resolve(entry);
}
});
}));
}
function getFile(file, fullPath) {
// relative path should not start with "/"
file.relativePath = fullPath.substring(1);
files.push(file);
decrement();
}
function readError(fileError) {
decrement();
throw fileError;
}
function decrement() {
if (--queue == 0) {
$.addFiles(files, event);
}
return file;
}
},

Expand Down Expand Up @@ -588,8 +642,12 @@
* @param {Event} [event] event is optional
*/
addFiles: function (fileList, event) {
var $ = this;
var files = [];
each(fileList, function (file) {
var errors = [];
var promises = [];

function addFile(file) {
// https://github.com/flowjs/flow.js/issues/55
if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
var uniqueIdentifier = this.generateUniqueIdentifier(file);
Expand All @@ -600,16 +658,54 @@
}
}
}
}, this);
if (this.fire('filesAdded', files, event)) {
each(files, function (file) {
if (this.opts.singleFile && this.files.length > 0) {
this.removeFile(this.files[0]);
}
this.files.push(file);
}, this);
this.fire('filesSubmitted', files, event);
}

/**
* Chrome's FileSystem API has a bug that files from dropped folders or files from input dialog's selected folder,
* with read errors (has absolute paths which exceed 260 chars) will have zero file size.
*/
function validateFile(file) {
// files with size greater than zero can upload
if (file.size > 0) {
addFile.bind($)(file);
return;
}

// try to read from from zero size file,
// if error occurs than file cannot be uploaded
promises.push(new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onloadend = function () {
if (reader.error) {
errors.push({
path: file.webkitRelativePath || file.name,
error: reader.error
});
} else {
addFile.bind($)(file);
}
resolve();
}.bind($);
reader.readAsArrayBuffer(file);
}));
}

each(fileList, validateFile);

Promise.all(promises).then(function () {
if (errors.length) {
this.fire('readErrors', errors, [], event);
}
if (this.fire('filesAdded', files, event)) {
each(files, function (file) {
if (this.opts.singleFile && this.files.length > 0) {
this.removeFile(this.files[0]);
}
this.files.push(file);
}, this);
this.fire('filesSubmitted', files, event);
}
}.bind(this));
},


Expand Down Expand Up @@ -716,12 +812,6 @@
*/
this.flowObj = flowObj;

/**
* Used to store the bytes read
* @type {Blob|string}
*/
this.bytes = null;

/**
* Reference to file
* @type {File}
Expand Down Expand Up @@ -937,9 +1027,16 @@
*/
bootstrap: function () {
if (typeof this.flowObj.opts.initFileFn === "function") {
this.flowObj.opts.initFileFn(this);
var ret = this.flowObj.opts.initFileFn(this);
if (ret && 'then' in ret) {
ret.then(this._bootstrap.bind(this));
return;
}
}
this._bootstrap();
},

_bootstrap: function () {
this.abort(true);
this.error = false;
// Rebuild stack of chunks from file
Expand Down Expand Up @@ -1144,6 +1241,11 @@
*/
this.readState = 0;

/**
* Used to store the bytes read
* @type {Blob|string}
*/
this.bytes = undefined;

/**
* Bytes transferred from total request size
Expand Down Expand Up @@ -1280,14 +1382,14 @@
*/
getParams: function () {
return {
flowChunkNumber: this.offset + 1,
flowChunkSize: this.chunkSize,
flowCurrentChunkSize: this.endByte - this.startByte,
flowTotalSize: this.fileObj.size,
flowIdentifier: this.fileObj.uniqueIdentifier,
flowFilename: this.fileObj.name,
flowRelativePath: this.fileObj.relativePath,
flowTotalChunks: this.fileObj.chunks.length
chunkNumber: this.offset + 1,
chunkSize: this.chunkSize,
currentChunkSize: this.endByte - this.startByte,
totalSize: this.fileObj.size,
requestId: this.fileObj.uniqueIdentifier,
filename: this.fileObj.name,
relativePath: this.fileObj.relativePath,
totalChunks: this.fileObj.chunks.length
};
},

Expand Down
Loading