Speicherung einer großen Anzahl von Dateien

    Bild

    Gute Gesundheit, Habrazhiteli! Im Zuge der Arbeit an einem Dating-Site-Projekt wurde es notwendig, die Speicherung von Fotos von Benutzern zu organisieren. Gemäß der Leistungsbeschreibung ist die Anzahl der Fotos eines Benutzers auf 10 Dateien beschränkt. Es kann jedoch Zehntausende von Benutzern geben. Insbesondere angesichts der Tatsache, dass das Projekt in seiner jetzigen Form bereits am Anfang der Null steht. Das heißt, es gibt bereits Tausende von Benutzern in der Datenbank. Nahezu jedes Dateisystem reagiert, soweit ich weiß, sehr negativ auf die große Anzahl untergeordneter Knoten im Ordner. Aus Erfahrung kann ich sagen, dass Probleme nach 1000-1500 Dateien / Ordnern im übergeordneten Ordner beginnen.

    Haftungsausschluss Ich habe gegoogelt, bevor ich den Artikel geschrieben habe, und habe mehrere Lösungen für das angesprochene Thema gefunden (zum Beispiel hier oder hier ). Ich habe aber keine einzige Lösung gefunden, die genau zu meiner passt. Darüber hinaus teile ich in diesem Artikel nur meine eigenen Erfahrungen bei der Lösung des Problems.

    Theorie


    Neben der Speicheraufgabe an sich gab es auch eine Bedingung in der TOR, wonach die Fähigkeit, Bildunterschriften und Fotos zu hinterlassen, erforderlich war. Ohne Datenbank reicht das natürlich nicht. Das heißt, als Erstes erstellen wir eine Tabelle, in der wir eine Zuordnung von Metadaten (Signaturen, Titel usw.) mit Dateien auf der Festplatte vorschreiben. Jede Datei entspricht einer Zeile in der Datenbank. Dementsprechend hat jede Datei eine Kennung.

    Ein kleiner Exkurs. Sprechen wir über Autoincrement. Auf einer Dating-Site kann es ein Dutzend oder zweitausend Benutzer geben. Die Frage ist, wie viele Benutzer das Projekt während seiner gesamten Dauer durchlaufen. Zum Beispiel ist das aktive Publikum von "dating-ru" mehrere Hunderttausend. Stellen Sie sich jedoch vor, wie viele Benutzer sich während der Laufzeit dieses Projekts zurückgezogen haben. Wie viele Benutzer wurden noch nicht aktiviert. Und jetzt fügen Sie unsere Gesetzgebung hinzu, die Sie verpflichtet, Informationen über Benutzer für mindestens sechs Monate zu speichern ... Früher oder später werden 4 von einer Milliarde UNSIGNED INT- Kopeken ausgehen. Daher ist es am besten, BIGINT als Primärschlüssel zu verwenden .

    Versuchen wir uns nun eine BIGINT- Nummer vorzustellen.. Das sind 8 Bytes. Jedes Byte reicht von 0 bis 255. 255 untergeordnete Knoten sind für jedes Dateisystem normal. Das heißt, wir nehmen die Datei-ID in hexadezimaler Darstellung und teilen sie in zwei Zeichen auf. Wir verwenden diese Blöcke als Ordnernamen, wobei letzterer der Name einer physischen Datei ist. PROFIT!

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

    Elegant und einfach. Die Dateierweiterung ist hier nicht wichtig. Trotzdem wird die Datei an das Skript übergeben, das dem Browser insbesondere den MIME-Typ gibt, den wir ebenfalls in der Datenbank speichern. Durch das Speichern von Informationen zu einer Datei in der Datenbank können Sie außerdem den Pfad des Browsers überschreiben. Nehmen wir an, die Datei, die wir tatsächlich haben, ist relativ zum Projektverzeichnis /content/files/0f/65/84/10/67/68/19/ff.file. Und in die Datenbank können Sie ihm die URL schreiben, zum Beispiel/content/users/678/files/somefile. SEO-Schnick hat jetzt wohl recht gelächelt. Dadurch können wir uns nicht mehr darum kümmern, wo die Datei physisch abgelegt wird.

    Tabelle in db


    Zusätzlich zu dem Bezeichner, dem MIME-Typ, der URL und dem physischen Ort werden wir in der Tabelle MD5- und SHA1-Dateien speichern, um bei Bedarf dieselben Dateien auszusuchen. Natürlich müssen wir auch Beziehungen zu Entitäten in dieser Tabelle speichern. Angenommen, die Benutzer-ID, zu der die Dateien gehören. Und wenn das Projekt nicht sehr groß ist, können wir in demselben System beispielsweise Fotos der Ware speichern. Dadurch speichern wir auch den Namen der Entitätsklasse, zu der der Eintrag gehört.

    Übrigens über die Vögel. Wenn Sie den Ordner mit .htaccess für den externen Zugriff schließen, kann die Datei nur über ein Skript abgerufen werden. Im Skript kann der Zugriff auf die Datei bestimmt werden. Wenn ich ein wenig in die Zukunft schaue, sage ich, dass der Zugriff in meinem CMS (wo das zuvor erwähnte Projekt gesägt wird) von einfachen Benutzergruppen bestimmt wird, von denen ich acht habe - Gäste, Benutzer, Manager, Administratoren, nicht aktivierte, blockierte, Remote- und Super-Administratoren. Ein Superadministrator kann absolut alles tun, also ist er nicht an der Definition des Zugriffs beteiligt. Wenn der Benutzer ein Super-Admin-Flag hat, ist er ein Super-Admin. Es ist einfach Das heißt, wir werden den Zugang der restlichen sieben Gruppen bestimmen. Der Zugriff ist einfach - entweder um die Datei zu geben oder nicht. Insgesamt können Sie den Feldtyp TINYINT annehmen .

    Und noch eine Sache. Gemäß unserer Gesetzgebung müssen wir benutzerdefinierte Bilder physisch speichern. Das heißt, wir müssen die Bilder irgendwie als gelöscht markieren, anstatt sie physisch zu löschen. Am bequemsten ist es, ein Bitfeld zu verwenden. In solchen Fällen verwende ich normalerweise das Feld INT . Sozusagen einkaufen. Außerdem habe ich bereits Tradition, das DELETED- Flag ab dem 5. Bit in das 5. Bit zu setzen. Grundsätzlich ist es nicht dasselbe.

    Was haben wir am Ende:

    createtable`files` (
      `id`bigintnotnull auto_increment, -- Первичный ключ`entity_type`char(32) notnulldefault'', -- Тип сущности`entity`bigintnull, -- ID сущности`mime`char(32) notnulldefault'', -- MIME-тип`md5`char(32) notnulldefault'', -- MD5`sha1`char(40) notnulldefault'', -- SHA1`file`char(64) notnulldefault'', -- Физическое расположение`url`varchar(250) notnulldefault'', -- URL`meta`textnull, -- Мета-данные в формате JSON или сериализованного массива`size`bigintnotnulldefault'0', -- Размер`created`     datetime notnull, -- Дата создания`updated`     datetime null, -- Дата редактирования`access`      tinyint notnulldefault'0', -- Битовый доступ`flags`intnotnulldefault'0', -- Флаги
      primary key (`id`),
      index (`entity_type`),
      index (`entity`),
      index (`mime`),
      index (`md5`),
      index (`sha1`),
      index (`url`)  
    ) engine = InnoDB;

    Dispatcher-Klasse


    Nun müssen wir eine Klasse erstellen, mit der wir Dateien hochladen können. Die Klasse muss das Erstellen von Dateien, das Ersetzen / Ändern von Dateien und das Löschen von Dateien ermöglichen. Darüber hinaus lohnt es sich, zwei Punkte zu berücksichtigen. Zunächst kann das Projekt von Server zu Server übertragen werden. In der Klasse müssen Sie also eine Eigenschaft definieren, die das Stammverzeichnis der Dateien enthält. Zweitens wird es sehr unangenehm sein, wenn jemand eine Tabelle in der Datenbank schlägt. Sie müssen also die Möglichkeit der Datenwiederherstellung bieten. Mit dem ersten ist alles klar. Bei der Datensicherung reservieren wir nur etwas, das nicht wiederhergestellt werden kann.

    ID - wird aus dem physischen Speicherort der Datei
    entity_type wiederhergestellt. Es wird keine
    Entität wiederhergestellt. Der Mime wird nicht wiederhergestellt
    - wieder mit Hilfe der Erweiterung finfo
    der md5 - von der Datei erholt
    SHA1 - aus der Datei erholt sich
    Datei - vom physischen Speicherort der Datei erholt
    keine URL - nicht wiederherstellen
    die Meta - nicht wiederhergestellt
    Size - Bett - aus der Datei erholt sich
    Erstellt - Sie die Informationen aus der Datei nehmen kann
    aktualisiert - nehmen Informationen aus der Datei
    Zugriff - nicht wiederhergestellt
    die Flaggen - nicht gestellt

    Sie können die Metainformationen sofort verwerfen. Es ist für das Funktionieren des Systems nicht kritisch. Und für eine schnellere Wiederherstellung müssen Sie den MIME-Typ immer noch speichern. Gesamt: Entitätstyp, Entitäts-ID, MIME, URL, Zugriff und Flags. Um die Zuverlässigkeit des Systems zu erhöhen, werden Sicherungsinformationen zu jedem endgültigen Ordner separat im Ordner selbst gespeichert.

    Klassencode
    <?phpclassBigFiles{
        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
         */publicfunctionwebRoot($v = null){
            if (!is_null($v)) {
                $this->_webRoot = $v;
            }
            return$this->_webRoot;
        }
        /**
         * Установка/чтение корня для файлов
         * @param string $v  Значение
         * @return string
         */publicfunctionrealRoot($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
         */publicfunctionupload(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)) {
                    returnfalse;
                }
                $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 nameif (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)) {
                thrownewException('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
         */publicfunctionread($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
         */publicfunctiondelete($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
         */publicfunctionsetDeleted($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})");
            returntrue;
        }
        /**
         * Имя файла
         * @param int $fid  Идентификатор
         * @return string
         * @throws Exception
         */publicfunctionfileName($fid){
            list($ffs, $fhn) = self::fid($fid);
            self::validateDir($this->_realRoot . $ffs);
            return $ffs . '/' . $fhn . '.file';
        }
        /**
         * Обработка идентификатора файла.
         * Возвращает массив с папкой к файлу и шестнадцатиричное представление младшего байта.
         * @param int $fid  Идентификатор файла
         * @return array
         */publicstaticfunctionfid($fid){
            $ffs = str_split(str_pad(dechex($fid), 16, '0', STR_PAD_LEFT), 2);
            $fhn = array_pop($ffs);
            $ffs = implode('/', $ffs);
            returnarray($ffs, $fhn);
        }
        /**
         * Проверка каталога файла
         * @param string $f  Полный путь к каталогу
         * @return bool
         * @throws Exception
         */publicstaticfunctionvalidateDir($f){
            if (!is_dir($f)) {
                if (!mkdir($f, 0700, true)) {
                    thrownewException('cannot make dir: ' . $f);
                }
            }
            returntrue;
        }
        /**
         * Чтение резервного индекса
         * @param string $f  Полный путь к файлу резервного индекса
         * @return array
         */publicstaticfunctiongetIndex($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
         */publicstaticfunctionsetIndex($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
         */publicstaticfunctiongranted($group, $value=0){
            $groups = array('anonimous', 'user', 'manager', 'admin', 'inactive', 'blocked', 'deleted');
            if ($group == 'root') {
                returntrue;
            }
            foreach ($groups as $groupID => $groupName) {
                if ($groupName == $group) {
                    return (((1 << $groupID) & $value) != 0);
                }
            }
            returnfalse;
        }
    }


    Beachten Sie einige Punkte:

    - realRoot - der vollständige Pfad zum Ordner, wobei das Dateisystem mit einem Schrägstrich endet.
    - webRoot - Der Pfad vom Stamm der Site ohne führenden Schrägstrich (siehe unten).
    - Als DBMS verwende ich die MySQLi- Erweiterung .
    - Die Upload- Methode überträgt das erste Argument in die Informationen aus dem $ _FILES-Array .
    - Wenn beim Aufrufen der Aktualisierungsmethode die ID einer vorhandenen Datei übertragen wird, wird diese ersetzt, wenn das Eingabearray im Namen tmp_name nicht leer ist.
    - Sie können die Dateiflaggen für mehrere Teile gleichzeitig entfernen und ändern. Dazu müssen Sie anstelle der Datei-ID entweder ein Array mit Bezeichnern oder eine durch Kommas getrennte Zeichenfolge übertragen.

    Routing


    Eigentlich läuft alles auf mehrere Zeilen in htaccess in der Site-Wurzel hinaus (es wird angenommen, dass mod_rewrite aktiviert ist):

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

    "Inhalt" ist in meinem Fall der Ordner im Stammverzeichnis der Site. Natürlich können Sie den Ordner auch anders benennen. Und natürlich die index.php selbst, die in meinem Fall im Inhaltsordner gespeichert ist:

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

    Nun, an sich schließen wir das Dateisystem selbst von einem externen Zugriff. Fügen Sie die content/filesDatei .htaccessmit nur einer Zeile im Stammverzeichnis des Ordners ein :

    Deny from all

    Das Ergebnis


    Diese Lösung vermeidet den Verlust der Dateisystemleistung aufgrund der erhöhten Anzahl von Dateien. Zumindest Probleme in Form von Tausenden von Dateien in einem Ordner können genau vermieden werden. Gleichzeitig können wir den Zugriff auf Dateien anhand von lesbaren Adressen organisieren und steuern. Plus Einhaltung unserer düsteren Gesetzgebung. Wenn Sie sofort reservieren, ist diese Lösung KEINE vollständige Möglichkeit, Inhalte zu schützen. Denken Sie daran: Wenn etwas in einem Browser abgespielt wird, kann es kostenlos heruntergeladen werden.

    Jetzt auch beliebt: