如何在上传前用JavaScript检查文件MIME类型?

我读过的问题,这似乎表明文件MIME类型可以在客户端使用JavaScript检查。现在,我知道真正的验证仍然必须在服务器端完成。我想执行客户端检查,以避免不必要的服务器资源浪费。

为了测试这是否可以在客户端完成,我将JPEG测试文件的扩展名更改为.png,并选择该文件进行上传。在发送文件之前,我使用JavaScript控制台查询文件对象:

document.getElementsByTagName('input')[0].files[0];

这是我在Chrome 28.0上得到的:

File {webkitRelativePath: "", lastModifiedDate: Tue Oct 16 2012 10:00:00 GMT+0000 (UTC),名称:" test.png",类型:" image/png",大小: 500055年…}< / p >

它显示type为image/png,这似乎表明检查是基于文件扩展名而不是MIME类型完成的。我尝试了火狐22.0,它给了我同样的结果。但是根据W3C规范MIME嗅探应该被实现。

我是正确的说,没有办法检查MIME类型与JavaScript的时刻?还是我遗漏了什么?

399690 次浏览

简短的回答是否定的。

正如你所注意到的,浏览器从文件扩展名派生type。Mac预览似乎也运行了扩展。我假设这是因为它更快地读取包含在指针中的文件名,而不是查找和读取磁盘上的文件。

我复制了一个重命名为png的jpg文件。

我能够始终如一地从chrome中的两个图像中获得以下内容(应该在现代浏览器中工作)。

ÿØÿàJFIFÿþ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90

你可以通过String.indexOf('jpeg')检查图像类型。

这里有一个小提琴来探索http://jsfiddle.net/bamboo/jkZ2v/1/

例子中我忘记注释的含糊行

console.log( /^(.*)$/m.exec(window.atob( image.src.split(',')[1] )) );

  • 拆分base64编码的img数据,保留在映像上
  • Base64解码映像
  • 只匹配图像数据的第一行

提琴代码使用base64解码,这不会在IE9中工作,我确实找到了一个很好的例子,使用VB脚本,在IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html中工作

加载图像的代码来自Joel Vardy,他在上传之前正在做一些很酷的图像画布调整客户端,这可能会引起兴趣

在将文件上传到服务器之前,您可以使用JavaScript的FileReader轻松确定文件的MIME类型。我同意我们应该更喜欢服务器端检查而不是客户端检查,但是客户端检查仍然是可能的。我将向您展示如何操作,并在底部提供一个工作演示。


检查浏览器是否同时支持FileBlob。所有主要国家都应该如此。

if (window.FileReader && window.Blob) {
// All the File APIs are supported.
} else {
// File and Blob are not supported
}

步骤1:

你可以像这样从<input>元素中检索File信息(裁判):

<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>

下面是上面的拖放版本(裁判):

<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>

步骤2:

现在我们可以检查文件并梳理出头文件和MIME类型。

& # x2718;快速的方法

你可以naïvely询问它所代表的任何文件的MIME类型,使用以下模式:

var blob = files[i]; // See step 1 above
console.log(blob.type);

对于图像,MIME类型返回如下所示:

< p >图像/ jpeg < br > 图像/ png < br > …< / p >

警告: MIME类型是从文件扩展名检测到的,可以被愚弄或欺骗。可以将.jpg重命名为.png, MIME类型将被报告为image/png


& # x2713;正确的头检方法

为了获得客户端文件的真实MIME类型,我们可以进一步检查给定文件的前几个字节,与所谓的神奇的数字进行比较。要注意的是,这并不完全简单,因为,例如,JPEG有几个“神奇的数字”。这是因为该格式自1991年以来已经发生了变化。您可能只检查前两个字节,但我更喜欢检查至少4个字节,以减少误报。

JPEG文件签名示例(前4个字节):

FF D8 FF E0 (SOI + ADD0)
FF D8 FF E1 (SOI + ADD1)
FF D8 FF E2 (SOI + ADD2)

下面是检索文件头的基本代码:

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);

然后你可以像这样确定真正的MIME类型(更多的文件签名在这里在这里):

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;
}

根据预期的MIME类型接受或拒绝文件上传。


演示

