Read only the first chunk of a file in Javascript - javascript

We have an browser-based interface where a user selects a file to upload.
When a file is selected, it is loaded into a FileReader object, and the contents is loaded into memory and validated by calling FileReader.readAsText().
This works fine on 50MB files (takes 2-3 seconds).
However when you go up to 200MB in size, and even after 20 mins, the file never finished reading and a processor remains pegged at 100% utilization.
I believe this is due to memory exhaustion, because the entirety of the file is being loaded into a single variable.
Is there any way to load the contents of the file in chunks, rather than loading the entire file into one enormous, memory sucking monster?
All we need is the first 1KB or so of the file to validate, so all we'd need to do is read the first chunk, valid, and be done.
Proof of concept, to test for yourself:
<form>
<input type="file" /> File
</form>
<script type="text/javascript">
var input = document.getElementsByTagName('input')[0];
input.onchange = function(e) {
var file = input.files[0];
var reader = new FileReader();
reader.onload = function() {
var file_contents = reader.result;
alert('File Read. Size:' + file_contents.length);
};
reader.readAsText(file);
};
</script>

I would leverage the Blob interface that File implements, whose .stream() method returns a ReadableStream.
async function validateFile(file) {
const stream = file.stream();
const reader = stream.getReader();
let bytesRead = 0;
let exhausted = false;
while (bytesRead < 1e3 && !exhausted) {
const {value, done} = await reader.read();
// value is a Uint8Array
bytesRead += value ? value.length : 0;
exhausted = done;
}
}

Related

file.slice fails second time

I'm trying to make a (front-end) Javascript that would be able to copy very large files (i.e. read them from a file input element and 'download' them using StreamSaver.js).
This is the actual code:
<html>
<header>
<title>File copying</title>
</header>
<body>
<script src="https://cdn.jsdelivr.net/npm/web-streams-polyfill#2.0.2/dist/ponyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/streamsaver#2.0.3/StreamSaver.min.js"></script>
<script type="text/javascript">
const streamSaver = window.streamSaver;
async function copyFile() {
const fileInput = document.getElementById("fileInput");
const file = fileInput.files[0];
if (!file) {
alert('select a (large) file');
return;
}
const newName = file.name + " - Copy";
let remaining = file.size;
let written = 0;
const chunkSize = 1048576; // 1MB
const writeStream = streamSaver.createWriteStream(newName);
const writer = writeStream.getWriter();
while (remaining > 0) {
let readSize = chunkSize > remaining ? remaining : chunkSize;
let blob = file.slice(written, readSize);
let aBuff = await blob.arrayBuffer();
await writer.write(new Uint8Array(aBuff));
written += readSize;
remaining -= readSize;
}
await writer.close();
}
</script>
<input type="file" id="fileInput"/>
<button onclick="copyFile()">Copy file</button>
</body>
</html>
It seems that during the second loop in the while the aBuff variable value (the blob.arrayBuffer) is an empty ArrayBuffer.
Am I reading the file the wrong way? My intent is to read a (potentially huge) file, chunk by chunk and do something with each chunk (in this case just output it to the downloading file by StreamSaver.js). What better approach is available in today's browsers?
I would go with something like blob.stream() or new Response(blob).body to read all chunks of the file and potentially a TransformStream if needed. But if you need a custom slice size or better browser support than you can create your own blob -> readableStream utility
// Taken from https://www.npmjs.com/package/screw-filereader
function stream (blob) {
var position = 0
var blob = this
return new ReadableStream({
pull (controller) {
var chunk = blob.slice(position, position + 1048576)
return chunk.arrayBuffer().then(buffer => {
position += buffer.byteLength
var uint8array = new Uint8Array(buffer)
controller.enqueue(uint8array)
if (position == blob.size)
controller.close()
})
}
})
}
stream(blob).pipeTo(writeStream)
This way you can just pipe it to streamsaver instead of writing each chunk manually

I want to load images withen the folder and want to get all images in base64 in my js for future use

