3 final class PhabricatorChunkedFileStorageEngine
4 extends PhabricatorFileStorageEngine
{
6 public function getEngineIdentifier() {
10 public function getEnginePriority() {
15 * We can write chunks if we have at least one valid storage engine
18 public function canWriteFiles() {
19 return (bool)$this->getWritableEngine();
22 public function hasFilesizeLimit() {
26 public function isChunkEngine() {
30 public function writeFile($data, array $params) {
31 // The chunk engine does not support direct writes.
32 throw new PhutilMethodNotImplementedException();
35 public function readFile($handle) {
36 // This is inefficient, but makes the API work as expected.
37 $chunks = $this->loadAllChunks($handle, true);
40 foreach ($chunks as $chunk) {
41 $data_file = $chunk->getDataFile();
43 throw new Exception(pht('This file data is incomplete!'));
46 $buffer .= $chunk->getDataFile()->loadFileData();
52 public function deleteFile($handle) {
53 $engine = new PhabricatorDestructionEngine();
54 $chunks = $this->loadAllChunks($handle, true);
55 foreach ($chunks as $chunk) {
56 $engine->destroyObject($chunk);
60 private function loadAllChunks($handle, $need_files) {
61 $chunks = id(new PhabricatorFileChunkQuery())
62 ->setViewer(PhabricatorUser
::getOmnipotentUser())
63 ->withChunkHandles(array($handle))
64 ->needDataFiles($need_files)
67 $chunks = msort($chunks, 'getByteStart');
73 * Compute a chunked file hash for the viewer.
75 * We can not currently compute a real hash for chunked file uploads (because
76 * no process sees all of the file data).
78 * We also can not trust the hash that the user claims to have computed. If
79 * we trust the user, they can upload some `evil.exe` and claim it has the
80 * same file hash as `good.exe`. When another user later uploads the real
81 * `good.exe`, we'll just create a reference to the existing `evil.exe`. Users
82 * who download `good.exe` will then receive `evil.exe`.
84 * Instead, we rehash the user's claimed hash with account secrets. This
85 * allows users to resume file uploads, but not collide with other users.
87 * Ideally, we'd like to be able to verify hashes, but this is complicated
88 * and time consuming and gives us a fairly small benefit.
90 * @param PhabricatorUser Viewing user.
91 * @param string Claimed file hash.
92 * @return string Rehashed file hash.
94 public static function getChunkedHash(PhabricatorUser
$viewer, $hash) {
95 if (!$viewer->getPHID()) {
97 pht('Unable to compute chunked hash without real viewer!'));
100 $input = $viewer->getAccountSecret().':'.$hash.':'.$viewer->getPHID();
101 return self
::getChunkedHashForInput($input);
104 public static function getChunkedHashForInput($input) {
105 $rehash = PhabricatorHash
::weakDigest($input);
107 // Add a suffix to identify this as a chunk hash.
108 $rehash = substr($rehash, 0, -2).'-C';
113 public function allocateChunks($length, array $properties) {
114 $file = PhabricatorFile
::newChunkedFile($this, $length, $properties);
116 $chunk_size = $this->getChunkSize();
118 $handle = $file->getStorageHandle();
121 for ($ii = 0; $ii < $length; $ii +
= $chunk_size) {
122 $chunks[] = PhabricatorFileChunk
::initializeNewChunk(
125 min($ii +
$chunk_size, $length));
128 $file->openTransaction();
129 foreach ($chunks as $chunk) {
132 $file->saveAndIndex();
133 $file->saveTransaction();
139 * Find a storage engine which is suitable for storing chunks.
141 * This engine must be a writable engine, have a filesize limit larger than
142 * the chunk limit, and must not be a chunk engine itself.
144 private function getWritableEngine() {
145 // NOTE: We can't just load writable engines or we'll loop forever.
146 $engines = parent
::loadAllEngines();
148 foreach ($engines as $engine) {
149 if ($engine->isChunkEngine()) {
153 if ($engine->isTestEngine()) {
157 if (!$engine->canWriteFiles()) {
161 if ($engine->hasFilesizeLimit()) {
162 if ($engine->getFilesizeLimit() < $this->getChunkSize()) {
173 public function getChunkSize() {
174 return (4 * 1024 * 1024);
177 public function getRawFileDataIterator(
178 PhabricatorFile
$file,
181 PhabricatorFileStorageFormat
$format) {
183 // NOTE: It is currently impossible for files stored with the chunk
184 // engine to have their own formatting (instead, the individual chunks
185 // are formatted), so we ignore the format object.
187 $chunks = id(new PhabricatorFileChunkQuery())
188 ->setViewer(PhabricatorUser
::getOmnipotentUser())
189 ->withChunkHandles(array($file->getStorageHandle()))
190 ->withByteRange($begin, $end)
191 ->needDataFiles(true)
194 return new PhabricatorFileChunkIterator($chunks, $begin, $end);