Bagaimana cara memeriksa jenis file MIME dengan javascript sebelum mengunggah?

177

Saya telah membaca ini dan ini pertanyaan yang tampaknya menunjukkan bahwa file tipe MIME dapat diperiksa menggunakan javascript di sisi client. Sekarang, saya mengerti bahwa validasi sebenarnya masih harus dilakukan di sisi server. Saya ingin melakukan pengecekan sisi klien untuk menghindari pemborosan sumber daya server yang tidak perlu.

Untuk menguji apakah ini dapat dilakukan di sisi klien, saya mengubah ekstensi JPEGfile uji .pngdan memilih file untuk diunggah. Sebelum mengirim file, saya meminta objek file menggunakan konsol javascript:

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

Ini yang saya dapatkan di Chrome 28.0:

File {webkitRelativePath: "", lastModifiedDate: Sel 16 Okt 2012 10:00:00 GMT + 0000 (UTC), nama: "test.png", ketik: "image / png", ukuran: 500055 ...}

Ini menunjukkan tipe image/pngyang tampaknya mengindikasikan bahwa pemeriksaan dilakukan berdasarkan ekstensi file, bukan tipe MIME. Saya mencoba Firefox 22.0 dan memberi saya hasil yang sama. Namun menurut spesifikasi W3C , MIME Sniffing harus diimplementasikan.

Apakah saya berhak mengatakan bahwa tidak ada cara untuk memeriksa jenis MIME dengan javascript saat ini? Atau apakah saya melewatkan sesuatu?

Pertanyaan Melimpah
sumber
5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Saya tidak mengerti bagaimana mengapa Anda mengatakan bahwa validasi harus dilakukan di sisi server, tetapi kemudian katakan Anda ingin mengurangi sumber daya server. Aturan emas: Jangan pernah percaya pada input pengguna . Apa gunanya memeriksa tipe MIME di sisi klien jika Anda hanya melakukannya di sisi server. Tentunya itu adalah "pemborosan sumber daya klien yang tidak perlu "?
Ian Clark
7
Memberikan pengecekan / umpan balik jenis file yang lebih baik kepada pengguna sisi klien adalah ide yang bagus. Namun, seperti yang telah Anda nyatakan, browser hanya mengandalkan ekstensi file saat menentukan nilai typeproperti untuk Fileobjek. Kode sumber webkit, misalnya, mengungkapkan kebenaran ini. Dimungkinkan untuk secara akurat mengidentifikasi file sisi klien dengan mencari "byte ajaib" dalam file, antara lain. Saat ini saya sedang mengerjakan perpustakaan MIT (dalam waktu senggang apa saya punya) yang akan melakukan itu. Jika Anda tertarik dengan kemajuan saya, lihat di github.com/rnicholus/determinater .
Ray Nicholus
32
@IanClark, intinya adalah bahwa jika file tersebut dari jenis yang tidak valid, saya dapat menolaknya di sisi klien daripada membuang bandwidth pengunggahan hanya untuk menolaknya di sisi server.
Pertanyaan Overflow
@ RayNicholus, Bung keren! Akan melihatnya saat aku punya waktu. Terima kasih :)
Pertanyaan Overflow
Apakah Anda yakin file pengujian Anda masih memiliki mimetype image/jpeg, dan Anda tidak benar-benar memodifikasinya dengan mengubah ekstensi?
Bergi

Jawaban:

344

Anda dapat dengan mudah menentukan jenis file MIME dengan JavaScript FileReadersebelum mengunggahnya ke server. Saya setuju bahwa kita harus memilih pemeriksaan sisi-server daripada sisi-klien, tetapi pemeriksaan sisi-klien masih memungkinkan. Saya akan menunjukkan caranya dan memberikan demo yang berfungsi di bagian bawah.


Pastikan browser Anda mendukung keduanya Filedan Blob. Semua yang utama harus.

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

Langkah 1:

