Path: blob/master/src/applications/files/engine/PhabricatorFileStorageEngine.php
12241 views
<?php12/**3* Defines a storage engine which can write file data somewhere (like a4* database, local disk, Amazon S3, the A:\ drive, or a custom filer) and5* retrieve it later.6*7* You can extend this class to provide new file storage backends.8*9* For more information, see @{article:File Storage Technical Documentation}.10*11* @task construct Constructing an Engine12* @task meta Engine Metadata13* @task file Managing File Data14* @task load Loading Storage Engines15*/16abstract class PhabricatorFileStorageEngine extends Phobject {1718const HMAC_INTEGRITY = 'file.integrity';1920/**21* Construct a new storage engine.22*23* @task construct24*/25final public function __construct() {26// <empty>27}282930/* -( Engine Metadata )---------------------------------------------------- */313233/**34* Return a unique, nonempty string which identifies this storage engine.35* This is used to look up the storage engine when files needs to be read or36* deleted. For instance, if you store files by giving them to a duck for37* safe keeping in his nest down by the pond, you might return 'duck' from38* this method.39*40* @return string Unique string for this engine, max length 32.41* @task meta42*/43abstract public function getEngineIdentifier();444546/**47* Prioritize this engine relative to other engines.48*49* Engines with a smaller priority number get an opportunity to write files50* first. Generally, lower-latency filestores should have lower priority51* numbers, and higher-latency filestores should have higher priority52* numbers. Setting priority to approximately the number of milliseconds of53* read latency will generally produce reasonable results.54*55* In conjunction with filesize limits, the goal is to store small files like56* profile images, thumbnails, and text snippets in lower-latency engines,57* and store large files in higher-capacity engines.58*59* @return float Engine priority.60* @task meta61*/62abstract public function getEnginePriority();636465/**66* Return `true` if the engine is currently writable.67*68* Engines that are disabled or missing configuration should return `false`69* to prevent new writes. If writes were made with this engine in the past,70* the application may still try to perform reads.71*72* @return bool True if this engine can support new writes.73* @task meta74*/75abstract public function canWriteFiles();767778/**79* Return `true` if the engine has a filesize limit on storable files.80*81* The @{method:getFilesizeLimit} method can retrieve the actual limit. This82* method just removes the ambiguity around the meaning of a `0` limit.83*84* @return bool `true` if the engine has a filesize limit.85* @task meta86*/87public function hasFilesizeLimit() {88return true;89}909192/**93* Return maximum storable file size, in bytes.94*95* Not all engines have a limit; use @{method:getFilesizeLimit} to check if96* an engine has a limit. Engines without a limit can store files of any97* size.98*99* By default, engines define a limit which supports chunked storage of100* large files. In most cases, you should not change this limit, even if an101* engine has vast storage capacity: chunked storage makes large files more102* manageable and enables features like resumable uploads.103*104* @return int Maximum storable file size, in bytes.105* @task meta106*/107public function getFilesizeLimit() {108// NOTE: This 8MB limit is selected to be larger than the 4MB chunk size,109// but not much larger. Files between 0MB and 8MB will be stored normally;110// files larger than 8MB will be chunked.111return (1024 * 1024 * 8);112}113114115/**116* Identifies storage engines that support unit tests.117*118* These engines are not used for production writes.119*120* @return bool True if this is a test engine.121* @task meta122*/123public function isTestEngine() {124return false;125}126127128/**129* Identifies chunking storage engines.130*131* If this is a storage engine which splits files into chunks and stores the132* chunks in other engines, it can return `true` to signal that other133* chunking engines should not try to store data here.134*135* @return bool True if this is a chunk engine.136* @task meta137*/138public function isChunkEngine() {139return false;140}141142143/* -( Managing File Data )------------------------------------------------- */144145146/**147* Write file data to the backing storage and return a handle which can later148* be used to read or delete it. For example, if the backing storage is local149* disk, the handle could be the path to the file.150*151* The caller will provide a $params array, which may be empty or may have152* some metadata keys (like "name" and "author") in it. You should be prepared153* to handle writes which specify no metadata, but might want to optionally154* use some keys in this array for debugging or logging purposes. This is155* the same dictionary passed to @{method:PhabricatorFile::newFromFileData},156* so you could conceivably do custom things with it.157*158* If you are unable to write for whatever reason (e.g., the disk is full),159* throw an exception. If there are other satisfactory but less-preferred160* storage engines available, they will be tried.161*162* @param string The file data to write.163* @param array File metadata (name, author), if available.164* @return string Unique string which identifies the stored file, max length165* 255.166* @task file167*/168abstract public function writeFile($data, array $params);169170171/**172* Read the contents of a file previously written by @{method:writeFile}.173*174* @param string The handle returned from @{method:writeFile} when the175* file was written.176* @return string File contents.177* @task file178*/179abstract public function readFile($handle);180181182/**183* Delete the data for a file previously written by @{method:writeFile}.184*185* @param string The handle returned from @{method:writeFile} when the186* file was written.187* @return void188* @task file189*/190abstract public function deleteFile($handle);191192193194/* -( Loading Storage Engines )-------------------------------------------- */195196197/**198* Select viable default storage engines according to configuration. We'll199* select the MySQL and Local Disk storage engines if they are configured200* to allow a given file.201*202* @param int File size in bytes.203* @task load204*/205public static function loadStorageEngines($length) {206$engines = self::loadWritableEngines();207208$writable = array();209foreach ($engines as $key => $engine) {210if ($engine->hasFilesizeLimit()) {211$limit = $engine->getFilesizeLimit();212if ($limit < $length) {213continue;214}215}216217$writable[$key] = $engine;218}219220return $writable;221}222223224/**225* @task load226*/227public static function loadAllEngines() {228return id(new PhutilClassMapQuery())229->setAncestorClass(__CLASS__)230->setUniqueMethod('getEngineIdentifier')231->setSortMethod('getEnginePriority')232->execute();233}234235236/**237* @task load238*/239private static function loadProductionEngines() {240$engines = self::loadAllEngines();241242$active = array();243foreach ($engines as $key => $engine) {244if ($engine->isTestEngine()) {245continue;246}247248$active[$key] = $engine;249}250251return $active;252}253254255/**256* @task load257*/258public static function loadWritableEngines() {259$engines = self::loadProductionEngines();260261$writable = array();262foreach ($engines as $key => $engine) {263if (!$engine->canWriteFiles()) {264continue;265}266267if ($engine->isChunkEngine()) {268// Don't select chunk engines as writable.269continue;270}271$writable[$key] = $engine;272}273274return $writable;275}276277/**278* @task load279*/280public static function loadWritableChunkEngines() {281$engines = self::loadProductionEngines();282283$chunk = array();284foreach ($engines as $key => $engine) {285if (!$engine->canWriteFiles()) {286continue;287}288if (!$engine->isChunkEngine()) {289continue;290}291$chunk[$key] = $engine;292}293294return $chunk;295}296297298299/**300* Return the largest file size which can not be uploaded in chunks.301*302* Files smaller than this will always upload in one request, so clients303* can safely skip the allocation step.304*305* @return int|null Byte size, or `null` if there is no chunk support.306*/307public static function getChunkThreshold() {308$engines = self::loadWritableChunkEngines();309310$min = null;311foreach ($engines as $engine) {312if (!$min) {313$min = $engine;314continue;315}316317if ($min->getChunkSize() > $engine->getChunkSize()) {318$min = $engine->getChunkSize();319}320}321322if (!$min) {323return null;324}325326return $engine->getChunkSize();327}328329public function getRawFileDataIterator(330PhabricatorFile $file,331$begin,332$end,333PhabricatorFileStorageFormat $format) {334335$formatted_data = $this->readFile($file->getStorageHandle());336337$known_integrity = $file->getIntegrityHash();338if ($known_integrity !== null) {339$new_integrity = $this->newIntegrityHash($formatted_data, $format);340if (!phutil_hashes_are_identical($known_integrity, $new_integrity)) {341throw new PhabricatorFileIntegrityException(342pht(343'File data integrity check failed. Dark forces have corrupted '.344'or tampered with this file. The file data can not be read.'));345}346}347348$formatted_data = array($formatted_data);349350$data = '';351$format_iterator = $format->newReadIterator($formatted_data);352foreach ($format_iterator as $raw_chunk) {353$data .= $raw_chunk;354}355356if ($begin !== null && $end !== null) {357$data = substr($data, $begin, ($end - $begin));358} else if ($begin !== null) {359$data = substr($data, $begin);360} else if ($end !== null) {361$data = substr($data, 0, $end);362}363364return array($data);365}366367public function newIntegrityHash(368$data,369PhabricatorFileStorageFormat $format) {370371$hmac_name = self::HMAC_INTEGRITY;372373$data_hash = PhabricatorHash::digestWithNamedKey($data, $hmac_name);374$format_hash = $format->newFormatIntegrityHash();375376$full_hash = "{$data_hash}/{$format_hash}";377378return PhabricatorHash::digestWithNamedKey($full_hash, $hmac_name);379}380381}382383384