我已阅读 this 和 this 问题,这些问题似乎表明可以在客户端使用 JavaScript 检查文件 MIME 类型。现在,我知道真正的验证仍然必须在服务器端完成。我想执行客户端检查以避免不必要的服务器资源浪费。
为了测试这是否可以在客户端完成,我将 JPEG
测试文件的扩展名更改为 .png
并选择要上传的文件。在发送文件之前,我使用 JavaScript 控制台查询文件对象:
document.getElementsByTagName('input')[0].files[0];
这是我在 Chrome 28.0 上得到的:
文件 {webkitRelativePath: "", lastModifiedDate: Tue Oct 16 2012 10:00:00 GMT+0000 (UTC), name: "test.png", type: "image/png", size: 500055…}
它显示类型为 image/png
,这似乎表明检查是基于文件扩展名而不是 MIME 类型完成的。我尝试了 Firefox 22.0,它给了我相同的结果。但是根据the W3C spec,应该实现MIME Sniffing。
我是否可以说目前无法使用 JavaScript 检查 MIME 类型?还是我错过了什么?
I want to perform a client side checking to avoid unnecessary wastage of server resource.
我不明白您为什么说必须在服务器端进行验证,然后又说您想减少服务器资源。黄金法则:永远不要相信用户输入。如果您只是在服务器端进行检查,那么在客户端检查 MIME 类型有什么意义。这肯定是“对 client 资源的不必要浪费”?
File
对象的 type
属性值时仅依赖文件扩展名。例如,webkit 源代码就揭示了这个事实。通过在文件中查找“魔术字节”等,可以准确地识别客户端文件。我目前正在研究一个可以做到这一点的 MIT 图书馆(在我仅有的一点空闲时间)。如果您对我的进展感兴趣,请查看 github.com/rnicholus/determinater。
在将文件上传到服务器之前,您可以使用 JavaScript 的 FileReader
轻松确定文件 MIME 类型。我同意我们应该更喜欢服务器端检查而不是客户端检查,但客户端检查仍然是可能的。我将向您展示如何操作并在底部提供一个工作演示。
检查您的浏览器是否同时支持 File
和 Blob
。所有主要的都应该。
if (window.FileReader && window.Blob) {
// All the File APIs are supported.
} else {
// File and Blob are not supported
}
步骤1:
您可以像这样 (ref) 从 <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>
这是上述 (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>
第2步:
我们现在可以检查文件并梳理出标题和 MIME 类型。
✘ 快速方法
您可以使用此模式天真地向 Blob 询问它所代表的任何文件的 MIME 类型:
var blob = files[i]; // See step 1 above
console.log(blob.type);
对于图像,MIME 类型返回如下:
图片/jpeg 图片/png ...
警告: MIME 类型是从文件扩展名中检测到的,可能会被欺骗或欺骗。可以将 .jpg
重命名为 .png
,MIME 类型将报告为 image/png
。
✓ 正确的标头检查方法
要获得客户端文件的真正 MIME 类型,我们可以更进一步,检查给定文件的前几个字节,以与所谓的 magic numbers 进行比较。请注意,这并不完全简单,因为例如 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 类型(更多文件签名 here 和 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;
}
根据预期的 MIME 类型,根据需要接受或拒绝文件上传。
演示
这是本地文件和远程文件的工作演示(为了这个演示,我不得不绕过 CORS)。打开代码片段,运行它,您应该会看到显示的三个不同类型的远程图像。在顶部,您可以选择本地图像或数据文件,并显示文件签名和/或 MIME 类型。
请注意,即使图像被重命名,也可以确定其真正的 MIME 类型。见下文。
截屏
https://i.stack.imgur.com/uwGCV.png
// 以十六进制字符串形式返回文件的前几个字节 function getBLOBFileHeader(url, blob, callback) { var fileReader = new FileReader(); fileReader.onloadend = function(e) { var arr = (new Uint8Array(e.target.result)).subarray(0, 4);变量头 = ""; for (var i = 0; i < arr.length; i++) { header += arr[i].toString(16); } 回调(网址,标头); }; fileReader.readAsArrayBuffer(blob); } function getRemoteFileHeader(url, callback) { var xhr = new XMLHttpRequest(); // 为这个演示绕过 CORS - 淘气,Drakes xhr.open('GET', '//cors-anywhere.herokuapp.com/' + url); xhr.responseType = "blob"; xhr.onload = function() { callback(url, xhr.response); }; xhr.onerror = function() { alert('发生网络错误!'); }; xhr.send(); } 函数 headerCallback(url, headerString) { printHeaderInfo(url, headerString); } 函数 remoteCallback(url, blob) { printImage(blob); getBLOBFileHeader(url, blob, headerCallback); } function printImage(blob) { // 将此图像添加到文档正文中以证明 GET 成功 var fr = new FileReader(); fr.onloadend = function() { $("hr").after($("").attr("src", fr.result)) .after($("
如其他答案所述,您可以通过在文件的第一个字节中检查文件的 signature 来检查 mime 类型。
但是其他答案正在做的是将整个文件加载到内存中以检查签名,这非常浪费并且如果您意外选择了一个大文件,很容易冻结您的浏览器。
/** * 根据文件第一个字节的签名加载 mime 类型 * @param {File} file File 的一个实例 * @param {Function} callback 带有结果的回调 * @author Victor www.vitim.us * @date 2017-03-23 */ function loadMime(file, callback) { //已知 mime 列表 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], } // 可以展开这个列表@参见 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。模式[i] !== 0) { 返回 false; } } 返回真; } var blob = file.slice(0, 4); //读取文件的前4个字节 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
Browser:" + file.type); } }; reader.readAsArrayBuffer(blob); } //当在输入上选择文件时 fileInput.onchange = function() { loadMime(fileInput.files[0], function(mime) { //将输出打印到屏幕 output.innerHTML = mime; }); };
对于不希望自己实现这一点的任何人,Sindresorhus 创建了一个在浏览器中工作的实用程序,并为您可能想要的大多数文档提供标题到 mime 的映射。
https://github.com/sindresorhus/file-type
您可以结合 Vitim.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);
"file-type": "12.4.0"
起作用了,我不得不使用 import * as fileType from "file-type";
如果您只是想检查上传的文件是否是图像,您可以尝试将其加载到 <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;
}
这是你必须做的
var fileVariable =document.getElementsById('fileId').files[0];
如果要检查图像文件类型,那么
if(fileVariable.type.match('image.*'))
{
alert('its an image');
}
这是一个支持 webp 的 Typescript 实现。这是基于 Vitim.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.');
}
});
};
正如 Drake 所说,这可以通过 FileReader 完成。但是,我在这里展示的是一个功能版本。考虑到使用 JavaScript 执行此操作的最大问题是重置输入文件。好吧,这仅限于 JPG(对于其他格式,您必须更改 mime type 和 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>
考虑到这是在最新版本的 Firefox 和 Chrome 以及 IExplore 10 上测试的。
For a complete list of mime types see Wikipedia。
For a complete list of magic number see Wikipedia。
我需要检查更多的文件类型。
继 Drakes 给出的 excellent answer 之后,我在找到 this website 后提出了以下代码,其中包含一个非常广泛的文件类型及其标题表。十六进制和字符串。
我还需要一个异步函数来处理与我正在工作的项目相关的许多文件和其他问题,这些问题在这里无关紧要。
这是香草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 函数中,您可以使用 2 种方法来查找正确的 MIME 类型。
直接搜索字节。将字节转换为字符串后搜索字符串。
我对 JPEG 使用了第一种方法,因为使其可识别的是前 2 个字节,而这些字节不是可读的字符串字符。
对于其他文件类型,我可以检查可读的字符串字符签名。例如:[video/mp4] -> 'ftypmp4' 或 'ftypisom'
如果您需要支持不在 Gary Kessler 列表中的文件,您可以 console.log() 字节或转换后的字符串为您需要支持的晦涩文件找到适当的签名。
Note1: Gary Kessler 的列表已经更新,现在 mp4 签名不同了,你应该在实现时检查它。注意 2:Array.from 旨在使用类似 .map 的函数作为第二个参数。
这是 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>
这是浏览器的最小打字稿/承诺工具;
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
简短的回答是否定的。
正如您所注意到的,浏览器从文件扩展名派生 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 对图像进行解码
仅匹配图像数据的第一行
小提琴代码使用在 IE9 中不起作用的 base64 解码,我确实找到了一个很好的例子,它使用在 IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html 中工作的 VB 脚本
加载图像的代码取自 Joel Vardy,他在上传之前正在做一些很酷的图像画布调整客户端大小,这可能很有趣https://joelvardy.com/writing/javascript-image-upload
JFIF
,那么 APP0
不必在 EXIF-JPEG 中包含 JFIF,所以这也没有了)。
fileReader.readAsArrayBuffer(blob.slice(0,4))
? (2) 为了复制/粘贴文件签名,标题不应该用前导 0 的for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }
构造吗?FF D8 FF E2
= CANNON EOS JPEG 文件,FF D8 FF E3
= SAMSUNG D500 JPEG 文件。 JPEG 签名的关键部分只有 2 个字节,但为了减少误报,我添加了最常见的 4 字节签名。我希望这会有所帮助。fileReader.readAsArrayBuffer(blob.slice(0, 4))