Anda dapat mengambil Fileinformasi dari <input>elemen seperti ini ( 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>

Berikut ini adalah versi drag-and-drop di atas ( 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>

Langkah 2:

Kami sekarang dapat memeriksa file dan mencari header dan tipe MIME.

✘ Metode cepat

Anda bisa bertanya kepada Blob tentang jenis file MIME apa pun yang diwakilinya menggunakan pola ini:

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

Untuk gambar, tipe MIME kembali seperti berikut:

gambar / gambar jpeg
/ png
...

Peringatan: Jenis MIME terdeteksi dari ekstensi file dan bisa dibodohi atau dipalsukan. Satu dapat mengubah nama .jpgke .pngdan jenis MIME akan dilaporkan sebagai image/png.


✓ Metode pemeriksaan header yang benar

Untuk mendapatkan tipe MIME bonafide dari file sisi klien, kita dapat melangkah lebih jauh dan memeriksa beberapa byte pertama dari file yang diberikan untuk membandingkan dengan apa yang disebut angka ajaib . Berhati-hatilah karena itu tidak sepenuhnya langsung karena, misalnya, JPEG memiliki beberapa "angka ajaib". Ini karena formatnya telah berevolusi sejak 1991. Anda mungkin lolos hanya dengan memeriksa dua byte pertama, tetapi saya lebih suka memeriksa setidaknya 4 byte untuk mengurangi false positive.

Contoh tanda tangan file JPEG (4 byte pertama):

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

Berikut adalah kode penting untuk mengambil 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);

Anda kemudian dapat menentukan tipe MIME yang sebenarnya seperti itu (lebih banyak tanda tangan file di sini dan di sini ):

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

Terima atau tolak unggahan file sesuai keinginan berdasarkan jenis MIME yang diharapkan.


Demo

Ini demo yang berfungsi untuk file lokal dan file jarak jauh (saya harus mem-bypass CORS hanya untuk demo ini). Buka cuplikan, jalankan, dan Anda akan melihat tiga gambar jarak jauh dari berbagai jenis ditampilkan. Di bagian atas Anda dapat memilih file gambar atau data lokal , dan tanda tangan file dan / atau tipe MIME akan ditampilkan.

Perhatikan bahwa meskipun gambar diubah namanya, jenis MIME yang sebenarnya dapat ditentukan. Lihat di bawah.

Tangkapan layar

Output demo yang diharapkan


Drakes
sumber
8
2 komentar kecil. (1) Bukankah lebih baik mengiris file menjadi 4 byte pertama sebelum membaca? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Untuk menyalin / menempelkan tanda tangan file, bukankah header harus dibuat dengan memimpin 0 for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson
1
@Eadpool Lihat di sini . Ada lebih banyak format JPEG dari pembuat yang berbeda. Misalnya, FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. Bagian penting dari tanda tangan JPEG hanya 2 byte, tetapi untuk mengurangi false positive saya menambahkan tanda tangan 4-byte yang paling umum. Saya harap itu membantu.
Drakes
24
Kualitas jawaban ini sungguh menakjubkan.
Luca
2
Anda tidak perlu memuat gumpalan lengkap sebagai ArrayBuffer untuk menentukan mimeType. Anda bisa mengiris dan mengirimkan 4 byte pertama dari gumpalan seperti ini:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine
2
Apa yang seharusnya menjadi cek untuk hanya mengizinkan teks biasa? 4 byte pertama untuk file teks tampaknya 4 karakter pertama dalam file teks.
MP Droid
19

Seperti yang dinyatakan dalam jawaban lain, Anda dapat memeriksa jenis pantomim dengan memeriksa tanda tangan file dalam byte pertama file.

Tapi apa jawaban lain yang dilakukan adalah memuat seluruh file dalam memori untuk memeriksa tanda tangan, yang sangat boros dan dapat dengan mudah membekukan browser Anda jika Anda memilih file besar secara tidak sengaja atau tidak.

/**
 * 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
sumber
Saya pikir readyStateakan selalu ada FileReader.DONEdi event handler ( spec W3C ) bahkan jika ada kesalahan - bukankah seharusnya cek itu kalau (!e.target.error)bukan?
boycy
5

Bagi siapa pun yang ingin tidak menerapkan ini sendiri, Sindresorhus telah membuat utilitas yang berfungsi di browser dan memiliki pemetaan header-to-mime untuk sebagian besar dokumen yang Anda inginkan.

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

Anda dapat menggabungkan saran Vitim.us untuk hanya membaca dalam byte X pertama untuk menghindari memuat semuanya ke dalam memori dengan menggunakan utilitas ini (contoh dalam 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);
Vinay
sumber
Bagi saya, versi terbaru perpustakaan tidak berfungsi tetapi "file-type": "12.4.0"bekerja dan saya harus menggunakanimport * as fileType from "file-type";
ssz
4

Jika Anda hanya ingin memeriksa apakah file yang diunggah adalah gambar, Anda bisa mencoba memuatnya ke dalam <img>tag untuk memeriksa kesalahan panggilan balik.

Contoh:

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
sumber
1
Berfungsi bagus, saya mencoba hack pengunggah file .gif dan melemparkan kesalahan :)
pathfinder
4

Inilah yang harus Anda lakukan

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

Jika Anda ingin memeriksa jenis file gambar maka

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
sumber
Saat ini tidak berfungsi untuk: Firefox untuk Android, Opera untuk Android, dan Safari di iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid
3

Berikut ini adalah implementasi Typescript yang mendukung webp. Ini didasarkan pada jawaban JavaScript oleh 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">

Eric Coulthard
sumber
1

Seperti yang dikatakan Drake, ini bisa dilakukan dengan FileReader. Namun, apa yang saya sajikan di sini adalah versi fungsional. Mempertimbangkan bahwa masalah besar dengan melakukan ini dengan JavaScript adalah mengatur ulang file input. Nah, ini membatasi hanya JPG (untuk format lain Anda harus mengubah tipe mime dan angka ajaib ):

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

Pertimbangkan bahwa ini diuji pada versi terbaru Firefox dan Chrome, dan pada IExplore 10.

Untuk daftar lengkap tipe pantomim, lihat Wikipedia .

Untuk daftar lengkap angka ajaib, lihat Wikipedia .

lmiguelmh
sumber
Tautan Wikipedia di atas tidak lagi valid.
Bob Quinn
@BobQuinn diperbaiki, thansk
lmiguelmh
0

Berikut ini adalah perpanjangan dari jawaban Roberto14 yang melakukan hal berikut:

INI HANYA AKAN MENGIZINKAN GAMBAR

Memeriksa apakah FileReader tersedia dan kembali ke pemeriksaan ekstensi jika tidak tersedia.

Memberikan peringatan kesalahan jika bukan gambar

Jika ini adalah gambar, ia memuat pratinjau

** Anda masih harus melakukan validasi sisi server, ini lebih merupakan kenyamanan bagi pengguna akhir daripada yang lainnya. Tapi ini berguna!

<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>
pramuka
sumber
-1

Jawaban singkatnya adalah tidak.

Seperti yang Anda perhatikan, browser berasal typedari ekstensi file. Pratinjau Mac juga tampaknya menjalankan ekstensi. Saya berasumsi karena lebih cepat membaca nama file yang terdapat dalam pointer, daripada melihat ke atas dan membaca file pada disk.

Saya membuat salinan jpg berganti nama menjadi png.

Saya dapat secara konsisten mendapatkan yang berikut dari kedua gambar di chrome (harus bekerja di browser modern).

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

Yang mana Anda bisa meretas cek String.indexOf ('jpeg') untuk jenis gambar.

Berikut adalah biola untuk mengeksplorasi http://jsfiddle.net/bamboo/jkZ2v/1/

Garis yang ambigius yang saya lupa komentari dalam contoh ini

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

  • Membagi data img yang dienkode base64, meninggalkan gambar
  • Base64 menerjemahkan gambar
  • Hanya cocok dengan baris pertama dari data gambar

Kode biola menggunakan decode base64 yang tidak akan berfungsi di IE9, saya memang menemukan contoh yang bagus menggunakan skrip VB yang bekerja di IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Kode untuk memuat gambar diambil dari Joel Vardy, yang sedang melakukan beberapa penyesuaian ukuran kanvas gambar sisi klien sebelum mengunggah yang mungkin menarik https://joelvardy.com/writing/javascript-image-upload

Lex
sumber
1
Tolong jangan mencari JPEG untuk substring "jpeg", itu hanya kebetulan Anda menemukannya di komentar. File JPEG tidak harus mengandungnya (dan jika Anda berpikir untuk mencari JFIFgantinya, well APP0tidak harus mengandung JFIF dalam EXIF-JPEG jadi itu juga keluar).
Kornel
Lihat di atas "Jawaban singkatnya adalah tidak".
Lex