Penyimpanan sejumlah besar file

gambar

Kesehatan yang bagus, Penjaja! Dalam proses pengerjaan proyek situs kencan, menjadi perlu untuk mengatur penyimpanan foto pengguna. Menurut kerangka acuan, jumlah foto per pengguna dibatasi hingga 10 file. Tetapi mungkin ada puluhan ribu pengguna. Terutama mengingat bahwa proyek dalam bentuk saat ini sudah ada sejak awal "nol". Artinya, sudah ada ribuan pengguna dalam database. Hampir semua sistem file, sejauh yang saya tahu, bereaksi sangat negatif terhadap sejumlah besar simpul anak dalam folder. Dari pengalaman, saya dapat mengatakan bahwa masalah dimulai setelah 1000-1500 file / folder di folder induk.

Penafian. Saya mencari di Google sebelum menulis artikel dan menemukan beberapa solusi untuk masalah yang sedang dibahas (misalnya, di sini atau di sini ). Tetapi saya tidak menemukan solusi tunggal yang sama persis dengan solusi saya. Selain itu, dalam artikel ini saya hanya membagikan pengalaman saya sendiri dalam menyelesaikan masalah.

Teori


Selain tugas penyimpanan seperti itu, ada juga kondisi dalam pernyataan kerja, yang menurutnya perlu meninggalkan keterangan dan keterangan untuk foto. Tentu saja, Anda tidak dapat melakukannya tanpa basis data. Artinya, hal pertama yang kita lakukan adalah membuat tabel di mana kita meresepkan pemetaan meta-data (tanda tangan, judul, dll.) Dengan file pada disk. Setiap file sesuai dengan satu baris dalam database. Dengan demikian, setiap file memiliki pengenal.

Penyimpangan kecil. Mari kita bicara tentang peningkatan otomatis. Situs kencan mungkin memiliki selusin atau dua ribu pengguna. Pertanyaannya adalah berapa banyak pengguna pada umumnya pergi melalui proyek selama seluruh keberadaannya. Misalnya, khalayak aktif kencan-ru adalah beberapa ratus ribu. Namun, bayangkan saja berapa banyak pengguna yang tersisa selama masa proyek ini; berapa banyak pengguna yang belum diaktifkan sejauh ini. Sekarang tambahkan ke undang-undang kami, yang mewajibkan kami untuk menyimpan informasi tentang pengguna selama setidaknya enam bulan ... Cepat atau lambat, 4 dengan satu sen dari INT TANDA akan berakhir. Oleh karena itu, yang terbaik adalah mengambil BIGINT untuk kunci utama.

Sekarang mari kita coba membayangkan sejumlah jenis BIGINT . Ini adalah 8 byte. Setiap byte dari 0 hingga 255. 255 node anak cukup normal untuk sistem file apa pun. Artinya, kami mengambil pengidentifikasi file dalam representasi heksadesimal, kami memecahnya menjadi potongan dua karakter. Kami menggunakan potongan ini sebagai nama folder, yang terakhir sebagai nama file fisik. KEUNTUNGAN!

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

Elegan dan sederhana. Ekstensi file tidak penting di sini. Bagaimanapun, file tersebut akan diberikan oleh skrip yang akan memberikan browser tipe MIME tertentu, yang juga akan kami simpan dalam database. Selain itu, menyimpan informasi tentang file di dalam basis data memungkinkan Anda untuk mendefinisikan kembali jalur ke sana untuk browser. Katakanlah, file yang kita miliki sebenarnya terletak relatif terhadap direktori proyek di sepanjang path /content/files/0f/65/84/10/67/68/19/ff.file . Dan dalam database Anda dapat menulis URL untuk itu, misalnya, /content/users/678/files/somefile . SEO mungkin cukup tersenyum sekarang. Semua ini memungkinkan kita untuk tidak khawatir lagi tentang di mana menempatkan file secara fisik.

Tabel dalam database


Selain pengenal, tipe MIME, URL, dan lokasi fisik, kami akan menyimpan file md5 dan sha1 dalam tabel untuk memfilter file yang sama jika perlu. Tentu saja, kita juga perlu menyimpan hubungan entitas dalam tabel ini. Misalkan ID pengguna tempat file tersebut berada. Dan jika proyeknya tidak terlalu besar, maka dalam sistem yang sama kita dapat menyimpan, katakanlah, foto barang. Oleh karena itu, kami juga akan menyimpan nama kelas entitas tempat catatan itu berada.

