Как проверить тип файла MIME с помощью JavaScript перед загрузкой?

177

Я прочитал это и это вопросы, которые, кажется, предполагают, что MIME-тип файла можно проверить с помощью javascript на стороне клиента. Теперь я понимаю, что настоящая проверка еще должна выполняться на стороне сервера. Я хочу выполнить проверку на стороне клиента, чтобы избежать ненужной потери ресурсов сервера.

Чтобы проверить, можно ли это сделать на стороне клиента, я изменил расширение JPEGтестового файла на .pngи выбрал файл для загрузки. Перед отправкой файла я запрашиваю объект файла с помощью консоли javascript:

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

Вот что я получаю на Chrome 28.0:

Файл {webkitRelativePath: "", lastModifiedDate: вторник, 16 октября 2012 г., 10:00:00 GMT + 0000 (UTC), имя: "test.png", тип: "image / png", размер: 500055…}

Он показывает тип, image/pngкоторый, по-видимому, указывает на то, что проверка выполняется на основе расширения файла, а не MIME-типа. Я попробовал Firefox 22.0, и он дает мне тот же результат. Но согласно спецификации W3C , MIME Sniffing должен быть реализован.

Прав ли я сказать, что в настоящее время нет способа проверить тип MIME с помощью javascript? Или я что-то упустил?

Вопрос переполнен
источник
5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Я не понимаю, почему вы говорите, что проверка должна выполняться на стороне сервера, но потом говорите, что хотите уменьшить ресурсы сервера. Золотое правило: никогда не доверяйте пользовательскому вводу . Какой смысл проверять тип MIME на стороне клиента, если вы просто делаете это на стороне сервера. Конечно, это «ненужная потеря ресурсов клиента »?
Ян Кларк
7
Хорошая идея - обеспечить лучшую проверку типов файлов и обратную связь для пользователей на стороне клиента. Однако, как вы сказали, браузеры просто полагаются на расширения файлов при определении значения typeсвойства для Fileобъектов. Например, исходный код webkit раскрывает эту истину. Можно точно идентифицировать файлы на стороне клиента, среди прочего, ища в них «волшебные байты». В настоящее время я работаю над библиотекой MIT (в то время, когда у меня мало свободного времени), которая будет заниматься именно этим. Если вы заинтересованы в моем прогрессе, взгляните на github.com/rnicholus/determinater .
Рэй Николус
32
@IanClark, дело в том, что если файл имеет недопустимый тип, я могу отклонить его на стороне клиента, а не тратить пропускную способность загрузки только для отклонения его на стороне сервера.
Вопрос переполнен
@RayNicholus, крутой чувак! Посмотрю, когда у меня будет время. Спасибо :)
Вопрос переполнен
Вы уверены, что ваш тестовый файл все еще имеет mimetype image/jpeg, и вы на самом деле не изменили его, изменив расширение?
Берги

Ответы:

344

Вы можете легко определить тип файла MIME с помощью JavaScript, FileReaderпрежде чем загружать его на сервер. Я согласен, что мы должны предпочесть проверку на стороне сервера, а не на стороне клиента, но проверка на стороне клиента все еще возможна. Я покажу вам, как и предоставить рабочую демонстрацию в нижней части.


Убедитесь, что ваш браузер поддерживает Fileи Blob. Все основные из них должны.

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

Шаг 1:

Вы можете извлечь Fileинформацию из такого <input>элемента, как этот ( ссылка ):

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

✘ Быстрый метод

Вы можете наивно запросить у Blob тип MIME любого файла, который он представляет, используя этот шаблон:

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

Для изображений MIME-типы возвращаются следующим образом:

изображение / JPEG
изображение / PNG
...

Предостережение: тип MIME определяется по расширению файла и может быть обманутым или поддельным. Можно переименовать a .jpgв a, .pngи тип MIME будет указан как image/png.


✓ Правильный метод проверки заголовка

Чтобы получить истинный 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. Увидеть ниже.

