Làm cách nào để kiểm tra loại MIME với javascript trước khi tải lên?


177

Tôi đã đọc nàynày câu hỏi mà dường như gợi ý rằng các tập tin định dạng MIME có thể được kiểm tra bằng javascript về phía khách hàng. Bây giờ, tôi hiểu rằng việc xác thực thực sự vẫn phải được thực hiện ở phía máy chủ. Tôi muốn thực hiện kiểm tra phía máy khách để tránh lãng phí tài nguyên máy chủ không cần thiết.

Để kiểm tra xem điều này có thể được thực hiện ở phía máy khách hay không, tôi đã thay đổi phần mở rộng của JPEGtệp thử nghiệm .pngvà chọn tệp để tải lên. Trước khi gửi tệp, tôi truy vấn đối tượng tệp bằng bảng điều khiển javascript:

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

Đây là những gì tôi nhận được trên Chrome 28.0:

Tệp {webkitRelativePath: "", lastModifiedDate: Tue 16/10/2012 10:00:00 GMT + 0000 (UTC), tên: "test.png", gõ: "image / png", kích thước: 500055.

Nó hiển thị loại image/pngdường như chỉ ra rằng việc kiểm tra được thực hiện dựa trên phần mở rộng tệp thay vì loại MIME. Tôi đã thử Firefox 22.0 và nó cho tôi kết quả tương tự. Nhưng theo thông số kỹ thuật của W3C , MIME Sniffing nên được thực hiện.

Tôi có đúng không khi nói rằng không có cách nào để kiểm tra loại MIME bằng javascript vào lúc này? Hay tôi đang thiếu một cái gì đó?


5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Tôi không hiểu tại sao bạn nói rằng việc xác thực phải được thực hiện ở phía máy chủ, nhưng sau đó nói rằng bạn muốn giảm tài nguyên máy chủ. Nguyên tắc vàng: Không bao giờ tin tưởng đầu vào của người dùng . Điểm kiểm tra loại MIME ở phía máy khách là gì nếu bạn chỉ thực hiện nó ở phía máy chủ. Chắc chắn đó là một sự "lãng phí không cần thiết của tài nguyên khách hàng "?
Ian Clark

7
Cung cấp kiểm tra / phản hồi loại tệp tốt hơn cho phía khách hàng là một ý tưởng tốt. Tuy nhiên, như bạn đã nêu, các trình duyệt chỉ cần dựa vào phần mở rộng tệp khi xác định giá trị của thuộc typetính cho Filecác đối tượng. Mã nguồn webkit, ví dụ, tiết lộ sự thật này. Có thể xác định chính xác các tệp phía máy khách bằng cách tìm kiếm "byte ma thuật" trong các tệp, trong số những thứ khác. Tôi hiện đang làm việc trên một thư viện MIT (trong thời gian rảnh tôi có) sẽ làm việc đó. Nếu bạn quan tâm đến sự tiến bộ của tôi, hãy xem github.com/rnicholus/determinater .
Ray Nicholus

32
@IanClark, vấn đề là nếu tệp thuộc loại không hợp lệ, tôi có thể từ chối nó ở phía máy khách thay vì chỉ lãng phí băng thông tải lên để từ chối nó ở phía máy chủ.
Câu hỏi tràn

@RayNicholus, anh chàng lạnh lùng! Sẽ xem qua nó khi tôi có thời gian. Cảm ơn :)
Câu hỏi tràn vào

Bạn có chắc chắn rằng tệp thử nghiệm của bạn vẫn có mimetype image/jpegvà bạn không thực sự sửa đổi điều đó bằng cách thay đổi tiện ích mở rộng?
Bergi

Câu trả lời:


342

Bạn có thể dễ dàng xác định loại MIME tệp bằng JavaScript FileReadertrước khi tải nó lên máy chủ. Tôi đồng ý rằng chúng ta nên thích kiểm tra phía máy chủ hơn phía máy khách, nhưng vẫn có thể kiểm tra phía máy khách. Tôi sẽ chỉ cho bạn cách và cung cấp một bản demo hoạt động ở phía dưới.


Kiểm tra xem trình duyệt của bạn hỗ trợ cả FileBlob. Tất cả những người quan trọng nên.

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

Bước 1:

