Як перевірити тип файлу 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, класний чувак! Переглянемо це, коли у мене буде час. Дякую :)
Переповнення запитань

Ви впевнені, що ваш тестовий файл все ще має міметик image/jpeg, і ви насправді не змінили його, змінивши розширення?
Бергі

Відповіді:


343

Ви можете легко визначити тип файлу 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>елемента ( 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>

Ось версія перетягування вище ( 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 повертаються таким чином:

image / jpeg
image / png
...

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


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

Щоб отримати тип bonafide 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) Для того, щоб скопіювати / вставити підписи файлів, чи не слід будувати заголовок із ведучими 0 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-байтні підписи. Я сподіваюся, що це допомагає.
Дрейки

23
Якість цієї відповіді просто вражаюча.
Лука

2
Вам не доведеться завантажувати повний блоб як ArrayBuffer, щоб визначити mimeType. Ви можете просто нарізати і пройти перші 4 байти краплі так:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine

2
Якою має бути чек, щоб дозволити лише звичайний текст? Перші 4 байти для текстових файлів здаються першими 4 символами текстового файлу.
MP 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>


Я думаю 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;
}

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
Reid

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.

Повний список типів mime див. У Вікіпедії .

Повний список магічного числа див. У Вікіпедії .


Наведені вище посилання на Вікіпедію більше не дійсні.
Боб Куінн

@BobQuinn виправлено,
thansk

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.

Я зміг послідовно отримати наступне з обох зображень у хромі (слід працювати в сучасних браузерах).

ÿØÿà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] )) );

  • Розбиває кодовані img дані 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, щоб це теж не було).
Корнель

Дивіться вгорі "Коротка відповідь - ні".
Лекс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.