Скриншот

Ожидаемый выход демо


Дрейки
источник
8
2 незначительных комментария. (1) Не лучше ли разделить файл на первые 4 байта перед чтением? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Для того, чтобы копировать / вставлять подписи файлов, не должен ли заголовок быть составлен с начальными нулями for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Мэтью Мэдсон
1
@Deadpool Смотрите здесь . Есть более, менее распространенные форматы JPEG от разных производителей. Например, FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. Ключевая часть подписи JPEG составляет всего 2 байта, но для уменьшения ложных срабатываний я добавил наиболее распространенные 4-байтовые подписи. Надеюсь, это поможет.
Дрейкс
24
Качество этого ответа просто потрясающее.
Лука
2
Вам не нужно загружать полный BLOB-объект как ArrayBuffer, чтобы определить mimeType. Вы можете просто нарезать и передать первые 4 байта fileReader.readAsArrayBuffer(blob.slice(0, 4))
большого двоичного объекта
2
Какой должен быть чек, чтобы разрешить только простой текст? Первые 4 байта для текстовых файлов кажутся первым 4 символам в текстовом файле.
депутат Droid
19

Как указано в других ответах, вы можете проверить тип 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>

Vitim.us
источник
Я думаю, readyStateчто всегда будет FileReader.DONEв обработчике событий ( спецификация W3C ), даже если была ошибка - разве проверка должна быть, если (!e.target.error)вместо этого?
мальчик
5

Для тех, кто хочет не реализовывать это самостоятельно, Sindresorhus создал утилиту, которая работает в браузере и имеет сопоставления заголовка с пантомимой для большинства документов, которые вы можете захотеть.

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";
ssz
4

Если вы просто хотите проверить, является ли загруженный файл изображением, вы можете просто попытаться загрузить его в <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
источник
1
Прекрасно работает, я попробовал взломать загрузчик файлов .gif, и он выдал ошибку :)
pathfinder
4

Это то, что вы должны сделать

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

Если вы хотите проверить типы файлов изображений, то

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Кайлас
источник
В настоящее время не работает для: Firefox для Android, Opera для Android и Safari на iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Рид
3

Вот реализация Typescript, которая поддерживает webp. Это основано на ответе JavaScript от 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">

Эрик Култхард
источник
1

Как утверждает Дрейк, это можно сделать с помощью 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.

Полный список типов пантомимы см. В Википедии .

Полный список магических чисел см. В Википедии .

lmiguelmh
источник
Ссылки на Википедию выше не действительны.
Боб Куинн
@BobQuinn исправлен, спасибо
lmiguelmh
0

Вот расширение ответа 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>
следопыт
источник
-1

Краткий ответ - нет.

Как вы заметили, браузеры происходят typeот расширения файла. Предпросмотр Mac также, кажется, бежит вне расширения. Я предполагаю его, потому что он быстрее читает имя файла, содержащегося в указателе, а не ищет и читает файл на диске.

Я сделал копию JPG, переименованный в PNG.

Мне удалось последовательно получить следующее из обоих изображений в 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] )) );

  • Разбивает im64 данные в кодировке base64, оставляя на изображении
  • Base64 декодирует изображение
  • Соответствует только первой строке данных изображения

В скриптовом коде используется декодирование base64, которое не работает в IE9, я нашел хороший пример использования скрипта VB, который работает в IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Код для загрузки изображения был взят от Джоэла Варди, который перед загрузкой делает несколько классных изображений для изменения размера холста на стороне клиента, которые могут быть интересны https://joelvardy.com/writing/javascript-image-upload

закон
источник
1
Пожалуйста, не ищите в JPEG-файлах подстроку «jpeg», это просто совпадение, которое вы нашли в комментарии. Файлы JPEG не должны содержать его (и, если вы думаете о поиске JFIF, APP0не обязательно содержать JFIF в EXIF-JPEG, так что это тоже не нужно).
Корнел
Смотрите топ "Краткий ответ - нет".
Лекс