Armazenamento de um grande número de arquivos

imagem

Boa saúde, vendedores ambulantes! No processo de trabalhar em um projeto de site de namoro, tornou-se necessário organizar o armazenamento de fotos do usuário. De acordo com os termos de referência, o número de fotos por usuário é limitado a 10 arquivos. Mas pode haver dezenas de milhares de usuários. Especialmente considerando que o projeto em sua forma atual já existe desde o início do "zero". Ou seja, já existem milhares de usuários no banco de dados. Quase todo sistema de arquivos, até onde eu sei, reage muito negativamente a um grande número de nós filhos em uma pasta. Por experiência, posso dizer que os problemas começam após 1000-1500 arquivos / pastas na pasta pai.

Isenção de responsabilidade. Pesquisei no Google antes de escrever o artigo e encontrei várias soluções para o problema em discussão (por exemplo, aqui ou aqui ). Mas não encontrei uma solução única que corresponda exatamente à minha. Além disso, neste artigo, apenas compartilho minha própria experiência na solução do problema.

Teoria


Além da tarefa de armazenamento, como tal, havia também uma condição na declaração de trabalho, segundo a qual era necessário deixar legendas e legendas para fotos. Claro, você não pode ficar sem um banco de dados. Ou seja, a primeira coisa que fazemos é criar uma tabela na qual escrevemos o mapeamento de metadados (assinaturas, títulos, etc.) com os arquivos no disco. Cada arquivo corresponde a uma linha no banco de dados. Assim, cada arquivo tem um identificador.

Uma pequena digressão. Vamos falar sobre o incremento automático. Um site de namoro pode ter uma dúzia ou dois mil usuários. A questão é quantos usuários geralmente passam pelo projeto durante todo o tempo de sua existência. Por exemplo, o público ativo do dating-ru é de várias centenas de milhares. No entanto, imagine quantos usuários restam durante a vida útil deste projeto; quantos usuários não estão ativados até o momento. E agora adicione nossa legislação, que nos obriga a armazenar informações sobre usuários por pelo menos seis meses ... Mais cedo ou mais tarde, 4 com um centavo de UNSIGNED INT terminará. Portanto, é melhor usar BIGINT para a chave primária.

Agora vamos tentar imaginar um número do tipo BIGINT . Isso é 8 bytes. Cada byte é de 0 a 255. 255 nós filhos são bastante normais para qualquer sistema de arquivos. Ou seja, pegamos o identificador de arquivo na representação hexadecimal, dividimos em pedaços de dois caracteres. Usamos esses pedaços como nomes de pastas, os últimos como o nome do arquivo físico. LUCRO!

0f/65/84/10/67/68/19/ff.file

Elegante e simples. A extensão do arquivo não é importante aqui. De qualquer forma, o arquivo será fornecido por um script que fornecerá ao navegador um tipo MIME específico, que também armazenaremos no banco de dados. Além disso, o armazenamento de informações sobre o arquivo no banco de dados permite redefinir o caminho do navegador para ele. Digamos que o arquivo que temos esteja localizado em relação ao diretório do projeto no caminho /content/files/0f/65/84/10/67/68/19/ff.file . E no banco de dados, você pode escrever uma URL para ele, por exemplo, /content/users/678/files/somefile . SEOs provavelmente são muito sorridentes agora. Tudo isso nos permite não nos preocupar mais sobre onde colocar o arquivo fisicamente.

Tabela no banco de dados


Além do identificador, tipo MIME, URL e localização física, armazenaremos os arquivos md5 e sha1 na tabela para filtrar os mesmos arquivos, se necessário. Obviamente, também precisamos armazenar relacionamentos de entidades nesta tabela. Suponha o ID do usuário ao qual os arquivos pertencem. E se o projeto não for muito grande, no mesmo sistema podemos armazenar, digamos, fotografias de mercadorias. Portanto, também armazenaremos o nome da classe de entidade à qual o registro pertence.

Falando em pássaros. Se você fechar a pasta usando .htaccess para acesso externo, o arquivo poderá ser obtido apenas através do script. E no script será possível determinar o acesso ao arquivo. Olhando um pouco mais adiante, direi que no meu CMS (que o projeto mencionado está sendo visto agora) o acesso é determinado por grupos de usuários básicos, dos quais eu tenho 8 - convidados, usuários, gerentes, administradores, inativos, bloqueados, excluídos e superadministradores. O superadministrador pode fazer absolutamente tudo, por isso não participa da determinação do acesso. Se o usuário tiver um sinalizador de superadministrador, ele será um superadministrador. Tudo é simples. Ou seja, determinaremos o acesso aos sete grupos restantes. O acesso é simples - dê o arquivo ou não. Total, você pode pegar um campo do tipo TINYINT .

