存储大量文件

图片

身体健康,小贩们! 在处理约会网站项目的过程中,必须组织用户照片的存储。 根据职权范围,每位用户的照片数量限制为10个文件。 但是可能有成千上万的用户。 特别要考虑到当前形式的项目从“零”开始就已经存在。 也就是说,数据库中已经有成千上万的用户。 据我所知,几乎所有文件系统都会对文件夹中的大量子节点做出非常负面的反应。 从经验来看,我可以说问题是在父文件夹中的1000-1500个文件/文件夹之后开始的。

免责声明 在撰写本文之前,我在Google上进行了搜索,并找到了正在讨论的问题的几种解决方案(例如, herehere )。 但是我没有找到一个完全符合我的解决方案。 另外,在本文中,我仅分享我自己解决问题的经验。

理论


除了这样的存储任务之外,工作陈述中还存在一个条件,根据该条件,有必要为照片保留标题和标题。 当然,离不开数据库。 也就是说,我们要做的第一件事是创建一个表,在其中写入带有磁盘上文件的元数据映射(签名,标题等)。 每个文件对应于数据库中的一行。 因此,每个文件都有一个标识符。

一个小题外话。 让我们谈谈自动增量。 约会网站可能有12或2000个用户。 问题是在项目存在的整个过程中,通常有多少用户浏览该项目。 例如,约会约会的活跃观众是几十万。 但是,试想一下,在这个项目的生命周期中还剩下多少用户? 到目前为止尚未激活多少用户。 现在添加我们的法律,该法律使我们有义务将有关用户的信息存储至少六个月。迟早会有4字钱的UNSIGNED INT终止。 因此,最好将BIGINT用作主键。

现在,让我们想象一下BIGINT类型的数量。 这是8个字节。 每个字节从0到255。255个子节点对于任何文件系统都是很正常的。 也就是说,我们采用十六进制表示形式的文件标识符,将其分成两个字符的块。 我们将这些块用作文件夹名称,将后者用作物理文件的名称。 赢利!

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

优雅而简单。 文件扩展名在这里并不重要。 无论如何,该文件将由脚本给定,该脚本将为浏览器提供特定的MIME类型,我们还将其存储在数据库中。 此外,将有关文件的信息存储在数据库中使您可以为浏览器重新定义文件的路径。 假设我们拥有的文件实际上位于/content/files/0f/65/84/10/67/68/19/ff.file路径/content/files/0f/65/84/10/67/68/19/ff.file对于项目目录的/content/files/0f/65/84/10/67/68/19/ff.file 。 并且,您可以在数据库中写入URL,例如/content/users/678/files/somefile 。 SEO现在可能已经被漂亮地笑了。 所有这些使我们不再担心文件实际放置的位置。

数据库中的表


除了标识符,MIME类型,URL和物理位置,我们还将在表中存储md5和sha1文件,以在必要时过滤掉相同的文件。 当然,我们还需要在此表中存储实体关系。 假设文件所属的用户标识。 如果项目不是很大,那么在同一个系统中,我们可以存储商品照片。 因此,我们还将存储记录所属的实体类的名称。

说起鸟。 如果使用.htaccess进行外部访问关闭文件夹,则只能通过脚本获取文件。 并且可以在脚本中确定对文件的访问。 展望未来,我要说的是,在CMS(现在正在查看上述项目的地方)中,访问权限是由基本用户组确定的,我有8位用户-来宾,用户,管理员,管理员,不活动,已阻止,已删除和超级管理员。 超级管理员可以做所有事情,因此它不参与确定访问权限。 如果用户具有超级管理员标志,则他是超级管理员。 一切都很简单。 也就是说,我们将确定对其余七个组的访问权限。 访问很简单-提供文件或不提供文件。 总的来说,您可以输入TINYINT类型的字段。

还有一件事。 根据我们的法律,我们将必须物理存储自定义图像。 也就是说,我们需要以某种方式将图片标记为已删除,而不是物理删除。 为此目的使用位字段是最方便的。 在这种情况下,我通常使用INT类型的字段。 可以保留,可以这么说。 此外,我已经建立了将DELETED标志放在末尾第5位的传统。 但这又没有关系。

结果是:

 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; 

调度员班


现在,我们需要创建一个用于上传文件的类。 该类应具有创建文件,替换/修改文件,删除文件的能力。 另外,值得考虑两点。 首先,项目可以在服务器之间传输。 因此,在该类中,您需要定义一个包含文件根目录的属性。 其次,如果有人在数据库中敲一个表将是非常不愉快的。 因此,您需要提供数据恢复的可能性。 首先,一切都基本清楚。 至于数据备份,我们将只保留无法恢复的内容。

ID-从文件的物理位置还原
entity_type-未还原
实体 -未还原
MIME-使用finfo扩展名还原
md5-从文件本身还原
sha1-从文件本身还原
文件 -从文件的物理位置还原
网址 -未还原
-未还原
大小 -从文件本身还原
已创建 -您可以从文件中获取信息
已更新 -您可以从文件中获取信息
访问 -未还原
标志 -未还原

您可以立即丢弃元信息。 这对系统的功能并不重要。 为了更快地恢复,您仍然需要保存MIME类型。 总计:实体类型,实体ID,MIME,URL,访问和标志。 为了提高系统的可靠性,我们将每个目标文件夹的备份信息分别存储在文件夹本身中。

类代码
 <?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; } } 


考虑一些要点:

-realRoot-文件系统以斜杠结尾的文件夹的完整路径。
-webRoot-从站点根开始的路径,不带斜杠(原因请参见下文)。
-作为DBMS,我使用MySQLi扩展。
-实际上,来自$ _FILES数组的信息作为第一个参数传递给上方法。
-如果调用update方法传递现有文件的ID,则如果tmp_name中的输入数组不为空,它将被替换。
-您可以一次删除和更改多个文件的标志。 为此,您必须传递带有标识符的数组或包含标识符的字符串,并用逗号分隔,而不是传递文件标识符。

路由选择


实际上,它可以归结为站点根目录下htaccess中的几行(假定已启用mod_rewrite):

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

就我而言,“内容”是网站根目录中的文件夹。 当然,您可以为文件夹命名不同。 当然还有index.php本身,以我的情况存储在内容文件夹中:

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

好吧,就其本身而言,我们从外部访问中关闭文件系统本身。 将.htaccess文件放在content/files夹的根目录中,只需一行:

 Deny from all 

总结


通过此解决方案,可以避免由于文件数量增加而导致文件系统性能下降。 至少可以肯定地避免一个文件夹中成千上万个文件形式的麻烦。 同时,我们可以组织和控制对人类可读地址文件的访问。 还要遵守我们令人沮丧的法律。 立即预订,此解决方案不是保护内容的完整方法。 请记住:如果浏览器中播放了某些内容,则可以免费下载。

Source: https://habr.com/ru/post/zh-CN423875/


All Articles