Como verificar o tipo de arquivo MIME com javascript antes do upload?

177

Eu li isso e estas perguntas que parecem sugerir que o tipo MIME do arquivo possa ser verificado usando javascript no lado do cliente. Agora, entendo que a validação real ainda precisa ser feita no servidor. Desejo executar uma verificação do lado do cliente para evitar desperdício desnecessário de recursos do servidor.

Para testar se isso pode ser feito no lado do cliente, alterei a extensão de um JPEGarquivo de teste para .pnge escolhi o arquivo para upload. Antes de enviar o arquivo, consulte o objeto de arquivo usando um console javascript:

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

É isso que recebo no Chrome 28.0:

Arquivo {webkitRelativePath: "", lastModifiedDate: terça-feira, 16 de outubro de 2012, 10:00:00 GMT + 0000 (UTC), nome: "test.png", tipo: "image / png", tamanho: 500055…}

Ele mostra o tipo image/pngque parece indicar que a verificação é feita com base na extensão do arquivo em vez do tipo MIME. Eu tentei o Firefox 22.0 e me dá o mesmo resultado. Mas, de acordo com as especificações do W3C , o MIME Sniffing deve ser implementado.

Estou certo em dizer que não há como verificar o tipo MIME com javascript no momento? Ou eu estou esquecendo de alguma coisa?

Pergunta Overflow
fonte
5
I want to perform a client side checking to avoid unnecessary wastage of server resource.Não entendo por que você diz que a validação deve ser feita no servidor, mas diz que deseja reduzir os recursos do servidor. Regra de ouro: nunca confie na entrada do usuário . Qual é o objetivo de verificar o tipo MIME no lado do cliente, se você está apenas fazendo no lado do servidor. Certamente isso é um "desperdício desnecessário de recursos do cliente "?
22813 Ian Ian
7
Fornecer uma melhor verificação / feedback do tipo de arquivo aos usuários do lado do cliente é uma boa ideia. No entanto, como você afirmou, os navegadores simplesmente confiam nas extensões de arquivo ao determinar o valor da typepropriedade para Fileobjetos. O código fonte do webkit, por exemplo, revela essa verdade. É possível identificar com precisão os arquivos do lado do cliente, procurando "bytes mágicos" nos arquivos, entre outras coisas. Atualmente, estou trabalhando em uma biblioteca do MIT (no pouco tempo livre que tenho) que fará exatamente isso. Se você estiver interessado em meu progresso, dê uma olhada em github.com/rnicholus/determinater .
precisa saber é o seguinte
32
@IanClark, o ponto é que, se o arquivo é de um tipo inválido, posso rejeitá-lo no lado do cliente, em vez de desperdiçar a largura de banda de upload apenas para rejeitá-lo no lado do servidor.
Pergunta Overflow
@RayNicholus, cara legal! Vou olhar através dele quando eu tiver tempo. Obrigado :)
Pergunta Overflow
Você tem certeza de que seu arquivo de teste ainda possui o tipo mimet image/jpege não o modificou alterando a extensão?
Bergi

Respostas:

342

Você pode determinar facilmente o tipo MIME do arquivo com JavaScript FileReaderantes de enviá-lo para um servidor. Concordo que devemos preferir a verificação do lado do servidor em vez do lado do cliente, mas a verificação do lado do cliente ainda é possível. Vou mostrar como e fornecer uma demonstração de trabalho na parte inferior.


Verifique se o seu navegador suporta ambos Filee Blob. Todos os principais deveriam.

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

Passo 1:

Você pode recuperar as Fileinformações de um <input>elemento como este ( 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>

Aqui está uma versão do tipo arrastar e soltar acima ( 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>

Passo 2:

Agora podemos inspecionar os arquivos e exibir cabeçalhos e tipos MIME.

✘ método rápido

Você pode, ingenuamente, solicitar ao Blob o tipo MIME de qualquer arquivo que ele represente usando este padrão:

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

Para imagens, os tipos MIME retornam da seguinte forma:

image / jpeg
imagem / png
...

Advertência: O tipo MIME é detectado na extensão do arquivo e pode ser enganado ou falsificado. Pode-se renomear a .jpgpara a .pnge o tipo MIME será relatado como image/png.


✓ Método adequado de inspeção de cabeçalho

Para obter o tipo MIME genuíno de um arquivo do lado do cliente, podemos ir um pouco além e inspecionar os primeiros bytes do arquivo fornecido para comparar com os chamados números mágicos . Esteja avisado de que não é totalmente simples, porque, por exemplo, o JPEG possui alguns "números mágicos". Isso ocorre porque o formato evoluiu desde 1991. Você pode verificar apenas os dois primeiros bytes, mas prefiro verificar pelo menos 4 bytes para reduzir os falsos positivos.

Exemplo de assinaturas de arquivo JPEG (primeiros 4 bytes):

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

Aqui está o código essencial para recuperar o cabeçalho do arquivo:

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

Você pode determinar o tipo MIME real da seguinte maneira (mais assinaturas de arquivo aqui e aqui ):

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

Aceite ou rejeite os uploads de arquivos como desejar, com base nos tipos MIME esperados.


Demo

Aqui está uma demonstração de trabalho para arquivos locais e arquivos remotos (eu tive que ignorar o CORS apenas para esta demonstração). Abra o trecho, execute-o e você verá três imagens remotas de diferentes tipos exibidas. Na parte superior, você pode selecionar uma imagem ou arquivo de dados local e a assinatura do arquivo e / ou o tipo MIME serão exibidos.

Observe que, mesmo que uma imagem seja renomeada, seu verdadeiro tipo MIME pode ser determinado. Ver abaixo.

Captura de tela

Saída esperada da demonstração


Drakes
fonte
8
2 pequenos comentários. (1) Não seria melhor dividir o arquivo nos primeiros 4 bytes antes da leitura? fileReader.readAsArrayBuffer(blob.slice(0,4))? (2) Para copiar / colar assinaturas de arquivo, o cabeçalho não deve ser construído com zeros à esquerda for(var i = 0; i < bytes.length; i++) { var byte = bytes[i]; fileSignature += (byte < 10 ? "0" : "") + byte.toString(16); }?
Matthew Madson
1
@Deadpool Veja aqui . Existem formatos JPEG mais, menos comuns, de diferentes fabricantes. Por exemplo, FF D8 FF E2= CANNON EOS JPEG FILE, FF D8 FF E3= SAMSUNG D500 JPEG FILE. A parte principal da assinatura JPEG é de apenas 2 bytes, mas para reduzir os falsos positivos, adicionei as assinaturas mais comuns de 4 bytes. Espero que ajude.
Drakes
23
A qualidade desta resposta é simplesmente incrível.
Luca
2
Você não precisa carregar o blob completo como ArrayBuffer para determinar o mimeType. Você pode simplesmente cortar e passar pela primeira 4 bytes do blob assim:fileReader.readAsArrayBuffer(blob.slice(0, 4))
codeVerine
2
Qual deve ser a verificação para permitir apenas texto sem formatação? Os primeiros 4 bytes para arquivos de texto parecem os 4 primeiros caracteres no arquivo de texto.
MP Droid
19

Conforme indicado em outras respostas, você pode verificar o tipo de mímica, verificando a assinatura do arquivo nos primeiros bytes do arquivo.

Mas o que outras respostas estão fazendo é carregar o arquivo inteiro na memória para verificar a assinatura, o que é muito inútil e pode congelar facilmente o navegador se você selecionar um arquivo grande por acidente ou não.

/**
 * 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
fonte
Eu acho readyStateque sempre estará FileReader.DONEno manipulador de eventos ( especificação W3C ), mesmo que tenha havido um erro - a verificação não deveria acontecer (!e.target.error)?
boycy
5

Para quem quer não implementar isso, o Sindresorhus criou um utilitário que funciona no navegador e possui os mapeamentos de cabeçalho para mimo para a maioria dos documentos que você deseja.

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

Você pode combinar a sugestão do Vitim.us de apenas ler os primeiros X bytes para evitar carregar tudo na memória com o uso deste utilitário (exemplo em 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
fonte
Para mim, a versão mais recente da biblioteca não funcionou, mas "file-type": "12.4.0"funcionou e eu tive que usarimport * as fileType from "file-type";
ssz
4

Se você quiser apenas verificar se o arquivo enviado é uma imagem, tente carregá-lo na <img>tag e verifique se há retorno de chamada de erro.

Exemplo:

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
fonte
1
Funciona muito bem, eu tentei um arquivo .gif uploader de corte e jogou um erro :)
pathfinder
4

Isto é o que você tem que fazer

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

Se você deseja verificar os tipos de arquivo de imagem,

if(fileVariable.type.match('image.*'))
{
 alert('its an image');
}
Kailas
fonte
Atualmente não está funcionando para: Firefox para Android, Opera para Android e Safari no iOS. developer.mozilla.org/pt-BR/docs/Web/API/File/type
Reid
3

Aqui está uma implementação do TypeScript que suporta webp. Isso é baseado na resposta JavaScript do 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
fonte
1

Como Drake afirma, isso pode ser feito com o FileReader. No entanto, o que apresento aqui é uma versão funcional. Leve em consideração que o grande problema de fazer isso com JavaScript é redefinir o arquivo de entrada. Bem, isso se restringe apenas ao JPG (para outros formatos, você precisará alterar o tipo de mímica e o número mágico ):

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

Leve em consideração que isso foi testado nas versões mais recentes do Firefox e Chrome e no IExplore 10.

Para uma lista completa dos tipos de mímica, consulte Wikipedia .

Para uma lista completa dos números mágicos, consulte Wikipedia .

lmiguelmh
fonte
Os links da Wikipedia acima não são mais válidos.
Bob Quinn
@BobQuinn fixou, thansk
lmiguelmh
0

Aqui está uma extensão da resposta de Roberto14 que faz o seguinte:

Isso só permitirá imagens

Verifica se o FileReader está disponível e volta para a extensão, verificando se não está disponível.

Dá um alerta de erro, se não uma imagem

Se for uma imagem, ele carrega uma visualização

** Você ainda deve fazer a validação no lado do servidor; isso é mais uma conveniência para o usuário final do que qualquer outra coisa. Mas é útil!

<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>
desbravador
fonte
-1

Resposta curta é não.

Como você observa, os navegadores derivam typeda extensão do arquivo. A pré-visualização do Mac também parece ficar sem a extensão. Estou assumindo que é porque é mais rápido ler o nome do arquivo contido no ponteiro, em vez de procurar e ler o arquivo no disco.

Fiz uma cópia de um jpg renomeado com png.

Consegui obter consistentemente o seguinte de ambas as imagens no chrome (deve funcionar em navegadores modernos).

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

Que você pode hackear uma verificação String.indexOf ('jpeg') para o tipo de imagem.

Aqui está um violino para explorar http://jsfiddle.net/bamboo/jkZ2v/1/

A linha ambígua que eu esqueci de comentar no exemplo

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

  • Divide os dados img codificados em base64, deixando na imagem
  • Base64 decodifica a imagem
  • Corresponde apenas à primeira linha dos dados da imagem

O código do violino usa a decodificação base64, que não funcionará no IE9, encontrei um bom exemplo usando o script VB que funciona no IE http://blog.nihilogic.dk/2008/08/imageinfo-reading-image-metadata-with.html

O código para carregar a imagem foi tirado de Joel Vardy, que está redimensionando a tela da imagem antes de fazer o upload, o que pode ser interessante https://joelvardy.com/writing/javascript-image-upload

Lex
fonte
1
Por favor, não procure em JPEGs a substring "jpeg", é apenas uma coincidência que você a encontrou em um comentário. Os arquivos JPEG não precisam contê-lo (e, se você estiver pensando em procurar JFIF, o poço APP0não precisa conter JFIF em EXIF-JPEG, para que também não ocorra).
Kornel
Consulte o topo "A resposta curta é não".
Lex