E mais uma coisa. De acordo com nossa lei, teremos que armazenar fisicamente imagens personalizadas. Ou seja, precisamos marcar as imagens como excluídas, em vez de excluir fisicamente. É mais conveniente usar um campo de bits para esses fins. Nesses casos, costumo usar um campo do tipo INT . Para reservar, por assim dizer. Além disso, eu já tenho uma tradição estabelecida de colocar a bandeira DELETED no quinto bit a partir do final. Mas isso não importa novamente.

O que temos como resultado:

 create table `files` ( `id` bigint not null auto_increment, --   `entity_type` char(32) not null default '', --   `entity` bigint null, -- ID  `mime` char(32) not null default '', -- MIME- `md5` char(32) not null default '', -- MD5 `sha1` char(40) not null default '', -- SHA1 `file` char(64) not null default '', --   `url` varchar(250) not null default '', -- URL `meta` text null, -- -   JSON    `size` bigint not null default '0', --  `created` datetime not null, --   `updated` datetime null, --   `access` tinyint not null default '0', --   `flags` int not null default '0', --  primary key (`id`), index (`entity_type`), index (`entity`), index (`mime`), index (`md5`), index (`sha1`), index (`url`) ) engine = InnoDB; 

Classe de expedidor


Agora precisamos criar uma classe com a qual faremos o upload de arquivos. A classe deve fornecer a capacidade de criar arquivos, substituir / modificar arquivos, excluir arquivos. Além disso, vale considerar dois pontos. Primeiramente, o projeto pode ser transferido de servidor para servidor. Portanto, na classe, você precisa definir uma propriedade que contenha o diretório raiz dos arquivos. Em segundo lugar, será muito desagradável se alguém bater em uma tabela no banco de dados. Então, você precisa prever a possibilidade de recuperação de dados. Com o primeiro, tudo fica geralmente claro. Quanto ao backup de dados, reservamos apenas o que não pode ser restaurado.

ID - restaurado a partir do local físico do arquivo
entity_type - não restaurado
entidade - não restaurada
mime - restaurado usando a extensão finfo
md5 - é restaurado a partir do próprio arquivo
sha1 - restaurado a partir do próprio arquivo
file - restaurado a partir do local físico do arquivo
URL - não restaurado
meta - não restaurado
tamanho - restaurado a partir do próprio arquivo
criado - você pode obter informações de um arquivo
atualizado - você pode obter informações de um arquivo
acesso - não restaurado
sinalizadores - não restaurados

Você pode descartar imediatamente as informações meta. Não é crítico para o funcionamento do sistema. E para uma recuperação mais rápida, você ainda precisa salvar o tipo MIME. Total: tipo de entidade, ID da entidade, MIME, URL, acesso e sinalizadores. Para aumentar a confiabilidade do sistema, armazenaremos informações de backup para cada pasta de destino separadamente na própria pasta.

