Envio de multipart/formdata com jQuery.ajax

I'tenho um problema ao enviar um arquivo para um script PHP do lado do servidor usando jQuery's ajax-function. It's possible to get the File-List with $('#fileinput').attr('files') but how is it possible to send this Data to the server? O array resultante ($_POST) no php-script do lado do servidor é 0 (NULL) quando se utiliza a entrada de arquivo.

Eu sei que é possível (embora eu não'não encontrei nenhuma solução jQuery até agora, apenas código Prototye (http://webreflection.blogspot.com/2009/03/safari-4-multiple-upload-with-progress.html)).

Isso parece ser relativamente novo, então não mencione que o upload de arquivos seria impossível via XHR/Ajax, porque ele's definitivamente funciona.

Eu preciso da funcionalidade no Safari 5, FF e Chrome seria bom, mas não é essencial.

O meu código por agora é:

$.ajax({
    url: 'php/upload.php',
    data: $('#file').attr('files'),
    cache: false,
    contentType: 'multipart/form-data',
    processData: false,
    type: 'POST',
    success: function(data){
        alert(data);
    }
});
Solução

Começando pelo Safari 5/Firefox 4, é mais fácil utilizar a classe FormData:

var data = new FormData();
jQuery.each(jQuery('#file')[0].files, function(i, file) {
    data.append('file-'+i, file);
});

Então agora você tem um objeto FormData, pronto para ser enviado junto com o XMLHttpRequest.

jQuery.ajax({
    url: 'php/upload.php',
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    method: 'POST',
    type: 'POST', // For jQuery < 1.9
    success: function(data){
        alert(data);
    }
});

É imperativo que você defina a opção contentType' parafalse', forçando jQuery a não adicionar um cabeçalho Content-Type' para você, caso contrário, a string de limite estará faltando. Além disso, você deve deixar o flagprocessDatadefinido como falso, caso contrário, jQuery irá tentar converter seuFormData` em uma string, o que falhará.

Agora você pode recuperar o arquivo em PHP usando:

$_FILES['file-0']

(Há apenas um arquivo, file-0, a menos que você tenha especificado o atributo múltiplo na entrada do seu arquivo, neste caso, os números serão incrementados com cada arquivo).

**Usando a emulação FormData para navegadores mais antigos***

var opts = {
    url: 'php/upload.php',
    data: data,
    cache: false,
    contentType: false,
    processData: false,
    method: 'POST',
    type: 'POST', // For jQuery < 1.9
    success: function(data){
        alert(data);
    }
};
if(data.fake) {
    // Make sure no text encoding stuff is done by xhr
    opts.xhr = function() { var xhr = jQuery.ajaxSettings.xhr(); xhr.send = xhr.sendAsBinary; return xhr; }
    opts.contentType = "multipart/form-data; boundary="+data.boundary;
    opts.data = data.toString();
}
jQuery.ajax(opts);

**Criar dados do formulário a partir de um formulário existente***

Em vez de iterar manualmente os arquivos, o objeto FormData também pode ser criado com o conteúdo de um objeto form existente:

var data = new FormData(jQuery('form')[0]);

**Utilize um array nativo de PHP ao invés de um contador***

Basta dar o mesmo nome aos elementos do seu ficheiro e terminar o nome entre parênteses:

jQuery.each(jQuery('#file')[0].files, function(i, file) {
    data.append('file[]', file);
});

$_FILES['file'] será então um array contendo os campos de carregamento de arquivos para cada arquivo carregado. Eu realmente recomendo isso sobre minha solução inicial, pois é mais simples iterar sobre.

Comentários (31)

Só queria acrescentar um pouco a resposta do Raphael'a grande resposta. Aqui's como obter o PHP para produzir os mesmos $_FILES, independentemente de você usar JavaScript para enviar.

Formulário HTML:


   <input name="media[]" type="file" multiple/>
   <input class="button" type="submit" alt="Upload" value="Upload" />

O PHP produz este $_FILES, quando submetido sem JavaScript:

Array
(
    [media] => Array
        (
            [name] => Array
                (
                    [0] => Galata_Tower.jpg
                    [1] => 518f.jpg
                )

            [type] => Array
                (
                    [0] => image/jpeg
                    [1] => image/jpeg
                )

            [tmp_name] => Array
                (
                    [0] => /tmp/phpIQaOYo
                    [1] => /tmp/phpJQaOYo
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 258004
                    [1] => 127884
                )

        )

)

Se você fizer aprimoramento progressivo, usando o Raphael's JS para submeter os arquivos...

var data = new FormData($('input[name^="media"]'));       
jQuery.each($('input[name^="media"]')[0].files, function(i, file) {
    data.append(i, file);
});

$.ajax({
    type: ppiFormMethod,
    data: data,
    url: ppiFormActionURL,
    cache: false,
    contentType: false,
    processData: false,
    success: function(data){
        alert(data);
    }
});

... isto é o que o array PHP's $_FILES parece, depois de utilizar aquele JavaScript para enviar:

Array
(
    [0] => Array
        (
            [name] => Galata_Tower.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpAQaOYo
            [error] => 0
            [size] => 258004
        )

    [1] => Array
        (
            [name] => 518f.jpg
            [type] => image/jpeg
            [tmp_name] => /tmp/phpBQaOYo
            [error] => 0
            [size] => 127884
        )

)

Isso'é um bom array, e na verdade no que algumas pessoas transformam $_FILES, mas eu acho isso'é útil para trabalhar com o mesmo $_FILES, independentemente se o JavaScript foi utilizado para enviar. Então, aqui estão algumas pequenas mudanças no JS:

// match anything not a [ or ]
regexp = /^[^[\]]+/;
var fileInput = $('.putImages input[type="file"]');
var fileInputName = regexp.exec( fileInput.attr('name') );

// make files available
var data = new FormData();
jQuery.each($(fileInput)[0].files, function(i, file) {
    data.append(fileInputName+'['+i+']', file);
});

(14 de Abril 2017 editar: removi o elemento form do construtor do FormData() -- que corrigiu este código no Safari).

Esse código faz duas coisas.

  1. Recupera automaticamente o atributo 'input' do nome, tornando o HTML mais manejável. Agora, desde que o form tenha a classe putImages, tudo o resto é tratado automaticamente. Ou seja, o input não precisa ter nenhum nome especial.
  2. O formato de array que o HTML normal submete é recriado pelo JavaScript na linha data.append. Note os parênteses.

Com estas alterações, submeter com JavaScript agora produz precisamente a mesma matriz $_FILES que submeter com HTML simples.

Comentários (1)

Acabei de construir esta função com base em algumas informações que li.

Utilize-o como se utilizasse .serialize(), em vez disso basta colocar .serializefiles();.
Trabalhar aqui nos meus testes.

//USAGE: $("#form").serializefiles();
(function($) {
$.fn.serializefiles = function() {
    var obj = $(this);
    /* ADD FILE TO PARAM AJAX */
    var formData = new FormData();
    $.each($(obj).find("input[type='file']"), function(i, tag) {
        $.each($(tag)[0].files, function(i, file) {
            formData.append(tag.name, file);
        });
    });
    var params = $(obj).serializeArray();
    $.each(params, function (i, val) {
        formData.append(val.name, val.value);
    });
    return formData;
};
})(jQuery);
Comentários (2)