i am facing the issue i always get the last image in my image array due to kind of Filereader library function onloadend.
how can i get base64 for all images in my folder.
<input id="file-input" multiple webkitdirectory type="file" />
var input = document.getElementById('file-input');
var file_names = "";
var entries_length = 0;
var entries_count = 0;
var image = new Array();
var obj = {};
var j = 0;
input.onchange = function(e) {
var files = e.target.files; // FileList
entries_length = files.length;
console.log(files);
for (var i = 0, f; f = files[i]; ++i){
console.log("i:"+i);
entries_count = entries_count + 1;
//console.debug(files[i].webkitRelativePath);
if(files[i].type=="image/jpeg")
{
var string = files[i].webkitRelativePath;
var name = string.split("/")[3]; //this is because my image in 3rd dir in the folder
var reader = new FileReader();
reader.onloadend = function() {
obj.name = string.split("/")[3];
obj.image = reader.result;
image[j] = obj;
j = j+1;
}
reader.readAsDataURL(files[i]);
}
}
console.log(image);
}
The issue is caused by the asynchronous loading of files. You iterate over the array and set the onloadend handler for the reader each time, then start loading by calling readAsDataURL.
One problem is that by the time your first image loads, it is possible the for loop has completed, and i is already at the last index of the array.
At this point, obtaining the path from files[i].webkitRelativePath will give you the last filename, and not the one you are expecting.
Check the example for readAsDataURL on MDN to see one possible solution - each load is performed in a separate function, which preserves its scope, along with file.name. Do not be put off by the construction they are using: [].forEach.call(files, readAndPreview). This is a way to map over the files, which are a FileList and not a regular array (so the list does not have a forEach method of its own).
So, it should be sufficient to wrap the loading logic in a function which takes the file object as a parameter:
var images = [];
function loadFile(f) {
var reader = new FileReader();
reader.onloadend = function () {
images.push({
name : f.name, // use whatever naming magic you prefer here
image : reader.result
});
};
reader.readAsDataURL(f);
}
for (var i=0; i<files.length; i++) {
loadFile(files[i]);
}
Each call of the function 'remembers' the file object it was called with, and prevents the filename from getting messed up. If you are interested, read up on closures.
This also has the nice effect of isolating your reader objects, because I have a sneaking suspicion that, although you create a new 'local' reader each iteration, javascript scoping rules are weird and readers could also be interfering with each other (what happens if one reader is loading, but in the same scope you create a new reader with the same variable name? Not sure).
Now, you do not know how long it would take for all images to be loaded, so if you want to take an action right after that, you would have to perform a check each time an onloadend gets called. This is the essence of asynchronous behavior.
As an aside, I should note that it is pointless to manually keep track of the last index of images, which is j. You should just use images.push({ name: "bla", image: "base64..." }). Keeping indices manually opens up possibilities for bugs.

Split A large JSON into Smaller parts in Javascript/jQuery

I have a file upload functionality in my application which can not upload JSON files which are more than 10MB in size. If user uploads a file >= 10 MB , My app should split it into smaller JSON files each less than 10MB. Also, the Proper JSON objects needs to be maintained in the new low-sized files.
Is there a way to do this in Javascript or jQuery?
I propose a solution like this without any specific library. It does use a bit of modern techniques but maybe useful to you:
var openFile = function(event, callback) {
// get target input
var input = event.target;
// create an instance of filereader
var reader = new FileReader();
// define handler to get results
reader.onload = function(e){
var contents = e.target.result;
// use a promise maybe to make this neater
callback(contents);
};
// make sure you tell it to read as text
// also maybe add some validation on your input
// for correct types
reader.readAsText(input.files[0]);
};
var getChunks = function(str){
var chunks = [];
// not best at these things but this should be
// around 1mb max
var chunkSize = 1000000;
// while the chunk is less than the size indicated it goes
// into the same item of array
while (str) {
if (str.length < chunkSize) {
chunks.push(str);
break;
}
else {
chunks.push(str.substr(0, chunkSize));
str = str.substr(chunkSize);
}
}
return chunks;
}
var fileInput = document.querySelector('#jsonUpload');
fileInput.addEventListener('change', function(event){
openFile(event, function(str){
console.log(getChunks(str));
});
});
Then it would read the json file from:
<input type='file' accept='*' id="jsonUpload">
Link to the fiddle

How to check file MIME type with JavaScript before upload?