Código da classe
 <?php class BigFiles { const FLAG_DELETED = 0x08000000; //    "" /** @var mysqli $_db */ protected $_db = null; protected $_webRoot = ''; protected $_realRoot = ''; function __construct(mysqli $db = null) { $this->_db = $db; } /** * /   URL- * @param string $v  * @return string */ public function webRoot($v = null) { if (!is_null($v)) { $this->_webRoot = $v; } return $this->_webRoot; } /** * /    * @param string $v  * @return string */ public function realRoot($v = null) { if (!is_null($v)) { $this->_realRoot = $v; } return $this->_realRoot; } /** *   * @param array $data   * @param string $url URL   * @param string $eType   * @param int $eID ID  * @param mixed $meta - * @param int $access  * @param int $flags  * @param int $fileID ID   * @return bool * @throws Exception */ public function upload(array $data, $url, $eType = '', $eID = null, $meta = null, $access = 127, $flags = 0, $fileID = 0) { $meta = is_array($meta) ? serialize($meta) : $meta; if (empty($data['tmp_name']) || empty($data['name'])) { $fid = intval($fileID); if (empty($fid)) { return false; } $meta = empty($meta) ? 'null' : "'" . $this->_db->real_escape_string($meta) . "'"; $q = "`meta`={$meta},`updated`=now()"; $this->_db->query("UPDATE `files` SET {$q} WHERE (`id` = {$fid}) AND (`entity_type` = '{$eType}')"); return $fid; } // File data $meta = empty($meta) ? 'null' : "'" . $this->_db->real_escape_string($meta) . "'"; $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo , $data['tmp_name']); finfo_close($finfo); // FID, file name if (empty($fileID)) { $eID = empty($eID) ? 'null' : intval($eID); $q = <<<sql insert into `files` set `mime` = '{$mime}', `entity` = {$eID}, `entityType` = '{$eType}', `created` = now(), `access` = {$access}, `flags` = {$flags} sql; $this->_db->query($q); $fid = $this->_db->insert_id; list($ffs, $fhn) = self::fid($fid); $url = $this->_webRoot . $url . '/' . $fid; $fdir = $this->_realRoot . $ffs; self::validateDir($fdir); $index = self::getIndex($fdir); $index[$fhn] = array($fhn, $mime, $url, ($eID == 'null' ? 0 : $eID), $access, $flags); self::setIndex($fdir, $index); $fname = $ffs . '/' . $fhn . '.file'; } else { $fid = intval($fileID); $fname = $this->fileName($fid); } // Move file $fdir = $this->_realRoot . $fname; if (!move_uploaded_file($data['tmp_name'], $fdir)) { throw new Exception('Upload error'); } $q = '`md5`=\'' . md5_file($fdir) . '\',`sha1`=\'' . sha1_file($fdir) . '\',' . '`size`=' . filesize($fdir) . ',`meta`=' . $meta . ',' . (empty($fileID) ? "`url`='{$url}',`file`='{$fname}'" : '`updated`=now()'); $this->_db->query("UPDATE `files` SET {$q} WHERE (`id` = {$fid}) AND (`entity_type` = '{$eType}')"); return $fid; } /** *   * @param string $url URL * @param string $basicGroup    * @throws Exception */ public function read($url, $basicGroup = 'anonimous') { if (!ctype_alnum(str_replace(array('/', '.', '-', '_'), '', $url))) { header('HTTP/1.1 400 Bad Request'); exit; } $url = $this->_db->real_escape_string($url); $q = "SELECT * FROM `files` WHERE `url` = '{$url}' ORDER BY `created` ASC"; if ($result = $this->_db->query($q)) { $vars = array(); $ints = array('id', 'entity', 'size', 'access', 'flags'); while ($row = $result->fetch_assoc()) { foreach ($ints as $i) { $row[$i] = intval($row[$i]); } $fid = $row['id']; $vars[$fid] = $row; } if (empty($vars)) { header('HTTP/1.1 404 Not Found'); exit; } $deleted = false; $access = true; $found = ''; $mime = ''; foreach ($vars as $fdata) { $flags = intval($fdata['flags']); $deleted = ($flags & self::FLAG_DELETED) != 0; $access = self::granted($basicGroup, $fdata['access']); if (!$access || $deleted) { continue; } $found = $fdata['file']; $mime = $fdata['mime']; } if (empty($found)) { if ($deleted) { header('HTTP/1.1 410 Gone'); exit; } elseif (!$access) { header('HTTP/1.1 403 Forbidden'); exit; } } else { header('Content-type: ' . $mime . '; charset=utf-8'); readfile($this->_realRoot . $found); exit; } } header('HTTP/1.1 404 Not Found'); exit; } /** *   ()   * @param mixed $fid () * @return bool * @throws Exception */ public function delete($fid) { $fid = is_array($fid) ? implode(',', $fid) : $fid; $q = "delete from `table` where `id` in ({$fid})"; $this->_db->query($q); $result = true; foreach ($fid as $fid_i) { list($ffs, $fhn) = self::fid($fid_i); $fdir = $this->_realRoot . $ffs; $index = self::getIndex($fdir); unset($index[$fhn]); self::setIndex($fdir, $index); $result &= unlink($fdir . '/'. $fhn . '.file'); } return $result; } /** *  ()  "" * @param int $fid () * @param bool $value   * @return bool */ public function setDeleted($fid, $value=true) { $fid = is_array($fid) ? implode(',', $fid) : $fid; $o = $value ? ' | ' . self::FLAG_DELETED : ' & ' . (~self::FLAG_DELETED); $this->_db->query("update `files` set `flags` = `flags` {$o} where `id` in ({$fid})"); return true; } /** *   * @param int $fid  * @return string * @throws Exception */ public function fileName($fid) { list($ffs, $fhn) = self::fid($fid); self::validateDir($this->_realRoot . $ffs); return $ffs . '/' . $fhn . '.file'; } /** *   . *           . * @param int $fid   * @return array */ public static function fid($fid) { $ffs = str_split(str_pad(dechex($fid), 16, '0', STR_PAD_LEFT), 2); $fhn = array_pop($ffs); $ffs = implode('/', $ffs); return array($ffs, $fhn); } /** *    * @param string $f     * @return bool * @throws Exception */ public static function validateDir($f) { if (!is_dir($f)) { if (!mkdir($f, 0700, true)) { throw new Exception('cannot make dir: ' . $f); } } return true; } /** *    * @param string $f       * @return array */ public static function getIndex($f) { $index = array(); if (file_exists($f . '/.index')) { $_ = file($f . '/.index'); foreach ($_ as $_i) { $row = trim($_i); $row = explode('|', $row); array_walk($row, 'trim'); $rid = $row[0]; $index[$rid] = $row; } } return $index; } /** *    * @param string $f       * @param array $index    * @return bool */ public static function setIndex($f, array $index) { $_ = array(); foreach ($index as $row) { $_[] = implode('|', $row); } return file_put_contents($f . '/.index', implode("\r\n", $_)); } /** *   * @param string $group   (. ) * @param int $value   * @return bool */ public static function granted($group, $value=0) { $groups = array('anonimous', 'user', 'manager', 'admin', 'inactive', 'blocked', 'deleted'); if ($group == 'root') { return true; } foreach ($groups as $groupID => $groupName) { if ($groupName == $group) { return (((1 << $groupID) & $value) != 0); } } return false; } } 


Considere alguns pontos:

- realRoot - o caminho completo para a pasta com o sistema de arquivos terminando em uma barra.
- webRoot - o caminho da raiz do site sem uma barra inicial (veja abaixo o porquê).
- Como DBMS, eu uso a extensão MySQLi .
- De fato, as informações da matriz $ _FILES são passadas como o primeiro argumento para o método de upload .
- Se você chamar o método de atualização para passar o ID de um arquivo existente, ele será substituído se a matriz de entrada em tmp_name não estiver vazia.
- Você pode excluir e alterar os sinalizadores de arquivos vários de cada vez. Para fazer isso, em vez de passar o identificador de arquivo, você deve passar uma matriz com identificadores ou uma string com eles, separada por vírgulas.

Encaminhamento


Na verdade, ele se resume a algumas linhas no htaccess na raiz do site (pressupõe-se que o mod_rewrite esteja ativado):

 RewriteCond %{REQUEST_URI} ^/content/(.*)$ RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.+)$ content/index.php?file=$1 [L,QSA] 

