Almacenamiento de una gran cantidad de archivos.

imagen

¡Buena salud, Hawkers! En el proceso de trabajar en un proyecto de sitio de citas, se hizo necesario organizar el almacenamiento de las fotos de los usuarios. Según los términos de referencia, el número de fotos por usuario está limitado a 10 archivos. Pero puede haber decenas de miles de usuarios. Especialmente considerando que el proyecto en su forma actual ya existe desde el comienzo del "cero". Es decir, ya hay miles de usuarios en la base de datos. Casi cualquier sistema de archivos, que yo sepa, reacciona muy negativamente a una gran cantidad de nodos secundarios en una carpeta. Por experiencia, puedo decir que los problemas comienzan después de 1000-1500 archivos / carpetas en la carpeta principal.

Descargo de responsabilidad. Busqué en Google antes de escribir el artículo y encontré varias soluciones al problema en discusión (por ejemplo, aquí o aquí ). Pero no encontré una solución única que coincida exactamente con la mía. Además, en este artículo solo comparto mi propia experiencia para resolver el problema.

Teoría


Además de la tarea de almacenamiento como tal, también había una condición en la declaración de trabajo, según la cual era necesario dejar subtítulos y subtítulos para las fotos. Por supuesto, no puedes prescindir de una base de datos. Es decir, lo primero que hacemos es crear una tabla en la que prescribimos la asignación de metadatos (firmas, títulos, etc.) con los archivos en el disco. Cada archivo corresponde a una fila en la base de datos. En consecuencia, cada archivo tiene un identificador.

Una pequeña digresión. Hablemos de auto-incremento. Un sitio de citas puede tener una docena o dos mil usuarios. La pregunta es cuántos usuarios generalmente pasan por el proyecto durante todo el tiempo de su existencia. Por ejemplo, la audiencia activa de citas-ru es varios cientos de miles. Sin embargo, solo imagine cuántos usuarios quedaron durante la vida útil de este proyecto; cuántos usuarios no están activados hasta ahora. Y ahora agregue nuestra legislación, que nos obliga a almacenar información sobre los usuarios durante al menos seis meses ... Tarde o temprano, 4 con un centavo de INT NO FIRMADO finalizará. Por lo tanto, es mejor tomar BIGINT para la clave primaria.

Ahora intentemos imaginar un número de tipo BIGINT . Esto es de 8 bytes. Cada byte es de 0 a 255. 255 nodos secundarios son bastante normales para cualquier sistema de archivos. Es decir, tomamos el identificador de archivo en representación hexadecimal, lo dividimos en fragmentos de dos caracteres. Usamos estos fragmentos como nombres de carpeta, este último como el nombre del archivo físico. BENEFICIOS!

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

Elegante y simple La extensión del archivo no es importante aquí. De todos modos, el archivo será proporcionado por un script que le dará al navegador un tipo MIME particular, que también almacenaremos en la base de datos. Además, almacenar información sobre el archivo en la base de datos le permite redefinir la ruta para el navegador. Digamos que el archivo que tenemos está ubicado en relación con el directorio del proyecto a lo largo de la ruta /content/files/0f/65/84/10/67/68/19/ff.file . Y en la base de datos puede escribirle una URL, por ejemplo, /content/users/678/files/somefile . Los SEO probablemente sonríen bastante en este momento. Todo esto nos permite no preocuparnos más sobre dónde colocar el archivo físicamente.

Tabla en la base de datos


Además del identificador, tipo MIME, URL y ubicación física, almacenaremos archivos md5 y sha1 en la tabla para filtrar los mismos archivos si es necesario. Por supuesto, también necesitamos almacenar las relaciones entre entidades en esta tabla. Suponga la ID de usuario a la que pertenecen los archivos. Y si el proyecto no es muy grande, entonces en el mismo sistema podemos almacenar, por ejemplo, fotografías de productos. Por lo tanto, también almacenaremos el nombre de la clase de entidad a la que pertenece el registro.

Hablando de pájaros. Si cierra la carpeta usando .htaccess para acceso externo, el archivo solo se puede obtener a través del script. Y en el script será posible determinar el acceso al archivo. Mirando hacia el futuro un poco, diré que en mi CMS (donde el proyecto mencionado se está cortando ahora) el acceso está determinado por grupos de usuarios básicos, de los cuales tengo 8: invitados, usuarios, gerentes, administradores, inactivos, bloqueados, eliminados y súper administradores. Super-admin puede hacer absolutamente todo, por lo que no participa en la determinación del acceso. Si el usuario tiene un indicador de superadministrador, entonces es un superadministrador. Todo es simple Es decir, determinaremos el acceso a los siete grupos restantes. El acceso es simple: dar el archivo o no dar. En total, puede tomar un campo de tipo TINYINT .