I have read this and this questions which seems to suggest that the file MIME type could be checked using JavaScript on client side. Now, I understand that the real validation still has to be done on server side. I want to perform a client side checking to avoid unnecessary wastage of server resource.
To test whether this can be done on client side, I changed the extension of a JPEG test file to .png and choose the file for upload. Before sending the file, I query the file object using a JavaScript console:
document.getElementsByTagName('input')[0].files[0];
This is what I get on Chrome 28.0:
File {webkitRelativePath: "", lastModifiedDate: Tue Oct 16 2012
10:00:00 GMT+0000 (UTC), name: "test.png", type: "image/png", size:
500055…}
It shows type to be image/png which seems to indicate that the checking is done based on file extension instead of MIME type. I tried Firefox 22.0 and it gives me the same result. But according to the W3C spec, MIME Sniffing should be implemented.
Am I right to say that there is no way to check the MIME type with JavaScript at the moment? Or am I missing something?
You can easily determine the file MIME type with JavaScript's FileReader before uploading it to a server. I agree that we should prefer server-side checking over client-side, but client-side checking is still possible. I'll show you how and provide a working demo at the bottom.
Check that your browser supports both File and Blob. All major ones should.
if (window.FileReader && window.Blob) {
// All the File APIs are supported.
} else {
// File and Blob are not supported
}
Step 1:
You can retrieve the File information from an <input> element like this (ref):
<input type="file" id="your-files" multiple>
<script>
var control = document.getElementById("your-files");
control.addEventListener("change", function(event) {
// When the control has changed, there are new files
var files = control.files,
for (var i = 0; i < files.length; i++) {
console.log("Filename: " + files[i].name);
console.log("Type: " + files[i].type);
console.log("Size: " + files[i].size + " bytes");
}
}, false);
</script>
Here is a drag-and-drop version of the above (ref):
<div id="your-files"></div>
<script>
var target = document.getElementById("your-files");
target.addEventListener("dragover", function(event) {
event.preventDefault();
}, false);
target.addEventListener("drop", function(event) {
// Cancel default actions
event.preventDefault();
var files = event.dataTransfer.files,
for (var i = 0; i < files.length; i++) {
console.log("Filename: " + files[i].name);
console.log("Type: " + files[i].type);
console.log("Size: " + files[i].size + " bytes");
}
}, false);
</script>
Step 2:
We can now inspect the files and tease out headers and MIME types.
✘ Quick method
You can naïvely ask Blob for the MIME type of whatever file it represents using this pattern:
var blob = files[i]; // See step 1 above
console.log(blob.type);
For images, MIME types come back like the following:
image/jpeg
image/png
...
Caveat: The MIME type is detected from the file extension and can be fooled or spoofed. One can rename a .jpg to a .png and the MIME type will be be reported as image/png.
✓ Proper header-inspecting method
To get the bonafide MIME type of a client-side file we can go a step further and inspect the first few bytes of the given file to compare against so-called magic numbers. Be warned that it's not entirely straightforward because, for instance, JPEG has a few "magic numbers". This is because the format has evolved since 1991. You might get away with checking only the first two bytes, but I prefer checking at least 4 bytes to reduce false positives.
Example file signatures of JPEG (first 4 bytes):
FF D8 FF E0 (SOI + ADD0)
FF D8 FF E1 (SOI + ADD1)
FF D8 FF E2 (SOI + ADD2)
Here is the essential code to retrieve the file header:
var blob = files[i]; // See step 1 above
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
var header = "";
for(var i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
console.log(header);
// Check the file signature against known types
};
fileReader.readAsArrayBuffer(blob);
You can then determine the real MIME type like so (more file signatures here and here):
switch (header) {
case "89504e47":
type = "image/png";
break;
case "47494638":
type = "image/gif";
break;
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
case "ffd8ffe3":
case "ffd8ffe8":
type = "image/jpeg";
break;
default:
type = "unknown"; // Or you can use the blob.type as fallback
break;
}
Accept or reject file uploads as you like based on the MIME types expected.
Demo
Here is a working demo for local files and remote files (I had to bypass CORS just for this demo). Open the snippet, run it, and you should see three remote images of different types displayed. At the top you can select a local image or data file, and the file signature and/or MIME type will be displayed.
Notice that even if an image is renamed, its true MIME type can be determined. See below.
Screenshot
// Return the first few bytes of the file as a hex string
function getBLOBFileHeader(url, blob, callback) {
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
var arr = (new Uint8Array(e.target.result)).subarray(0, 4);
var header = "";
for (var i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
callback(url, header);
};
fileReader.readAsArrayBuffer(blob);
}
function getRemoteFileHeader(url, callback) {
var xhr = new XMLHttpRequest();
// Bypass CORS for this demo - naughty, Drakes
xhr.open('GET', '//cors-anywhere.herokuapp.com/' + url);
xhr.responseType = "blob";
xhr.onload = function() {
callback(url, xhr.response);
};
xhr.onerror = function() {
alert('A network error occurred!');
};
xhr.send();
}
function headerCallback(url, headerString) {
printHeaderInfo(url, headerString);
}
function remoteCallback(url, blob) {
printImage(blob);
getBLOBFileHeader(url, blob, headerCallback);
}
function printImage(blob) {
// Add this image to the document body for proof of GET success
var fr = new FileReader();
fr.onloadend = function() {
$("hr").after($("<img>").attr("src", fr.result))
.after($("<div>").text("Blob MIME type: " + blob.type));
};
fr.readAsDataURL(blob);
}
// Add more from http://en.wikipedia.org/wiki/List_of_file_signatures
function mimeType(headerString) {
switch (headerString) {
case "89504e47":
type = "image/png";
break;
case "47494638":
type = "image/gif";
break;
case "ffd8ffe0":
case "ffd8ffe1":
case "ffd8ffe2":
type = "image/jpeg";
break;
default:
type = "unknown";
break;
}
return type;
}
function printHeaderInfo(url, headerString) {
$("hr").after($("<div>").text("Real MIME type: " + mimeType(headerString)))
.after($("<div>").text("File header: 0x" + headerString))
.after($("<div>").text(url));
}
/* Demo driver code */
var imageURLsArray = ["http://media2.giphy.com/media/8KrhxtEsrdhD2/giphy.gif", "http://upload.wikimedia.org/wikipedia/commons/e/e9/Felis_silvestris_silvestris_small_gradual_decrease_of_quality.png", "http://static.giantbomb.com/uploads/scale_small/0/316/520157-apple_logo_dec07.jpg"];
// Check for FileReader support
if (window.FileReader && window.Blob) {
// Load all the remote images from the urls array
for (var i = 0; i < imageURLsArray.length; i++) {
getRemoteFileHeader(imageURLsArray[i], remoteCallback);
}
/* Handle local files */
$("input").on('change', function(event) {
var file = event.target.files[0];
if (file.size >= 2 * 1024 * 1024) {
alert("File size must be at most 2MB");
return;
}
remoteCallback(escape(file.name), file);
});
} else {
// File and Blob are not supported
$("hr").after( $("<div>").text("It seems your browser doesn't support FileReader") );
} /* Drakes, 2015 */
img {
max-height: 200px
}
div {
height: 26px;
font: Arial;
font-size: 12pt
}
form {
height: 40px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<form>
<input type="file" />
<div>Choose an image to see its file signature.</div>
</form>
<hr/>
As stated in other answers, you can check the mime type by checking the signature of the file in the first bytes of the file.
But what other answers are doing is loading the entire file in memory in order to check the signature, which is very wasteful and could easily freeze your browser if you select a big file by accident or not.
/**
* Load the mime type based on the signature of the first bytes of the file
* #param {File} file A instance of File
* #param {Function} callback Callback with the result
* #author Victor www.vitim.us
* #date 2017-03-23
*/
function loadMime(file, callback) {
//List of known mimes
var mimes = [
{
mime: 'image/jpeg',
pattern: [0xFF, 0xD8, 0xFF],
mask: [0xFF, 0xFF, 0xFF],
},
{
mime: 'image/png',
pattern: [0x89, 0x50, 0x4E, 0x47],
mask: [0xFF, 0xFF, 0xFF, 0xFF],
}
// you can expand this list #see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
];
function check(bytes, mime) {
for (var i = 0, l = mime.mask.length; i < l; ++i) {
if ((bytes[i] & mime.mask[i]) - mime.pattern[i] !== 0) {
return false;
}
}
return true;
}
var blob = file.slice(0, 4); //read the first 4 bytes of the file
var reader = new FileReader();
reader.onloadend = function(e) {
if (e.target.readyState === FileReader.DONE) {
var bytes = new Uint8Array(e.target.result);
for (var i=0, l = mimes.length; i<l; ++i) {
if (check(bytes, mimes[i])) return callback("Mime: " + mimes[i].mime + " <br> Browser:" + file.type);
}
return callback("Mime: unknown <br> Browser:" + file.type);
}
};
reader.readAsArrayBuffer(blob);
}
//when selecting a file on the input
fileInput.onchange = function() {
loadMime(fileInput.files[0], function(mime) {
//print the output to the screen
output.innerHTML = mime;
});
};
<input type="file" id="fileInput">
<div id="output"></div>
For anyone who's looking to not implement this themselves, Sindresorhus has created a utility that works in the browser and has the header-to-mime mappings for most documents you could want.
https://github.com/sindresorhus/file-type
You could combine Vitim.us's suggestion of only reading in the first X bytes to avoid loading everything into memory with using this utility (example in es6):
import fileType from 'file-type'; // or wherever you load the dependency
const blob = file.slice(0, fileType.minimumBytes);
const reader = new FileReader();
reader.onloadend = function(e) {
if (e.target.readyState !== FileReader.DONE) {
return;
}
const bytes = new Uint8Array(e.target.result);
const { ext, mime } = fileType.fromBuffer(bytes);
// ext is the desired extension and mime is the mimetype
};
reader.readAsArrayBuffer(blob);
Here is a Typescript implementation that supports webp.
This is based on the JavaScript answer by Vitim.us.
interface Mime {
mime: string;
pattern: (number | undefined)[];
}
// tslint:disable number-literal-format
// tslint:disable no-magic-numbers
const imageMimes: Mime[] = [
{
mime: 'image/png',
pattern: [0x89, 0x50, 0x4e, 0x47]
},
{
mime: 'image/jpeg',
pattern: [0xff, 0xd8, 0xff]
},
{
mime: 'image/gif',
pattern: [0x47, 0x49, 0x46, 0x38]
},
{
mime: 'image/webp',
pattern: [0x52, 0x49, 0x46, 0x46, undefined, undefined, undefined, undefined, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50],
}
// You can expand this list #see https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
];
// tslint:enable no-magic-numbers
// tslint:enable number-literal-format
function isMime(bytes: Uint8Array, mime: Mime): boolean {
return mime.pattern.every((p, i) => !p || bytes[i] === p);
}
function validateImageMimeType(file: File, callback: (b: boolean) => void) {
const numBytesNeeded = Math.max(...imageMimes.map(m => m.pattern.length));
const blob = file.slice(0, numBytesNeeded); // Read the needed bytes of the file
const fileReader = new FileReader();
fileReader.onloadend = e => {
if (!e || !fileReader.result) return;
const bytes = new Uint8Array(fileReader.result as ArrayBuffer);
const valid = imageMimes.some(mime => isMime(bytes, mime));
callback(valid);
};
fileReader.readAsArrayBuffer(blob);
}
// When selecting a file on the input
fileInput.onchange = () => {
const file = fileInput.files && fileInput.files[0];
if (!file) return;
validateImageMimeType(file, valid => {
if (!valid) {
alert('Not a valid image file.');
}
});
};
<input type="file" id="fileInput">
If you just want to check if the file uploaded is an image you can just try to load it into <img> tag an check for any error callback.
Example:
var input = document.getElementsByTagName('input')[0];
var reader = new FileReader();
reader.onload = function (e) {
imageExists(e.target.result, function(exists){
if (exists) {
// Do something with the image file..
} else {
// different file format
}
});
};
reader.readAsDataURL(input.files[0]);
function imageExists(url, callback) {
var img = new Image();
img.onload = function() { callback(true); };
img.onerror = function() { callback(false); };
img.src = url;
}
This is what you have to do
var fileVariable =document.getElementsById('fileId').files[0];
If you want to check for image file types then
if(fileVariable.type.match('image.*'))
{
alert('its an image');
}
As Drake states this could be done with FileReader. However, what I present here is a functional version. Take in consideration that the big problem with doing this with JavaScript is to reset the input file. Well, this restricts to only JPG (for other formats you will have to change the mime type and the magic number):
<form id="form-id">
<input type="file" id="input-id" accept="image/jpeg"/>
</form>
<script type="text/javascript">
$(function(){
$("#input-id").on('change', function(event) {
var file = event.target.files[0];
if(file.size>=2*1024*1024) {
alert("JPG images of maximum 2MB");
$("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
return;
}
if(!file.type.match('image/jp.*')) {
alert("only JPG images");
$("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
return;
}
var fileReader = new FileReader();
fileReader.onload = function(e) {
var int32View = new Uint8Array(e.target.result);
//verify the magic number
// for JPG is 0xFF 0xD8 0xFF 0xE0 (see https://en.wikipedia.org/wiki/List_of_file_signatures)
if(int32View.length>4 && int32View[0]==0xFF && int32View[1]==0xD8 && int32View[2]==0xFF && int32View[3]==0xE0) {
alert("ok!");
} else {
alert("only valid JPG images");
$("#form-id").get(0).reset(); //the tricky part is to "empty" the input file here I reset the form.
return;
}
};
fileReader.readAsArrayBuffer(file);
});
});
</script>
Take in consideration that this was tested on latest versions of Firefox and Chrome, and on IExplore 10.
For a complete list of mime types see Wikipedia.
For a complete list of magic number see Wikipedia.
I needed to check for a few more file types.
Following up the excellent answer given by Drakes, I came up with the below code after I found this website with a very extensive table of file types and their headers. Both in Hex and String.
I also needed an asynchronous function to deal with many files and other problems related to the project I'm working that does not matter here.
Here is the code in vanilla javascript.
// getFileMimeType
// #param {Object} the file object created by the input[type=file] DOM element.
// #return {Object} a Promise that resolves with the MIME type as argument or undefined
// if no MIME type matches were found.
const getFileMimeType = file => {
// Making the function async.
return new Promise(resolve => {
let fileReader = new FileReader();
fileReader.onloadend = event => {
const byteArray = new Uint8Array(event.target.result);
// Checking if it's JPEG. For JPEG we need to check the first 2 bytes.
// We can check further if more specific type is needed.
if(byteArray[0] == 255 && byteArray[1] == 216){
resolve('image/jpeg');
return;
}
// If it's not JPEG we can check for signature strings directly.
// This is only the case when the bytes have a readable character.
const td = new TextDecoder("utf-8");
const headerString = td.decode(byteArray);
// Array to be iterated [<string signature>, <MIME type>]
const mimeTypes = [
// Images
['PNG', 'image/png'],
// Audio
['ID3', 'audio/mpeg'],// MP3
// Video
['ftypmp4', 'video/mp4'],// MP4
['ftypisom', 'video/mp4'],// MP4
// HTML
['<!DOCTYPE html>', 'text/html'],
// PDF
['%PDF', 'application/pdf']
// Add the needed files for your case.
];
// Iterate over the required types.
for(let i = 0;i < mimeTypes.length;i++){
// If a type matches we return the MIME type
if(headerString.indexOf(mimeTypes[i][0]) > -1){
resolve(mimeTypes[i][1]);
return;
}
}
// If not is found we resolve with a blank argument
resolve();
}
// Slice enough bytes to get readable strings.
// I chose 32 arbitrarily. Note that some headers are offset by
// a number of bytes.
fileReader.readAsArrayBuffer(file.slice(0,32));
});
};
// The input[type=file] DOM element.
const fileField = document.querySelector('#file-upload');
// Event to detect when the user added files.
fileField.onchange = event => {
// We iterate over each file and log the file name and it's MIME type.
// This iteration is asynchronous.
Array.from(fileField.files, async file => {
console.log(file.name, await getFileMimeType(file));
});
};
Notice that in the getFileMimeType function you can employ 2 approaches to find the correct MIME type.
Search the bytes directly.
Search for Strings after converting the bytes to string.
I used the first approach with JPEG because what makes it identifiable are the first 2 bytes and those bytes are not readable string characters.
With the rest of the file types I could check for readable string character signatures. For example:
[video/mp4] -> 'ftypmp4' or 'ftypisom'
If you need to support a file that is not on the Gary Kessler's list, you can console.log() the bytes or converted string to find a proper signature for the obscure file you need to support.
Note1: The Gary Kessler's list has been updated and the mp4 signatures are different now, you should check it when implementing this.
Note2: the Array.from is designed to use a .map like function as it second argument.
Here's a minimal typescript/promise util for the browser;
export const getFileHeader = (file: File): Promise<string> => {
return new Promise(resolve => {
const headerBytes = file.slice(0, 4); // Read the first 4 bytes of the file
const fileReader = new FileReader();
fileReader.onloadend = (e: ProgressEvent<FileReader>) => {
const arr = new Uint8Array(e?.target?.result as ArrayBufferLike).subarray(
0,
4,
);
let header = '';
for (let i = 0; i < arr.length; i++) {
header += arr[i].toString(16);
}
resolve(header);
};
fileReader.readAsArrayBuffer(headerBytes);
});
};
Use like so in your validation (I needed a PDF check);
// https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
const pdfBytePattern = "25504446"
const fileHeader = await getFileHeader(file)
const isPdf = fileHeader === pdfBytePattern // => true
Here is an extension of Roberto14's answer that does the following:
THIS WILL ONLY ALLOW IMAGES
Checks if FileReader is available and falls back to extension checking if it is not available.
Gives an error alert if not an image
If it is an image it loads a preview
** You should still do server side validation, this is more a convenience for the end user than anything else. But it is handy!
<form id="myform">
<input type="file" id="myimage" onchange="readURL(this)" />
<img id="preview" src="#" alt="Image Preview" />
</form>
<script>
function readURL(input) {
if (window.FileReader && window.Blob) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
var img = new Image();
img.onload = function() {
var preview = document.getElementById('preview');
preview.src = e.target.result;
};
img.onerror = function() {
alert('error');
input.value = '';
};
img.src = e.target.result;
}
reader.readAsDataURL(input.files[0]);
}
}
else {
var ext = input.value.split('.');
ext = ext[ext.length-1].toLowerCase();
var arrayExtensions = ['jpg' , 'jpeg', 'png', 'bmp', 'gif'];
if (arrayExtensions.lastIndexOf(ext) == -1) {
alert('error');
input.value = '';
}
else {
var preview = document.getElementById('preview');
preview.setAttribute('alt', 'Browser does not support preview.');
}
}
}
</script>
For Png files you can do even more checking than just checking for some magic header bytes, as Png files have a particular file format that you can check.
TLDR: there are a series of chunks that must be in a specific order, and each chunk has a crc error correction code that you can check if it is valid.
https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format
I have made a little library that checks that the chunk layout is correct, and it checks that the crc code for each chunk is valid. Ready to consume as a npm package here:
https://www.npmjs.com/package/png-validator
Short answer is no.
As you note the browsers derive type from the file extension. Mac preview also seems to run off the extension. I'm assuming its because its faster reading the file name contained in the pointer, rather than looking up and reading the file on disk.
I made a copy of a jpg renamed with png.
I was able to consistently get the following from both images in chrome (should work in modern browsers).
ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90
Which you could hack out a String.indexOf('jpeg') check for image type.
Here is a fiddle to explore http://jsfiddle.net/bamboo/jkZ2v/1/
The ambigious line I forgot to comment in the example
console.log( /^(.*)$/m.exec(window.atob( image.src.split(',')[1] )) );
Splits the base64 encoded img data, leaving on the image
Base64 decodes the image
Matches only the first line of the image data
The fiddle code uses base64 decode which wont work in IE9, I did find a nice example using VB script that works in IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html
The code to load the image was taken from Joel Vardy, who is doing some cool image canvas resizing client side before uploading which may be of interest https://joelvardy.com/writing/javascript-image-upload