这是一个本地文件而且远程文件的工作演示(我不得不为这个演示绕过CORS)。打开该代码片段并运行它,您应该会看到显示了三个不同类型的远程映像。在顶部,你可以选择一个本地映像数据文件,文件签名和/或MIME类型将会显示出来。

注意,即使重命名了图像,也可以确定其真实的MIME类型。见下文。

截图

 demo预期输出


// 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/>

正如Drake所说,这可以用FileReader完成。然而,我在这里介绍的是一个功能版本。请注意,使用JavaScript执行此操作的最大问题是重置输入文件。好吧,这仅限于JPG(对于其他格式,你必须改变mime类型神奇的数字):

<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>

考虑到这是在最新版本的Firefox和Chrome以及IExplore 10上进行的测试。

有关mime类型的完整列表,请参阅维基百科

有关魔法数字的完整列表,请参阅维基百科

如果你只是想检查上传的文件是否是一张图像,你可以尝试将它加载到<img>标记中,以检查任何错误回调。

例子:

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;
}

以下是Roberto14的回答的扩展:

这将只允许图像

检查FileReader是否可用,如果不可用,则返回到扩展检查。

如果不是图像,则给出错误警报

如果它是一个图像,它加载预览

**你仍然应该做服务器端验证,这对最终用户来说比其他任何东西都更方便。但它很方便!

<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>

这是你必须要做的

var fileVariable =document.getElementsById('fileId').files[0];

如果你想检查图像文件类型,那么

if(fileVariable.type.match('image.*'))
{
alert('its an image');
}

正如在其他答案中所述,您可以通过检查文件的第一个字节中的文件的签名来检查mime类型。

但是其他答案正在做的是在内存中加载整个文件以检查签名,这是非常浪费的,如果你意外地选择了一个大文件,很容易冻结你的浏览器。

/**
* 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>

对于那些不希望自己实现这个功能的人来说,Sindresorhus创建了一个在浏览器中工作的实用程序,并为您想要的大多数文档提供了头到mime的映射。

https://github.com/sindresorhus/file-type

你可以结合受害者。us的建议是只读取前X字节,以避免使用这个实用程序将所有内容加载到内存中(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);
这是一个支持webp的Typescript实现。 这是基于vic .us的JavaScript回答
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">

我需要检查更多的文件类型。

根据给出的优秀的答案,在我发现这个网站具有一个非常广泛的文件类型及其头文件表之后,我想出了下面的代码。都在十六进制和字符串。

我还需要一个异步函数来处理与我正在工作的项目相关的许多文件和其他问题,这些问题在这里并不重要。

下面是香草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));
});


};

注意,在getFileMimeType函数中,您可以使用两种方法来查找正确的MIME类型。

  1. 直接搜索字节。
  2. 在将字节转换为字符串后搜索字符串。

我对JPEG使用第一种方法,因为使其可识别的是前两个字节,而这些字节不是可读的字符串字符。

对于其余的文件类型,我可以检查可读的字符串字符签名。例如: (视频/ mp4)→'ftypmp4' or 'ftypisom'

如果需要支持不在Gary Kessler列表中的文件,可以console.log()字节或转换后的字符串,为需要支持的模糊文件找到合适的签名。

Note1: Gary Kessler的列表已经更新,mp4签名现在不同了,你应该在实现这个时检查它。 注意2:Array.from被设计成使用类似.map的函数作为第二个参数

这是一个最小的typescript/promise util浏览器;

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);
});
};

在你的验证中像这样使用(我需要一个PDF检查);

// https://mimesniff.spec.whatwg.org/#matching-an-image-type-pattern
const pdfBytePattern = "25504446"
const fileHeader = await getFileHeader(file)
const isPdf = fileHeader === pdfBytePattern // => true

对于Png文件,你可以做更多的检查,而不仅仅是检查一些神奇的头字节,因为Png文件有一个特定的文件格式,你可以检查。

TLDR:有一系列必须以特定顺序排列的块,并且每个块都有一个crc错误纠正码,您可以检查它是否有效。

https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format

我做了一个小库,检查块布局是正确的,它检查每个块的crc代码是有效的。准备作为npm包在这里消费:

https://www.npmjs.com/package/png-validator