Y una cosa más. De acuerdo con nuestra ley, tendremos que almacenar físicamente imágenes personalizadas. Es decir, necesitamos marcar de alguna manera las imágenes como eliminadas, en lugar de eliminarlas físicamente. Es más conveniente utilizar un campo de bits para estos fines. En tales casos, generalmente uso un campo de tipo INT . Para reservar, por así decirlo. Además, tengo una tradición ya establecida de colocar la bandera BORRADO en el quinto bit desde el final. Pero ya no importa otra vez.

¿Qué tenemos 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; 

Clase de despachador


Ahora necesitamos crear una clase con la cual cargaremos archivos. La clase debe proporcionar la capacidad de crear archivos, reemplazar / modificar archivos, eliminar archivos. Además, vale la pena considerar dos puntos. En primer lugar, el proyecto se puede transferir de un servidor a otro. Entonces, en la clase, debe definir una propiedad que contenga el directorio raíz de los archivos. En segundo lugar, será muy desagradable si alguien golpea una tabla en la base de datos. Por lo tanto, debe prever la posibilidad de recuperación de datos. Con el primero, todo está generalmente claro. En cuanto a la copia de seguridad de datos, reservaremos solo lo que no se pueda restaurar.

ID : restaurado desde la ubicación física del archivo
entidad_tipo : no restaurado
entidad - no restaurada
mime - restaurado usando la extensión finfo
md5 : se restaura desde el archivo en sí
sha1 : restaurado desde el archivo mismo
archivo : restaurado desde la ubicación física del archivo
url - no restaurado
meta - no restaurado
tamaño : restaurado desde el archivo en sí
creado : puede tomar información de un archivo
actualizado : puede tomar información de un archivo
acceso - no restaurado
banderas - no restauradas

Puede descartar inmediatamente la metainformación. No es crítico para el funcionamiento del sistema. Y para una recuperación más rápida, aún necesita guardar el tipo MIME. Total: tipo de entidad, ID de entidad, MIME, URL, acceso e indicadores. Para aumentar la confiabilidad del sistema, almacenaremos la información de respaldo para cada carpeta de destino por separado en la carpeta misma.

Código de clase
 <?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 algunos puntos:

- realRoot : la ruta completa a la carpeta con el sistema de archivos que termina en una barra inclinada.
- webRoot : la ruta desde la raíz del sitio sin una barra inclinada (ver a continuación para saber por qué).
- Como DBMS, uso la extensión MySQLi .
- De hecho, la información de la matriz $ _FILES se pasa como el primer argumento para el método de carga .
- Si llama al método de actualización para pasar la ID de un archivo existente, se reemplazará si la matriz de entrada en tmp_name no está vacía.
- Puede eliminar y cambiar las banderas de los archivos varias a la vez. Para hacer esto, en lugar de pasar el identificador de archivo, debe pasar una matriz con identificadores o una cadena con ellos, separados por comas.

Enrutamiento


En realidad, se reduce a unas pocas líneas en htaccess en la raíz del sitio (se supone que mod_rewrite está habilitado):

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

"Contenido" es la carpeta en la raíz del sitio en mi caso. Por supuesto, puede nombrar la carpeta de manera diferente. Y, por supuesto, index.php en sí, almacenado en mi caso en la carpeta de contenido:

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

Bueno, por sí mismo, cerramos el sistema de archivos del acceso externo. Coloque el archivo .htaccess en la raíz de la carpeta de content/files con solo una línea:

 Deny from all 

Resumen


Esta solución le permite evitar la pérdida del rendimiento del sistema de archivos debido a un aumento en la cantidad de archivos. Al menos, definitivamente se pueden evitar los problemas en forma de miles de archivos en una carpeta. Y al mismo tiempo, podemos organizar y controlar el acceso a los archivos en direcciones legibles por humanos. Además del cumplimiento de nuestra legislación sombría. Haga una reserva de inmediato, esta solución NO es una forma completa de proteger el contenido. Recuerde: si algo se reproduce en el navegador, se puede descargar de forma gratuita.

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


All Articles