Bạn có thể truy xuất Filethông tin từ một <input>yếu tố như thế này ( 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>

Đây là phiên bản kéo và thả của phần trên ( 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>

Bước 2:

Bây giờ chúng ta có thể kiểm tra các tệp và trêu chọc các tiêu đề và các loại MIME.

Phương pháp nhanh

Bạn có thể yêu cầu Blob một cách ngây thơ về loại MIME của bất kỳ tệp nào mà nó đại diện bằng cách sử dụng mẫu này:

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

Đối với hình ảnh, các loại MIME quay lại như sau:

hình ảnh / hình ảnh jpeg
/ png
...

Hãy cẩn thận: Loại MIME được phát hiện từ phần mở rộng tệp và có thể bị đánh lừa hoặc giả mạo. Người ta có thể đổi tên .jpgthành a .pngvà loại MIME sẽ được báo cáo là image/png.


✓ Phương pháp kiểm tra tiêu đề phù hợp

Để có được loại MIME bonafide của tệp phía máy khách, chúng ta có thể tiến thêm một bước và kiểm tra một vài byte đầu tiên của tệp đã cho để so sánh với các số được gọi là ma thuật . Được cảnh báo rằng nó không hoàn toàn đơn giản bởi vì, ví dụ, JPEG có một vài "số ma thuật". Điều này là do định dạng đã phát triển từ năm 1991. Bạn có thể thoát khỏi việc chỉ kiểm tra hai byte đầu tiên, nhưng tôi thích kiểm tra ít nhất 4 byte để giảm dương tính giả.

Chữ ký tệp ví dụ của JPEG (4 byte đầu tiên):

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

Đây là mã cần thiết để lấy tiêu đề tệp:

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

Sau đó, bạn có thể xác định loại MIME thực như vậy (thêm chữ ký tệp ở đâyđây ):

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

Chấp nhận hoặc từ chối tải lên tệp theo ý muốn dựa trên các loại MIME dự kiến.


Bản giới thiệu

Đây là một bản demo hoạt động cho các tệp cục bộ các tệp từ xa (tôi đã phải bỏ qua CORS chỉ cho bản demo này). Mở đoạn mã, chạy nó và bạn sẽ thấy ba hình ảnh từ xa thuộc các loại khác nhau được hiển thị. Ở đầu bạn có thể chọn tệp hình ảnh hoặc dữ liệu cục bộ và chữ ký tệp và / hoặc loại MIME sẽ được hiển thị.

Lưu ý rằng ngay cả khi một hình ảnh được đổi tên, loại MIME thực sự của nó có thể được xác định. Xem bên dưới.

Ảnh chụp màn hình

Sản lượng dự kiến ​​của bản demo



8
2 bình luận nhỏ. (1) Sẽ không tốt hơn nếu cắt tập tin thành 4 byte đầu tiên trước khi đọc? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Để sao chép / dán chữ ký tập tin, tiêu đề có nên được xây dựng với số 0 for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }không?
Matthew Madson

1
@Deadpool Xem tại đây . Có nhiều định dạng JPEG hơn, ít phổ biến hơn từ các nhà sản xuất khác nhau. Ví dụ: FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. Phần quan trọng của chữ ký JPEG chỉ có 2 byte, nhưng để giảm dương tính giả, tôi đã thêm chữ ký 4 byte phổ biến nhất. Tôi hy vọng điều đó sẽ giúp.
Drakes

23
Chất lượng của câu trả lời này chỉ là tuyệt vời.
Luca

2
Bạn không phải tải blob hoàn chỉnh dưới dạng ArrayBuffer để xác định mimeType. Bạn chỉ có thể cắt và chuyển 4 byte đầu tiên của blob như thế này:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine

2
Điều gì nên được kiểm tra để chỉ cho phép văn bản đơn giản? 4 byte đầu tiên cho tệp văn bản dường như 4 ký tự đầu tiên trong tệp văn bản.
MP Droid

19

Như đã nêu trong các câu trả lời khác, bạn có thể kiểm tra loại mime bằng cách kiểm tra chữ ký của tệp trong các byte đầu tiên của tệp.

Nhưng những gì các câu trả lời khác đang làm là tải toàn bộ tệp vào bộ nhớ để kiểm tra chữ ký, điều này rất lãng phí và có thể dễ dàng đóng băng trình duyệt của bạn nếu bạn chọn một tệp lớn một cách tình cờ.

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


Tôi nghĩ rằng readyStatesẽ luôn FileReader.DONEở trong trình xử lý sự kiện ( thông số W3C ) ngay cả khi có lỗi - không nên kiểm tra nếu (!e.target.error)thay vào đó?
boycy

5

Đối với bất kỳ ai muốn tự mình thực hiện điều này, Sindresorhus đã tạo ra một tiện ích hoạt động trong trình duyệt và có ánh xạ từ đầu đến cuối cho hầu hết các tài liệu bạn muốn.

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

Bạn có thể kết hợp đề xuất của Vitim.us về việc chỉ đọc trong các byte X đầu tiên để tránh tải mọi thứ vào bộ nhớ bằng cách sử dụng tiện ích này (ví dụ trong 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);

Đối với tôi, phiên bản mới nhất của thư viện không hoạt động nhưng "file-type": "12.4.0"đã hoạt động và tôi phải sử dụngimport * as fileType from "file-type";
ssz

4

Nếu bạn chỉ muốn kiểm tra xem tệp đã tải lên có phải là hình ảnh không, bạn có thể thử tải nó vào <img>thẻ để kiểm tra xem có bất kỳ lỗi gọi lại nào không.

Thí dụ:

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
Hoạt động rất tốt, tôi đã thử hack .gif tải lên tệp và nó đã phát sinh lỗi :)
tìm đường

