Achei que escreveria uma "resposta" curta (para mim, isso é curto) apenas para poder resumir meus pontos.
Algumas "Melhores Práticas" ao criar um sistema de armazenamento de arquivos. O armazenamento de arquivos é uma categoria ampla, portanto, sua milhagem pode variar para alguns deles. Tome-os apenas como sugestão do que eu encontrei funciona bem.
Nomes de arquivo Não armazene o arquivo com o nome dado por um usuário final. Eles podem e usarão todos os tipos de personagens ruins que tornarão sua vida miserável. Alguns podem ser tão ruins quanto
'
aspas simples, o que no linux basicamente torna impossível ler ou até mesmo excluir o arquivo (diretamente). Algumas coisas podem parecer simples como
um espaço, mas dependendo de onde você o usa e do sistema operacional em seu servidor, você pode acabar com one%20two.txt
ou one+two.txt
ou one two.txt
que podem ou não criar todos os tipos de problemas em seus links. A melhor coisa a fazer é criar um hash, algo como
sha1
isso pode ser tão simples quanto {user_id}{orgianl_name}
O nome de usuário diminui a probabilidade de colisões com nomes de arquivos de outros usuários. Eu prefiro fazer
file_hash('sha1', $contents)
dessa forma, se alguém enviar o mesmo arquivo mais de uma vez, você poderá pegá-lo (o conteúdo é o mesmo, o hash é o mesmo). Mas se você espera ter arquivos grandes, você pode querer fazer alguma marcação nele para ver que tipo de desempenho ele tem. Eu lido principalmente com arquivos pequenos, então funciona bem para isso.-note que com o carimbo de data/hora o arquivo ainda pode ser salvo porque o nome completo é diferente, mas torna bastante fácil de ver e pode ser verificado no banco de dados. Independentemente do que você fizer, eu prefixaria com um carimbo de data/hora
time().'-'.$filename
. Esta é uma informação útil, porque é a hora absoluta em que o arquivo foi criado. Quanto ao nome que um usuário dá ao arquivo. Apenas armazene isso no registro do banco de dados. Dessa forma, você pode mostrar a eles o nome que eles esperam, mas use um nome que você sabe que é sempre seguro para links.
$filename ='alguma porcaria^ fileane.jpg';
$ext = strrchr($filename, '.');
echo "\nExt: {$ext}\n";
$hash = sha1('some crapy^ fileane.jpg');
echo "Hash: {$hash}\n";
$time = time();
echo "Timestamp: {$time}\n";
$hashname = $time.'-'.$hash.$ext;
echo "Hashname: $hashname\n";
Saídas
Ext: .jpg
Hash: bb9d2c2c7c73bb8248537a701870e35742b41c02
Timestamp: 1511853063
Hashname: 1511853063-bb9d2c2c7c73bb8248537a701870e35742b41c02.jpg
Você pode experimentar aqui
Caminhos nunca armazene o caminho completo para o arquivo. Tudo o que você precisa no banco de dados é o hash da criação do nome do hash. O caminho "raiz" para a pasta em que o arquivo está armazenado deve ser feito em PHP. Isso tem vários benefícios.
- impede a transferência do diretório. Como você não passa por nenhuma parte do caminho ao seu redor, não precisa se preocupar tanto com alguém escorregando um
\..\..
lá e indo a lugares que não deveriam. Um mau exemplo disso seria alguém sobrescrevendo um.htpassword
ao fazer upload de um arquivo chamado com o diretório transversal nele. - Tem links de aparência mais uniforme, tamanho uniforme, conjunto uniforme de caracteres.
https://en.wikipedia.org/wiki/Directory_traversal_attack
- Manutenção. Os caminhos mudam, os servidores mudam. Demandas em seu sistema mudam. Se você precisar realocar esses arquivos, mas armazenou o caminho completo absoluto para eles no banco de dados, está colando tudo junto com
symlinks
ou atualizando todos os seus registros.
Existem algumas exceções a isso. Se você quiser armazená-los em uma pasta mensal ou por nome de usuário. Você pode salvar essa parte do caminho, em um campo separado. Mas mesmo nesse caso, você pode construí-lo dinamicamente com base nos dados salvos no registro. Descobri que é melhor salvar o mínimo possível de informações de caminho. E eles fazem uma configuração ou uma constante que você pode usar em todos os lugares que precisar para colocar o caminho do arquivo.
Também o
path
e o link
são muito diferentes, portanto, salvando apenas o nome, você pode vinculá-lo de qualquer página PHP desejada sem precisar subtrair dados do caminho. Sempre achei mais fácil adicionar ao nome do arquivo do que subtrair de um caminho. Banco de dados (apenas algumas sugestões, o uso pode variar) Como sempre com dados pergunte a si mesmo, quem, o quê, onde, quando
- código -
int
incremento automático de chave primária - user_id -
int
chave estrangeira, quem carregou - hash -
char[40] *sha1*, unique
o que o hash - hashname -
varchar
{timestapl}-{hash}.{ext} onde o nome dos arquivos no disco rígido - nome do arquivo -
varchar
o nome original dado pelo usuário, assim podemos mostrar o nome que eles esperam (se isso for importante) - estado -
enum[public,private,deleted,pending.. etc]
status do arquivo, dependendo do seu caso de uso, você pode ter que revisar os arquivos, ou talvez alguns sejam privados apenas o usuário pode vê-los, talvez alguns sejam públicos etc. - status_date -
timestamp|datetime
momento em que o status foi alterado. - create_date -
timestamp|datetime
quando hora em que o arquivo foi criado, um timestamp é preferível, pois facilita algumas coisas, mas deve ser o mesmo timestamp usado no hashname, nesse caso. - tipo -
varchar
- tipo mime, pode ser útil para definir o tipo mime ao baixar etc.
Se você espera que usuários diferentes carreguem o mesmo arquivo e você usa o
file_hash
você pode fazer o hash
campo um índice exclusivo combinado do user_id
e o hash
dessa forma, só entraria em conflito se o mesmo usuário carregasse o mesmo arquivo. Você também pode fazer isso com base no carimbo de data e hora e no hash, dependendo de suas necessidades. Essa é a coisa básica que eu poderia pensar, isso não é absoluto, apenas alguns campos que eu pensei que seriam úteis.
É útil ter o hash sozinho, se você armazená-lo sozinho, poderá armazená-lo em um
CHAR(40)
para sha1 (ocupa menos espaço no banco de dados que VARCHAR
) e defina o agrupamento para UTF8_bin
que é binário. Isso torna as pesquisas nele sensíveis a maiúsculas e minúsculas. Embora haja pouca possibilidade de uma colisão de hash, isso adiciona um pouco mais de proteção porque os hashes são letras maiúsculas e minúsculas. Você sempre pode construir o
hashname
em tempo real se você armazenar a extensão e o carimbo de data/hora separados. Se você estiver criando coisas várias vezes, você pode apenas armazená-las no banco de dados para simplificar o trabalho em PHP. Eu gosto apenas de colocar o hash no link, sem extensão, sem nada, então meus links ficam assim.
http://www.example.com/download/ad87109bfff0765f4dd8cf4943b04d16a4070fea
Realmente simples, genérico de verdade, seguro em urls sempre do mesmo tamanho etc.
O
hashname
para este "arquivo" seria assim 1511848005-ad87109bfff0765f4dd8cf4943b04d16a4070fea.jpg
Se você tiver conflitos com o mesmo arquivo e usuário diferente (que mencionei acima). Você sempre pode adicionar a parte do timestamp no link, o user_id ou ambos. Se você usar o user_id, pode ser útil preenchê-lo com zeros à esquerda. Por exemplo, alguns usuários podem ter
ID:1
e alguns podem ser ID:234
então você pode deixar 4 lugares e torná-los 0001
e 0234
. Em seguida, adicione isso ao hash, que é quase imperceptível:1511848005-ad87109bfff0765f4dd8cf4943b04d16a4070fea0234.jpg
O importante aqui é que, porque
sha1
é sempre 40
e o id é sempre 4
podemos separar os dois com precisão e facilidade. E desta forma, você ainda pode procurá-lo de forma exclusiva. Existem muitas opções diferentes, mas muito depende de suas necessidades. Acesso Como baixar. Você deve sempre produzir o arquivo com PHP, não dê acesso direto ao arquivo. A melhor maneira é armazenar os arquivos fora do webroot ( acima do
public_html
, ou www
pasta ). Então, no PHP, você pode definir os cabeçalhos para o tipo correto e basicamente ler o arquivo. Isso funciona para praticamente tudo, exceto vídeo. Eu não lido com vídeos, então esse é um tópico fora da minha experiência. Mas acho melhor pensar nisso como todos os dados do arquivo são texto, são os cabeçalhos que transformam esse texto em uma imagem ou um arquivo do Excel ou um pdf. A grande vantagem de não dar acesso direto ao arquivo é que se você tem um site de membros, ou não quer que seu conteúdo seja acessível sem um login, você pode verificar facilmente no PHP se eles estão logados antes de fornecer o conteúdo. E, como o arquivo está fora do webroot, eles não podem acessá-lo de outra maneira.
O mais importante é escolher algo consistente, que ainda seja flexível o suficiente para lidar com todas as suas necessidades.
Tenho certeza de que posso chegar a mais, mas se você tiver alguma sugestão, sinta-se à vontade para comentar.
FLUXO DE PROCESSO BÁSICO
- O usuário envia o formulário (
enctype="multipart/form-data"
)
https://www.w3schools.com/tags/att_form_enctype.asp
- O servidor recebe a postagem do formulário, Super Globals
$_POST
e os$_FILES
http://php.net/manual/en/reserved.variables.files .php
$_FILES = [
'fieldname' => [
'name' => "MyFile.txt" // (comes from the browser, so treat as tainted)
'type' => "text/plain" // (not sure where it gets this from - assume the browser, so treat as tainted)
'tmp_name' => "/tmp/php/php1h4j1o" // (could be anywhere on your system, depending on your config settings, but the user has no control, so this isn't tainted)
'error' => "0" //UPLOAD_ERR_OK (= 0)
'size' => "123" // (the size in bytes)
]
];
-
Verifique se há errosif(!$_FILES['fielname']['error'])
-
Limpar nome de exibição$filename = htmlentities($str, ENT_NOQUOTES, "UTF-8");
-
Salvar arquivo, criar registro de banco de dados ( PSUDO-CODE )
Assim:
$path = __DIR__.'/uploads/'; //for exmaple
$time = time();
$hash = hash_file('sha1',$_FILES['fielname']['tmp_name']);
$type = $_FILES['fielname']['type'];
$hashname = $time.'-'.$hash.strrchr($_FILES['fielname']['name'], '.');
$status = 'pending';
if(!move_uploaded_file ($_FILES['fielname']['tmp_name'], $path.$hashname )){
//failed
//do somehing for errors.
die();
}
//store record in db
http://php.net/manual/en/function.move -uploaded-file.php
-
Criar link ( varia de acordo com o roteamento ), a maneira mais simples é fazer seu link assimhttp://www.example.com/download?file={$hash}
mas é mais feio quehttp://www.example.com/download/{$hash}
-
o usuário clica no link vai para a página de download.
obter INPUT e procurar registro
$hash = $_GET['file'];
$stmt = $PDO->prepare("SELECT * FROM attachments WHERE hash = :hash LIMIT 1");
$stmt->execute([":hash" => $hash]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($row);
http://php.net/manual/en/intro.pdo.php
etc...
Felicidades!