"Conteúdo" é a pasta na raiz do site no meu caso. Claro que você pode nomear a pasta de maneira diferente. E, claro, o próprio index.php, armazenado no meu caso na pasta de conteúdo:

 <?php $dbHost = '127.0.0.1'; $dbUser = 'user'; $dbPass = '****'; $dbName = 'database'; try { if (empty($_REQUEST['file'])) { header('HTTP/1.1 400 Bad Request'); exit; } $userG = 'anonimous'; //      ;      $files = new BigFiles(new mysqli($dbHost,$dbUser,$dbPass,$dbName)); $files->realRoot(dirname(__FILE__).'/files/'); $files->read($_REQUEST['file'],$userG); } catch (Exception $e) { header('HTTP/1.1 500 Internal Error'); header('Content-Type: text/plain; charset=utf-8'); echo $e->getMessage(); exit; } 

Bem, por si só, fechamos o próprio sistema de arquivos do acesso externo. Coloque o arquivo .htaccess na raiz da pasta content/files com apenas uma linha:

 Deny from all 

Sumário


Esta solução permite evitar a perda de desempenho do sistema de arquivos devido a um aumento no número de arquivos. Pelo menos os problemas na forma de milhares de arquivos em uma pasta podem definitivamente ser evitados. E, ao mesmo tempo, podemos organizar e controlar o acesso a arquivos em endereços legíveis por humanos. Além disso, conformidade com nossa legislação sombria. Faça uma reserva imediatamente, esta solução NÃO é uma maneira completa de proteger o conteúdo. Lembre-se: se algo é reproduzido no navegador, ele pode ser baixado gratuitamente.

Source: https://habr.com/ru/post/pt423875/


All Articles