Berbicara tentang burung. Jika Anda menutup folder menggunakan .htaccess untuk akses eksternal, file hanya dapat diperoleh melalui skrip. Dan dalam skrip akan dimungkinkan untuk menentukan akses ke file. Ke depan sedikit, saya akan mengatakan bahwa dalam CMS saya (di mana proyek tersebut digergaji sekarang) akses ditentukan oleh grup pengguna dasar, yang saya punya 8 - tamu, pengguna, manajer, admin, tidak aktif, diblokir, dihapus dan admin super. Super-admin dapat melakukan segalanya, sehingga tidak berpartisipasi dalam menentukan akses. Jika pengguna memiliki bendera admin super, maka ia adalah admin super. Semuanya sederhana. Artinya, kami akan menentukan akses ke tujuh grup yang tersisa. Akses sederhana - baik memberikan file atau tidak memberi. Secara total, Anda dapat mengambil bidang tipe TINYINT .

Dan satu hal lagi. Menurut hukum kami, kami harus secara fisik menyimpan gambar khusus. Artinya, kita perlu menandai gambar entah bagaimana terhapus, daripada menghapus secara fisik. Akan lebih mudah untuk menggunakan bidang bit untuk keperluan ini. Dalam kasus seperti itu, saya biasanya menggunakan bidang bertipe INT . Untuk cadangan, untuk berbicara. Selain itu, saya sudah memiliki tradisi menempatkan bendera DIHAPUS di bit ke-5 dari akhir. Tapi itu tidak masalah lagi.

Apa yang kita miliki sebagai hasilnya:

 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; 

Kelas operator


Sekarang kita perlu membuat kelas untuk mengunggah file. Kelas harus menyediakan kemampuan untuk membuat file, mengganti / memodifikasi file, menghapus file. Selain itu, ada baiknya mempertimbangkan dua poin. Pertama, proyek dapat ditransfer dari server ke server. Jadi di kelas Anda perlu mendefinisikan properti yang berisi direktori root file. Kedua, akan sangat tidak menyenangkan jika seseorang membuat tabel dalam database. Jadi, Anda perlu menyediakan kemungkinan pemulihan data. Dengan yang pertama, semuanya umumnya jelas. Adapun cadangan data, kami hanya akan memesan apa yang tidak dapat dipulihkan.

ID - dipulihkan dari lokasi fisik file
entitas_type - tidak dipulihkan
entitas - tidak dipulihkan
mime - dipulihkan menggunakan ekstensi finfo
md5 - dipulihkan dari file itu sendiri
sha1 - dipulihkan dari file itu sendiri
file - dipulihkan dari lokasi fisik file
url - tidak dikembalikan
meta - tidak dikembalikan
ukuran - dikembalikan dari file itu sendiri
dibuat - Anda dapat mengambil informasi dari file
diperbarui - Anda dapat mengambil informasi dari file
akses - tidak dipulihkan
bendera - tidak dipulihkan

Anda dapat segera membuang informasi meta. Itu tidak penting untuk fungsi sistem. Dan untuk pemulihan yang lebih cepat, Anda masih perlu menyimpan tipe MIME. Total: jenis entitas, ID entitas, MIME, URL, akses, dan bendera. Untuk meningkatkan keandalan sistem, kami akan menyimpan informasi cadangan untuk setiap folder tujuan secara terpisah di folder itu sendiri.

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


Pertimbangkan beberapa hal:

- realRoot - path lengkap ke folder dengan sistem file berakhir dengan garis miring.
- webRoot - lintasan dari root situs tanpa garis miring (lihat alasannya di bawah).
- Sebagai DBMS, saya menggunakan ekstensi MySQLi .
- Faktanya, informasi dari array $ _FILES diteruskan sebagai argumen pertama ke metode unggah .
- Jika Anda memanggil metode pembaruan untuk meneruskan ID dari file yang ada, itu akan diganti jika array input di tmp_name tidak kosong.
- Anda dapat menghapus dan mengubah flag beberapa file sekaligus. Untuk melakukan ini, alih-alih meneruskan pengidentifikasi file, Anda harus meneruskan array dengan pengidentifikasi, atau string dengan mereka, dipisahkan dengan koma.

Routing


Sebenarnya, ia turun ke beberapa baris di htaccess di root situs (diasumsikan bahwa mod_rewrite diaktifkan):

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

"Konten" adalah folder di root situs dalam kasus saya. Tentu saja Anda dapat memberi nama folder secara berbeda. Dan tentu saja index.php sendiri, disimpan dalam case saya di folder konten:

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

Nah, dengan sendirinya, kita menutup sistem file itu sendiri dari akses eksternal. Letakkan file .htaccess di root folder content/files dengan hanya satu baris:

 Deny from all 

Ringkasan


Solusi ini memungkinkan Anda untuk menghindari hilangnya kinerja sistem file karena peningkatan jumlah file. Setidaknya masalah dalam bentuk ribuan file dalam satu folder pasti bisa dihindari. Dan pada saat yang sama, kita dapat mengatur dan mengontrol akses ke file di alamat yang dapat dibaca manusia. Ditambah kepatuhan dengan undang-undang suram kami. Segera melakukan reservasi, solusi ini BUKAN cara lengkap untuk melindungi konten. Ingat: jika sesuatu diputar di browser, itu dapat diunduh secara gratis.

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


All Articles