4

Đây là những việc bạn phải làm

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

Nếu bạn muốn kiểm tra các loại tệp hình ảnh thì

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

Hiện không hoạt động cho: Firefox cho Android, Opera cho Android và Safari trên iOS. developer.mozilla.org/en-US/docs/Web/API/File/type
Reid

3

Đây là một triển khai Typecript hỗ trợ webp. Điều này dựa trên câu trả lời JavaScript của 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

Như Drake tuyên bố điều này có thể được thực hiện với FileReader. Tuy nhiên, những gì tôi trình bày ở đây là một phiên bản chức năng. Hãy xem xét rằng vấn đề lớn khi thực hiện điều này với JavaScript là đặt lại tệp đầu vào. Chà, điều này chỉ giới hạn ở JPG (đối với các định dạng khác, bạn sẽ phải thay đổi loại mimesố ma thuật ):

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

Hãy xem xét rằng điều này đã được thử nghiệm trên các phiên bản Firefox và Chrome mới nhất và trên IExplore 10.

Để biết danh sách đầy đủ các loại mime, xem Wikipedia .

Để biết danh sách đầy đủ về số ma thuật, xem Wikipedia .


Các liên kết Wikipedia ở trên không còn hiệu lực.
Bob Quinn

@BobQuinn đã sửa,
tiếng Phạn

0

Đây là phần mở rộng của câu trả lời của Roberto14, thực hiện như sau:

ĐIỀU NÀY S ONLY CHỈ CHO PHÉP HÌNH ẢNH

Kiểm tra nếu FileReader có sẵn và quay lại kiểm tra tiện ích mở rộng nếu nó không khả dụng.

Đưa ra một cảnh báo lỗi nếu không phải là một hình ảnh

Nếu nó là một hình ảnh, nó tải một bản xem trước

** Bạn vẫn nên xác thực phía máy chủ, điều này thuận tiện cho người dùng cuối hơn bất kỳ điều gì khác. Nhưng nó là tiện dụng!

<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

Câu trả lời ngắn gọn là không.

Như bạn lưu ý các trình duyệt xuất phát typetừ phần mở rộng tập tin. Mac xem trước dường như cũng chạy ra phần mở rộng. Tôi giả sử nó bởi vì nó đọc nhanh hơn tên tệp có trong con trỏ, thay vì tìm kiếm và đọc tệp trên đĩa.

Tôi đã tạo một bản sao của một jpg được đổi tên bằng png.

Tôi đã có thể liên tục nhận được những điều sau từ cả hai hình ảnh trong chrome (nên hoạt động trong các trình duyệt hiện đại).

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

Mà bạn có thể hack kiểm tra String.indexOf ('jpeg') cho loại hình ảnh.

Dưới đây là một bí quyết để khám phá http://jsfiddle.net/blyn/jkZ2v/1/

Dòng tuyệt vời tôi quên bình luận trong ví dụ

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

  • Tách dữ liệu img được mã hóa base64, để lại trên hình ảnh
  • Base64 giải mã hình ảnh
  • Chỉ khớp với dòng đầu tiên của dữ liệu hình ảnh

Mã fiddle sử dụng giải mã base64 không hoạt động trong IE9, tôi đã tìm thấy một ví dụ hay sử dụng tập lệnh VB hoạt động trong IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

Mã để tải hình ảnh được lấy từ Joel Vardy, người đang thực hiện một số khung hình ảnh thú vị thay đổi kích thước phía máy khách trước khi tải lên có thể được quan tâm https://joelvardy.com/wr/javascript-image-upload


1
Vui lòng không tìm kiếm JPEG cho chuỗi con "jpeg", đó chỉ là sự trùng hợp ngẫu nhiên mà bạn tìm thấy trong một nhận xét. Các tệp JPEG không phải chứa nó (và nếu bạn đang suy nghĩ về việc tìm kiếm JFIFthay vào đó, thì cũng APP0không phải chứa JFIF trong EXIF-JPEG vì vậy cũng không có).
Kornel

Xem đầu "Câu trả lời ngắn là không".
Lex
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.