Upload assíncrono de arquivos em CakePHP
Autor: Adriano "duke" Bacha | Publicado: 2010-03-29T14:09:00-04:00

Após várias horas tentando implementar um upload de múltiplos arquivos de forma assíncrona no CakePHP, finalmente consegui desenvolver algo exatamente como eu queria, vou explicar a seguir como fiz:
Primeiramente baixei o plugin Uploadify do jQuery e distribui os arquivos da seguinte forma:
- /app/webroot/js/: jquery-1.3.2.min, jquery.uploadify.v2.1.0.min e swfobject
- /app/webroot/css/: uploadify.css
- app/webroot/uploadify/: uploadify.php, uploadify.swf e cancel.png
* criar uma sub-pasta chamada files(app/webroot/uploadify/files) que será usada para guardar temporariamente os arquivos de upload.

Fiz uma alteração no arquivo uploadify.php e descomentei tudo que estava comentado para verificar o tipo do arquivo e criar o diretório se não existir.
O arquivo ficará assim:
if (!empty($_FILES)) {
 $tempFile = $_FILES['Filedata']['tmp_name'];
 $targetPath = $_SERVER['DOCUMENT_ROOT'] . $_REQUEST['folder'] . '/';
 $targetFile =  str_replace('//','/',$targetPath) . $_FILES['Filedata']['name'];
 
 $fileTypes  = str_replace('*.','',$_REQUEST['fileext']);
 $fileTypes  = str_replace(';','|',$fileTypes);
 $typesArray = split('\|',$fileTypes);
 $fileParts  = pathinfo($_FILES['Filedata']['name']);
 
 if (in_array($fileParts['extension'],$typesArray)) {
  // Uncomment the following line if you want to make the directory if it doesn't exist
  mkdir(str_replace('//','/',$targetPath), 0755, true);
  
  move_uploaded_file($tempFile,$targetFile);
  echo "1";
 } else {
 echo 'Invalid file type.';
 }
}

Agora na view, importarei os javascripts e css's:
&lt;?=$javascript->link(array("jquery-1.3.2.min", "jquery.uploadify.v2.1.0.min", "swfobject"))?&gt;
&lt;?=$html->css(array("uploadify"));?&gt;

além de adicionar o código parar criar o botão de upload e o botão para limpar a fila:
&lt;?= $form->file("upload") ?&gt;
&lt;a href="javascript:jQuery('#BookUpload').uploadifyUpload();">Upload&lt;/a&gt;
&lt;a href="javascript:jQuery('#BookUpload').uploadifyClearQueue();"&gt;Clear&lt;/a&gt;
no caso, o id do meu input é "Upload" e como estou no form "Book" devo referenciá-lo como "#BookUpload"

E criarei o seguinte bloco javascript:
&lt;script type="text/javascript"&gt;
    jQuery.noConflict();
    jQuery(document).ready(function() {
        jQuery('#BookUpload').uploadify({
            'uploader':'/uploadify/uploadify.swf',
            'script':'/uploadify/uploadify.php',
            'cancelImg':'/uploadify/cancel.png',
            'folder':'/uploadify/files/&lt;?=$_SESSION["Config"]["userAgent"]?&gt;',
            'fileExt':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2',
            'fileDesc':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2',
            'sizeLimit':104857600,
            'multi':true
        });
    });
&lt;/script&gt;

Irei explicar algumas partes relevantes do código acima:

jQuery.noConflict();: como utilizo nessa mesma view a prototype, não posso utilizar o "$" para chamadas jquery além de ter que utilizar essa função para não gerar conflitos.

jQuery('#BookUpload').uploadify({: é necessário informar o id de um input tipo "file"

'folder':'/uploadify/files/&lt;?=$_SESSION["Config"]["userAgent"]?&gt;': Uma grande dúvida que eu tive era como eu iria relacionar um livro que ainda não foi cadastro com os arquivos subidos.
A solução que eu encontrei foi de criar uma pasta temporária com o nome de uma variável de sessão desse usuário, ou seja, enquanto o usuário estiver "em tempo de criação" de um livro, os arquivos que já foram subidos irão ficar nessa pasta temporária. Como apontado pelo Arlindo, no caso de estar rodando em uma máquina windows, deve-se utilizar o endereço completo da pasta. ('folder':'/app/webroot/uploadify/files/...)

'fileExt':'*.zip;*.pdf;*.rar;*.txt;*.doc;*.xls;*.docx;*.xlsx;*.chm;*.tar;*.tar.gz;*.tgz;*.tar.bz;*.tar.bz2': Extensões aceitas pelo upload


'sizeLimit':104857600: Tamanho máximo dos arquivos em bytes (104857600b = 100mb)

'multi':true: Possibilidade de enviar mais de um arquivo


Para finalizar, criei uma função que eu chamo no beforeSave:
function beforeSave() {
        if (isset($_SESSION["Config"]["userAgent"]))
            $this->data["BookFiles"] = $this->uploadFiles();
        return true;
    }
function uploadFiles() {
        $dir = "uploadify/files/".$_SESSION["Config"]["userAgent"]."/";
        if (is_dir($dir)) {
            $handle = opendir($dir);

            while (false !== ($file = readdir($handle)))
                if ($file != "." && $file != "..") {
                    $old_name = $file;
                    $cont = 2;
                    while (file_exists("files/".$file)) {
                        $file = pathinfo($file);
                        $file = $file["filename"]."-".$cont.".".$file["extension"];
                        $cont++;
                    }
                    rename($dir.$old_name, "files/".$file);
                    $files[] = array("name" => $file);
                    chmod("files/".$file, 0755);
                }
            closedir($handle);
            rmdir($dir);
            return $files;
        }
        else return false;
    }

Essa função só será utilizada se o diretório identificado pela variável de sessão existir ("uploadify/files/".$_SESSION["Config"]["userAgent"]."/"), se existir, movo os arquivos para a pasta /app/webroot/files/ (nisso eu verifico se já existe algum arquivo com o mesmo nome, caso existir, renomeio colocando um numeral no fim).
A seguir dou permissão no arquivo (chmod("files/".$file, 0755)) e retorno um array com o nome de todos os arquivos.




Em um próximo momento, irei procurar uma solução para deletar os arquivos que ficarem no servidor quando a sessão for destruída, no caso de uma pessoa subir arquivos mas não submeter o formulário