Page MenuHomePhabricator

D12072.id29055.diff
No OneTemporary

D12072.id29055.diff

diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php
--- a/src/aphront/response/AphrontFileResponse.php
+++ b/src/aphront/response/AphrontFileResponse.php
@@ -3,11 +3,15 @@
final class AphrontFileResponse extends AphrontResponse {
private $content;
+ private $contentIterator;
+ private $contentLength;
+
private $mimeType;
private $download;
private $rangeMin;
private $rangeMax;
private $allowOrigins = array();
+ private $fileToken;
public function addAllowOrigin($origin) {
$this->allowOrigins[] = $origin;
@@ -36,17 +40,34 @@
}
public function setContent($content) {
+ $this->setContentLength(strlen($content));
$this->content = $content;
return $this;
}
+ public function setContentIterator($iterator) {
+ $this->contentIterator = $iterator;
+ return $this;
+ }
+
public function buildResponseString() {
- if ($this->rangeMin || $this->rangeMax) {
- $length = ($this->rangeMax - $this->rangeMin) + 1;
- return substr($this->content, $this->rangeMin, $length);
- } else {
- return $this->content;
+ return $this->content;
+ }
+
+ public function getContentIterator() {
+ if ($this->contentIterator) {
+ return $this->contentIterator;
}
+ return parent::getContentIterator();
+ }
+
+ public function setContentLength($length) {
+ $this->contentLength = $length;
+ return $this;
+ }
+
+ public function getContentLength() {
+ return $this->contentLength;
}
public function setRange($min, $max) {
@@ -55,19 +76,36 @@
return $this;
}
+ public function setTemporaryFileToken(PhabricatorAuthTemporaryToken $token) {
+ $this->fileToken = $token;
+ return $this;
+ }
+
+ public function getTemporaryFileToken() {
+ return $this->fileToken;
+ }
+
public function getHeaders() {
$headers = array(
array('Content-Type', $this->getMimeType()),
- array('Content-Length', strlen($this->buildResponseString())),
+ // This tells clients that we can support requests with a "Range" header,
+ // which allows downloads to be resumed, in some browsers, some of the
+ // time, if the stars align.
+ array('Accept-Ranges', 'bytes'),
);
if ($this->rangeMin || $this->rangeMax) {
- $len = strlen($this->content);
+ $len = $this->getContentLength();
$min = $this->rangeMin;
$max = $this->rangeMax;
$headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}");
+ $content_len = ($max - $min) + 1;
+ } else {
+ $content_len = $this->getContentLength();
}
+ $headers[] = array('Content-Length', $this->getContentLength());
+
if (strlen($this->getDownload())) {
$headers[] = array('X-Download-Options', 'noopen');
@@ -90,4 +128,15 @@
return $headers;
}
+ public function didCompleteWrite($aborted) {
+ if (!$aborted) {
+ $token = $this->getTemporaryFileToken();
+ if ($token) {
+ $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
+ $token->delete();
+ unset($unguarded);
+ }
+ }
+ }
+
}
diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php
--- a/src/aphront/response/AphrontResponse.php
+++ b/src/aphront/response/AphrontResponse.php
@@ -18,6 +18,22 @@
return $this->request;
}
+
+/* -( Content )------------------------------------------------------------ */
+
+
+ public function getContentIterator() {
+ return array($this->buildResponseString());
+ }
+
+ public function buildResponseString() {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+
+/* -( Metadata )----------------------------------------------------------- */
+
+
public function getHeaders() {
$headers = array();
if (!$this->frameable) {
@@ -165,6 +181,8 @@
return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
}
- abstract public function buildResponseString();
+ public function didCompleteWrite($aborted) {
+ return;
+ }
}
diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php
--- a/src/aphront/sink/AphrontHTTPSink.php
+++ b/src/aphront/sink/AphrontHTTPSink.php
@@ -94,8 +94,10 @@
* @return void
*/
final public function writeResponse(AphrontResponse $response) {
- // Do this first, in case it throws.
- $response_string = $response->buildResponseString();
+ // Build the content iterator first, in case it throws. Ideally, we'd
+ // prefer to handle exceptions before we emit the response status or any
+ // HTTP headers.
+ $data = $response->getContentIterator();
$all_headers = array_merge(
$response->getHeaders(),
@@ -105,7 +107,17 @@
$response->getHTTPResponseCode(),
$response->getHTTPResponseMessage());
$this->writeHeaders($all_headers);
- $this->writeData($response_string);
+
+ $abort = false;
+ foreach ($data as $block) {
+ if (!$this->isWritable()) {
+ $abort = true;
+ break;
+ }
+ $this->writeData($block);
+ }
+
+ $response->didCompleteWrite($abort);
}
@@ -115,5 +127,6 @@
abstract protected function emitHTTPStatus($code, $message = '');
abstract protected function emitHeader($name, $value);
abstract protected function emitData($data);
+ abstract protected function isWritable();
}
diff --git a/src/aphront/sink/AphrontIsolatedHTTPSink.php b/src/aphront/sink/AphrontIsolatedHTTPSink.php
--- a/src/aphront/sink/AphrontIsolatedHTTPSink.php
+++ b/src/aphront/sink/AphrontIsolatedHTTPSink.php
@@ -21,6 +21,10 @@
$this->data .= $data;
}
+ protected function isWritable() {
+ return true;
+ }
+
public function getEmittedHTTPStatus() {
return $this->status;
}
diff --git a/src/aphront/sink/AphrontPHPHTTPSink.php b/src/aphront/sink/AphrontPHPHTTPSink.php
--- a/src/aphront/sink/AphrontPHPHTTPSink.php
+++ b/src/aphront/sink/AphrontPHPHTTPSink.php
@@ -21,6 +21,15 @@
protected function emitData($data) {
echo $data;
+
+ // Try to push the data to the browser. This has a lot of caveats around
+ // browser buffering and display behavior, but approximately works most
+ // of the time.
+ flush();
+ }
+
+ protected function isWritable() {
+ return !connection_aborted();
}
}
diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php
--- a/src/applications/files/controller/PhabricatorFileDataController.php
+++ b/src/applications/files/controller/PhabricatorFileDataController.php
@@ -117,13 +117,14 @@
}
}
- $data = $file->loadFileData();
$response = new AphrontFileResponse();
- $response->setContent($data);
if ($cache_response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
}
+ $begin = null;
+ $end = null;
+
// NOTE: It's important to accept "Range" requests when playing audio.
// If we don't, Safari has difficulty figuring out how long sounds are
// and glitches when trying to loop them. In particular, Safari sends
@@ -133,14 +134,18 @@
if ($range) {
$matches = null;
if (preg_match('/^bytes=(\d+)-(\d+)$/', $range, $matches)) {
+ // Note that the "Range" header specifies bytes differently than
+ // we do internally: the range 0-1 has 2 bytes (byte 0 and byte 1).
+ $begin = (int)$matches[1];
+ $end = (int)$matches[2] + 1;
+
$response->setHTTPResponseCode(206);
- $response->setRange((int)$matches[1], (int)$matches[2]);
+ $response->setRange($begin, ($end - 1));
}
} else if (isset($validated_token)) {
- // consume the one-time token if we have one.
- $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
- $validated_token->delete();
- unset($unguarded);
+ // We set this on the response, and the response deletes it after the
+ // transfer completes. This allows transfers to be resumed, in theory.
+ $response->setTemporaryFileToken($validated_token);
}
$is_viewable = $file->isViewableInBrowser();
@@ -165,6 +170,11 @@
$response->setDownload($file->getName());
}
+ $iterator = $file->getFileDataIterator($begin, $end);
+
+ $response->setContentLength($file->getByteSize());
+ $response->setContentIterator($iterator);
+
return $response;
}
diff --git a/src/applications/files/query/PhabricatorFileSearchEngine.php b/src/applications/files/query/PhabricatorFileSearchEngine.php
--- a/src/applications/files/query/PhabricatorFileSearchEngine.php
+++ b/src/applications/files/query/PhabricatorFileSearchEngine.php
@@ -25,8 +25,12 @@
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
- $query = id(new PhabricatorFileQuery())
- ->withAuthorPHIDs($saved->getParameter('authorPHIDs', array()));
+ $query = id(new PhabricatorFileQuery());
+
+ $author_phids = $saved->getParameter('authorPHIDs', array());
+ if ($author_phids) {
+ $query->withAuthorPHIDs($author_phids);
+ }
if ($saved->getParameter('explicit')) {
$query->showOnlyExplicitUploads(true);

File Metadata

Mime Type
text/plain
Expires
Wed, Mar 19, 4:23 PM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7668564
Default Alt Text
D12072.id29055.diff (9 KB)

Event Timeline