file input does not update except in Chrome

I have a local program which writes a JSON object to a file so that a JavaScript can pick up its data and process it. The file is selected using an <input> object:
<form id = "getfiles">
<input type = "file" multiple id = "files" />
</form>
with the following JS function setInterval to repeat every 300ms. However, when the file changes, only Google Chrome reloads the file and processes the new content; I have to manually reselect the file on the page in IE 10 and Firefox 20.
function speakText()
{
var thefile = document.getElementById('files').files[0];
var lastChanged = thefile.lastModifiedDate;
var reader = new FileReader();
reader.onload = function(event)
{
var lcd = document.getElementById("last_change_date");
if (!lcd)
{
var spanLastChanged = document.createElement("span");
spanLastChanged.id = "last_change_date";
spanLastChanged.innerText = lastChanged;
console.log(lastChanged);
document.body.appendChild(spanLastChanged);
}
else
{
// compare lastChanged with last_change_date
var last_known_change = Date.parse(lcd.innerText);
// var last_known_change = Date.parse(thefile.lastModifiedDate);
if (last_known_change !== Date.parse(lastChanged))
{
console.log("Something is new since " + lcd.innerText);
var fileContent = event.target.result;
var commands = JSON.parse(fileContent);
handleJSON(fileContent);
lcd.innerText = lastChanged;
}
}
}
reader.readAsText(thefile, "UTF-8");
}
Firefox and IE are doing the right thing per spec: the File objects associated with a file input are supposed to be immutable snapshots of a file at the point when the File object was created. It's a known bug in WebKit/Blink that they just store a reference to the file's data, so that mutating the data will change what the File object sees.
In fact, the WebKit/Blink behavior is a privacy bug: when a user selects a file in a file input, they are giving a web page permission to read the data of the file at that time, not for all future versions of the file! Which is why the spec is written as it is.

Categories

Resources