17

I have recently contributed some code to Moodle which uses some of the capabilities of HTML5 to allow files to be uploaded in forms via drag and drop from the desktop (the core part of the code is here: https://github.com/moodle/moodle/blob/master/lib/form/dndupload.js for reference).

This is working well, except for when a user drags a folder / directory instead of a real file. Garbage is then uploaded to the server, but with the filename matching the folder.

What I am looking for is an easy and reliable way to detect the presence of a folder in the FileList object, so I can skip it (and probably return a friendly error message as well).

I've looked through the documentation on MDN, as well as a more general web search, but not turned up anything. I've also looked through the data in the Chrome developer tools and it appears that the 'type' of the File object is consistently set to "" for folders. However, I'm not quite convinced this is the most reliable, cross-browser detection method.

Does anyone have any better suggestions?

VDP
  • 6,340
  • 4
  • 31
  • 53
davosmith
  • 6,037
  • 2
  • 14
  • 23

7 Answers7

25

You cannot rely on file.type. A file without an extension will have a type of "". Save a text file with a .jpg extension and load it into a file control, and its type will display as image/jpeg. And, a folder named "someFolder.jpg" will also have its type as image/jpeg.

Instead, try to read the first byte of the file. If you are able to read the first byte, you have a file. If an error is thrown, you probably have a directory:

try {
    await file.slice(0, 1).arrayBuffer();
    // it's a file!
}
catch (err) {
    // it's a directory!
}

If you are in the unfortunate position of supporting IE11, The file will not have the arrayBuffer method. You have to resort to the FileReader object:

// use this code if you support IE11
var reader = new FileReader();
reader.onload = function (e) {
    // it's a file!
};
reader.onerror = function (e) {
    // it's a directory!
};
reader.readAsArrayBuffer(file.slice(0, 1));
gilly3
  • 87,962
  • 25
  • 144
  • 176
  • Thanks for the suggestion - I was wondering if rejecting zero sized files would be a good starting point (this seems to be the size of folders on Windows) and then using FileReader to check if the contents is \0 when File.size % 4096 is zero – davosmith Jan 14 '12 at 12:47
  • Absolutely. The most surprising discovery for me was that size was not consistently 0 for folders. The `% 4096` thing is interesting. Certainly, more research is called for. Like, is 4096 universal, or is it specific to 64 bit Windows 7? – gilly3 Jan 15 '12 at 00:51
  • Odd - I was getting size 0 consistently with Win 7 (64bit) (will double-check, but tried with Chrome/Firefox). However, I was getting 4096 bytes (exactly) with Ubuntu 11.10 (64bit) (again with Chrome/Firefox). – davosmith Jan 15 '12 at 20:00
  • Looks like the best solution, as you originally stated, is just to handle on the server. – davosmith Jan 17 '12 at 22:53
  • 8
    Do **NOT** rely on 4096 or 0. On Linux it can be more than 4096, for instance 8192 or more, depending on how many files the folder contains (or used to contain, as removing files won't decrease the size). On Mac OS X you can have various small numbers as the directory size, like 68, 102, 136, 170, and growing as it contains more files. Basically the size is not something you can use. – Florent Guillaume May 21 '13 at 17:40
  • @FlorentGuillaume - I agree, do not rely on that number. I hope it doesn't sound like I'm suggesting in my answer that it is at all reliable. Just interesting. That being said, `8192 % 4096` *is* zero. – gilly3 May 21 '13 at 18:21
  • The type check is also fragile. A folder named `hello.txt` will be reported as type `text/plain` – Fabian Jakobs May 24 '13 at 15:31
  • @FabianJakobs - Indeed. Did you read the first paragraph of my answer? – gilly3 May 24 '13 at 17:07
  • Note that the `Chrome` method works on any webkit-based browser. – Daan Sep 04 '15 at 18:55
  • 2
    I would recommend calling `reader.readAsText(file.slice(0,5)` to avoid reading large files into memory. – Dave Cherkassky May 18 '16 at 15:14
  • Should also note that FileReader implemented with onload and onerror like this does not work well for large files. A 10GB file hits the onerror event. – MortenMoulder Aug 04 '20 at 07:33
  • @MortenMoulder - You can use `file.slice(0, 1)` to get a File object consisting of only the first byte of the file. The `FileReader` trick will then work fine. You can also use `file.arrayBuffer()`, as a convenient, promise-based alternative to `FileReader`. Perhaps my answer could benefit from an update, since it has been 8.5 years since I first answered. – gilly3 Aug 05 '20 at 00:00
  • 1
    Unfortunately neither solution works in Firefox (tried on v94.0). Behavior for folder is the same as for file. – undeletable Dec 02 '21 at 17:51
  • The same approach with file.text() instead of file.slice(0, 1).arrayBuffer() looks like working fine – undeletable Dec 02 '21 at 18:20
  • 2
    using Firefox 99 and `file.slice(0,1).text()` or `file.slice(0,1).arrayBuffer()` will throw for only some folders, while others pass the test. Same for Chrome. – Marcus Krahl Apr 08 '22 at 04:59
8

I also ran into this problem and below is my solution. Basically, I took have a two pronged approach:

(1) check whether the File object's size is large, and consider it to be a genuine file if it is over 1MB (I'm assuming folders themselves are never that large). (2) If the File object is smaller than 1MB, then I read it using FileReader's 'readAsArrayBuffer' method. Successful reads call 'onload' and I believe this indicates the file object is a genuine file. Failed reads call 'onerror' and I consider it a directory. Here is the code:

var isLikelyFile = null;
if (f.size > 1048576){ isLikelyFile = false; }
else{
    var reader = new FileReader();
    reader.onload = function (result) { isLikelyFile = true; };
    reader.onerror = function(){ isLikelyFile = false; };
    reader.readAsArrayBuffer(f);
}
//wait for reader to finish : should be quick as file size is < 1MB ;-)
var interval = setInterval(function() {
    if (isLikelyFile != null){
        clearInterval(interval);
        console.log('finished checking File object. isLikelyFile = ' + isLikelyFile);
    }
}, 100);

I tested this in FF 26, Chrome 31, and Safari 6 and three browsers call 'onerror' when attempting to read directories. Let me know if anyone can think of a use case where this fails.

Nick G.
  • 579
  • 6
  • 6
  • Good approach. Except of black magic with setInterval, which I find error-prone. I'd better use async callback system and let reader finish with success or failure. Also your readAsArrayBuffer() misses parameter. – Pointer Null Jan 23 '14 at 10:55
  • Good catch with readAsArrayBuffer - I updated the code to reflect the bug fix (and to fix another bug : isLikelyFile is now false if the file size is > 1 MB). I haven't had trouble with setInterval, but good to keep in mind. – Nick G. Jan 23 '14 at 15:01
  • Note that the `(f.size > 1048576)` optimization in some cases will be problematic, since folder size can actually be larger than that. For a folder with a large number of files, I checked the size of my temp folder (Windows 7), and it's 2883584 bytes... – Daryn Jan 08 '15 at 19:56
  • 2
    Should presumably read: if (f.size > 1048576){ isLikelyFile = true; } – jarmod Jul 08 '15 at 20:54
  • This doesn't work in Firefox 94.0 (onload is executed for directory as well) – undeletable Dec 02 '21 at 18:02
  • Replacing readAsArrayBuffer with readAsText looks like working fine, though. – undeletable Dec 02 '21 at 18:21
2

I proposing calling FileReader.readAsBinaryString on the File object. In Firefox, this will raise an Exception when the File is a Directory. I only do this if the File meets the conditions proposed by gilly3.

Please see my blog post at http://hs2n.wordpress.com/2012/08/13/detecting-folders-in-html-drop-area/ for more details.

Also, version 21 of Google Chrome now supports dropping folders. You can easily check if the dropped items are folders, and also read their contents.

Unfortunately, I don´t have any (client-side) solution for older Chrome versions.

Marko
  • 20,385
  • 13
  • 48
  • 64
Richie
  • 21
  • 4
0

One other note is that type is "" for any file that has an unknown extension. Try uploading a file named test.blah and the type will be empty. AND... try dragging and dropping a folder named test.jpg - type will be set to "image/jpeg". To be 100% correct, you can't depend on type solely (or if at all, really).

In my testing, folders have always been of size 0 (on FF and Chrome on 64-bit Windows 7 and under Linux Mint (Ubuntu essentially). So, my folder check is just checking if size is 0 and it seems to work for me in our environment. We also don't want 0-byte files uploaded either so if it's 0 byte the message comes back as "Skipped - 0 bytes (or folder)"

Brian Pipa
  • 808
  • 9
  • 23
0

FYI, this post will tell you how to use dataTransfer API in Chrome to detect file type: http://updates.html5rocks.com/2012/07/Drag-and-drop-a-folder-onto-Chrome-now-available

Nam Nguyen
  • 5,668
  • 14
  • 56
  • 70
0

The best option is to use both the 'progress' and 'load' events on a FileReader instance.

var fr = new FileReader();
var type = '';

// Early terminate reading files.
fr.addEventListener('progress', function(e) {
    console.log('progress - valid file');

    fr.abort();

    type = 'file';
});

// The whole file loads before a progress event happens.
fr.addEventListener('load', function(e) {
    console.log('load - valid file');

    type = 'file';
});

// Not a file.  Possibly a directory.
fr.addEventListener('error', function(e) {
    console.log('error - not a file or is not readable by the web browser');
});

fr.readAsArrayBuffer(thefile);

This fires the error handler when presented with a directory and most files will fire the progress handler after reading just a few KB. I've seen both events fire. Triggering abort() in the progress handler stops the FileReader from reading more data off disk into RAM. That allows for really large files to be dropped without reading all of the data of such files into RAM just to determine that they are files.

It may be tempting to say that if an error happens that the File is a directory. However, a number of scenarios exist where the File is unreadable by the web browser. It is safest to just report the error to the user and ignore the item.

CubicleSoft
  • 2,274
  • 24
  • 20
0

An easy method is the following:

  1. Check if the file's type is an empty string: type === ""
  2. Check if the file's size is 0, 4096, or a multiple of it: size % 4096 === 0.
if (file.type === "" && file.size % 4096 === 0) {
    // The file is a folder
} else {
    // The file is not a folder
}

Note: Just by chance, there could be files without a file extension that have the size of some multiple of 4096. Even though this will not happen very often, be aware of it.


For reference, please see the great answer from user Marco Bonelli to a similar topic. This is just a short summary of it.

georg-un
  • 1,123
  • 13
  • 24