2016-01-21 13:25:38 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
abstract class PhabricatorFileUploadSource
|
|
|
|
extends Phobject {
|
|
|
|
|
|
|
|
private $name;
|
Make the Files "TTL" API more structured
Summary:
Ref T11357. When creating a file, callers can currently specify a `ttl`. However, it isn't unambiguous what you're supposed to pass, and some callers get it wrong.
For example, to mean "this file expires in 60 minutes", you might pass either of these:
- `time() + phutil_units('60 minutes in seconds')`
- `phutil_units('60 minutes in seconds')`
The former means "60 minutes from now". The latter means "1 AM, January 1, 1970". In practice, because the GC normally runs only once every four hours (at least, until recently), and all the bad TTLs are cases where files are normally accessed immediately, these 1970 TTLs didn't cause any real problems.
Split `ttl` into `ttl.relative` and `ttl.absolute`, and make sure the values are sane. Then correct all callers, and simplify out the `time()` calls where possible to make switching to `PhabricatorTime` easier.
Test Plan:
- Generated an SSH keypair.
- Viewed a changeset.
- Viewed a raw diff.
- Viewed a commit's file data.
- Viewed a temporary file's details, saw expiration date and relative time.
- Ran unit tests.
- (Didn't really test Phragment.)
Reviewers: chad
Reviewed By: chad
Subscribers: hach-que
Maniphest Tasks: T11357
Differential Revision: https://secure.phabricator.com/D17616
2017-04-04 20:01:43 +02:00
|
|
|
private $relativeTTL;
|
2016-01-21 13:25:38 +01:00
|
|
|
private $viewPolicy;
|
|
|
|
|
|
|
|
private $rope;
|
|
|
|
private $data;
|
|
|
|
private $shouldChunk;
|
|
|
|
private $didRewind;
|
|
|
|
private $totalBytesWritten = 0;
|
|
|
|
|
|
|
|
public function setName($name) {
|
|
|
|
$this->name = $name;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getName() {
|
|
|
|
return $this->name;
|
|
|
|
}
|
|
|
|
|
Make the Files "TTL" API more structured
Summary:
Ref T11357. When creating a file, callers can currently specify a `ttl`. However, it isn't unambiguous what you're supposed to pass, and some callers get it wrong.
For example, to mean "this file expires in 60 minutes", you might pass either of these:
- `time() + phutil_units('60 minutes in seconds')`
- `phutil_units('60 minutes in seconds')`
The former means "60 minutes from now". The latter means "1 AM, January 1, 1970". In practice, because the GC normally runs only once every four hours (at least, until recently), and all the bad TTLs are cases where files are normally accessed immediately, these 1970 TTLs didn't cause any real problems.
Split `ttl` into `ttl.relative` and `ttl.absolute`, and make sure the values are sane. Then correct all callers, and simplify out the `time()` calls where possible to make switching to `PhabricatorTime` easier.
Test Plan:
- Generated an SSH keypair.
- Viewed a changeset.
- Viewed a raw diff.
- Viewed a commit's file data.
- Viewed a temporary file's details, saw expiration date and relative time.
- Ran unit tests.
- (Didn't really test Phragment.)
Reviewers: chad
Reviewed By: chad
Subscribers: hach-que
Maniphest Tasks: T11357
Differential Revision: https://secure.phabricator.com/D17616
2017-04-04 20:01:43 +02:00
|
|
|
public function setRelativeTTL($relative_ttl) {
|
|
|
|
$this->relativeTTL = $relative_ttl;
|
2016-01-21 13:25:38 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
Make the Files "TTL" API more structured
Summary:
Ref T11357. When creating a file, callers can currently specify a `ttl`. However, it isn't unambiguous what you're supposed to pass, and some callers get it wrong.
For example, to mean "this file expires in 60 minutes", you might pass either of these:
- `time() + phutil_units('60 minutes in seconds')`
- `phutil_units('60 minutes in seconds')`
The former means "60 minutes from now". The latter means "1 AM, January 1, 1970". In practice, because the GC normally runs only once every four hours (at least, until recently), and all the bad TTLs are cases where files are normally accessed immediately, these 1970 TTLs didn't cause any real problems.
Split `ttl` into `ttl.relative` and `ttl.absolute`, and make sure the values are sane. Then correct all callers, and simplify out the `time()` calls where possible to make switching to `PhabricatorTime` easier.
Test Plan:
- Generated an SSH keypair.
- Viewed a changeset.
- Viewed a raw diff.
- Viewed a commit's file data.
- Viewed a temporary file's details, saw expiration date and relative time.
- Ran unit tests.
- (Didn't really test Phragment.)
Reviewers: chad
Reviewed By: chad
Subscribers: hach-que
Maniphest Tasks: T11357
Differential Revision: https://secure.phabricator.com/D17616
2017-04-04 20:01:43 +02:00
|
|
|
public function getRelativeTTL() {
|
|
|
|
return $this->relativeTTL;
|
2016-01-21 13:25:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function setViewPolicy($view_policy) {
|
|
|
|
$this->viewPolicy = $view_policy;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getViewPolicy() {
|
|
|
|
return $this->viewPolicy;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function uploadFile() {
|
|
|
|
if (!$this->shouldChunkFile()) {
|
|
|
|
return $this->writeSingleFile();
|
|
|
|
} else {
|
|
|
|
return $this->writeChunkedFile();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getDataIterator() {
|
|
|
|
if (!$this->data) {
|
|
|
|
$this->data = $this->newDataIterator();
|
|
|
|
}
|
|
|
|
return $this->data;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getRope() {
|
|
|
|
if (!$this->rope) {
|
|
|
|
$this->rope = new PhutilRope();
|
|
|
|
}
|
|
|
|
return $this->rope;
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract protected function newDataIterator();
|
|
|
|
abstract protected function getDataLength();
|
|
|
|
|
|
|
|
private function readFileData() {
|
|
|
|
$data = $this->getDataIterator();
|
|
|
|
|
|
|
|
if (!$this->didRewind) {
|
|
|
|
$data->rewind();
|
|
|
|
$this->didRewind = true;
|
|
|
|
} else {
|
2016-03-18 14:22:37 +01:00
|
|
|
if ($data->valid()) {
|
|
|
|
$data->next();
|
|
|
|
}
|
2016-01-21 13:25:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$data->valid()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$rope = $this->getRope();
|
|
|
|
$rope->append($data->current());
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function shouldChunkFile() {
|
|
|
|
if ($this->shouldChunk !== null) {
|
|
|
|
return $this->shouldChunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
$threshold = PhabricatorFileStorageEngine::getChunkThreshold();
|
|
|
|
|
2016-02-03 23:44:19 +01:00
|
|
|
if ($threshold === null) {
|
|
|
|
// If there are no chunk engines available, we clearly can't chunk the
|
|
|
|
// file.
|
|
|
|
$this->shouldChunk = false;
|
|
|
|
} else {
|
|
|
|
// If we don't know how large the file is, we're going to read some data
|
|
|
|
// from it until we know whether it's a small file or not. This will give
|
|
|
|
// us enough information to make a decision about chunking.
|
|
|
|
$length = $this->getDataLength();
|
|
|
|
if ($length === null) {
|
|
|
|
$rope = $this->getRope();
|
|
|
|
while ($this->readFileData()) {
|
|
|
|
$length = $rope->getByteLength();
|
|
|
|
if ($length > $threshold) {
|
|
|
|
break;
|
|
|
|
}
|
2016-01-21 13:25:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-03 23:44:19 +01:00
|
|
|
$this->shouldChunk = ($length > $threshold);
|
|
|
|
}
|
2016-01-21 13:25:38 +01:00
|
|
|
|
|
|
|
return $this->shouldChunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function writeSingleFile() {
|
|
|
|
while ($this->readFileData()) {
|
|
|
|
// Read the entire file.
|
|
|
|
}
|
|
|
|
|
|
|
|
$rope = $this->getRope();
|
|
|
|
$data = $rope->getAsString();
|
|
|
|
|
|
|
|
$parameters = $this->getNewFileParameters();
|
|
|
|
|
|
|
|
return PhabricatorFile::newFromFileData($data, $parameters);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function writeChunkedFile() {
|
|
|
|
$engine = $this->getChunkEngine();
|
|
|
|
|
|
|
|
$parameters = $this->getNewFileParameters();
|
|
|
|
|
|
|
|
$data_length = $this->getDataLength();
|
|
|
|
if ($data_length !== null) {
|
|
|
|
$length = $data_length;
|
|
|
|
} else {
|
|
|
|
$length = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
$file = PhabricatorFile::newChunkedFile($engine, $length, $parameters);
|
2017-04-18 17:13:34 +02:00
|
|
|
$file->saveAndIndex();
|
2016-01-21 13:25:38 +01:00
|
|
|
|
|
|
|
$rope = $this->getRope();
|
|
|
|
|
|
|
|
// Read the source, writing chunks as we get enough data.
|
|
|
|
while ($this->readFileData()) {
|
|
|
|
while (true) {
|
|
|
|
$rope_length = $rope->getByteLength();
|
|
|
|
if ($rope_length < $engine->getChunkSize()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
$this->writeChunk($file, $engine);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we have extra bytes at the end, write them.
|
|
|
|
if ($rope->getByteLength()) {
|
|
|
|
$this->writeChunk($file, $engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
$file->setIsPartial(0);
|
|
|
|
if ($data_length === null) {
|
|
|
|
$file->setByteSize($this->getTotalBytesWritten());
|
|
|
|
}
|
|
|
|
$file->save();
|
|
|
|
|
|
|
|
return $file;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function writeChunk(
|
|
|
|
PhabricatorFile $file,
|
|
|
|
PhabricatorFileStorageEngine $engine) {
|
|
|
|
|
|
|
|
$offset = $this->getTotalBytesWritten();
|
|
|
|
$max_length = $engine->getChunkSize();
|
|
|
|
$rope = $this->getRope();
|
|
|
|
|
|
|
|
$data = $rope->getPrefixBytes($max_length);
|
|
|
|
$actual_length = strlen($data);
|
|
|
|
$rope->removeBytesFromHead($actual_length);
|
|
|
|
|
|
|
|
$chunk_data = PhabricatorFile::newFromFileData(
|
|
|
|
$data,
|
|
|
|
array(
|
|
|
|
'name' => $file->getMonogram().'.chunk-'.$offset,
|
|
|
|
'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
|
|
|
|
));
|
|
|
|
|
|
|
|
$chunk = PhabricatorFileChunk::initializeNewChunk(
|
|
|
|
$file->getStorageHandle(),
|
|
|
|
$offset,
|
|
|
|
$offset + $actual_length);
|
|
|
|
|
|
|
|
$chunk
|
|
|
|
->setDataFilePHID($chunk_data->getPHID())
|
|
|
|
->save();
|
|
|
|
|
|
|
|
$this->setTotalBytesWritten($offset + $actual_length);
|
|
|
|
|
|
|
|
return $chunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getNewFileParameters() {
|
2017-06-12 20:19:37 +02:00
|
|
|
$parameters = array(
|
2016-01-21 13:25:38 +01:00
|
|
|
'name' => $this->getName(),
|
|
|
|
'viewPolicy' => $this->getViewPolicy(),
|
|
|
|
);
|
2017-06-12 20:19:37 +02:00
|
|
|
|
|
|
|
$ttl = $this->getRelativeTTL();
|
|
|
|
if ($ttl !== null) {
|
|
|
|
$parameters['ttl.relative'] = $ttl;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $parameters;
|
2016-01-21 13:25:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private function getChunkEngine() {
|
|
|
|
$chunk_engines = PhabricatorFileStorageEngine::loadWritableChunkEngines();
|
|
|
|
if (!$chunk_engines) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'Unable to upload file: this server is not configured with any '.
|
|
|
|
'storage engine which can store large files.'));
|
|
|
|
}
|
|
|
|
|
|
|
|
return head($chunk_engines);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function setTotalBytesWritten($total_bytes_written) {
|
|
|
|
$this->totalBytesWritten = $total_bytes_written;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getTotalBytesWritten() {
|
|
|
|
return $this->totalBytesWritten;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|