Deliver archives incrementally to avoid OOM errors, by not using smarty
Deliver archives incrementally to avoid OOM errors, by not using smarty

gzip requires a nasty hack using a tempfile because gzip files have a
header and trailer, so they can't be compressed in chunks. And php's
gzopen and compress.zlib:// don't seem to work with anything that's not
a file, so you can't compress on the fly to something like php://temp or
php://output

--- a/include/controller/Controller_Snapshot.class.php
+++ b/include/controller/Controller_Snapshot.class.php
@@ -38,10 +38,18 @@
 	 */
 	public function __construct()
 	{
-		parent::__construct();
+		if (isset($_GET['p'])) {
+			$this->project = GitPHP_ProjectList::GetInstance()->GetProject(str_replace(chr(0), '', $_GET['p']));
+			if (!$this->project) {
+				throw new GitPHP_MessageException(sprintf(__('Invalid project %1$s'), $_GET['p']), true);
+			}
+		}
+
 		if (!$this->project) {
 			throw new GitPHP_MessageException(__('Project is required'), true);
 		}
+
+		$this->ReadQuery();
 	}
 
 	/**
@@ -54,7 +62,6 @@
 	 */
 	protected function GetTemplate()
 	{
-		return 'snapshot.tpl';
 	}
 
 	/**
@@ -118,10 +125,24 @@
 	{
 		$this->archive = new GitPHP_Archive($this->project, null, $this->params['format'], (isset($this->params['path']) ? $this->params['path'] : ''), (isset($this->params['prefix']) ? $this->params['prefix'] : ''));
 
-		$headers = $this->archive->GetHeaders();
-		
-		if (count($headers) > 0)
-			$this->headers = array_merge($this->headers, $headers);
+		switch ($this->archive->GetFormat()) {
+			case GITPHP_COMPRESS_TAR:
+				$this->headers[] = 'Content-Type: application/x-tar';
+				break;
+			case GITPHP_COMPRESS_BZ2:
+				$this->headers[] = 'Content-Type: application/x-bzip2';
+				break;
+			case GITPHP_COMPRESS_GZ:
+				$this->headers[] = 'Content-Type: application/x-gzip';
+				break;
+			case GITPHP_COMPRESS_ZIP:
+				$this->headers[] = 'Content-Type: application/x-zip';
+				break;
+			default:
+				throw new Exception('Unknown compression type');
+		}
+
+		$this->headers[] = 'Content-Disposition: attachment; filename=' . $this->archive->GetFilename();
 	}
 
 	/**
@@ -141,8 +162,28 @@
 			$commit = $this->project->GetCommit($this->params['hash']);
 
 		$this->archive->SetObject($commit);
+	}
 
-		$this->tpl->assign('archive', $this->archive->GetData());
+	/**
+	 * Render
+	 *
+	 * Render this controller
+	 *
+	 * @access public
+	 */
+	public function Render()
+	{
+		$this->LoadData();
+
+		$format = $this->archive->GetFormat();
+
+		if ($this->archive->Open()) {
+			while (($data = $this->archive->Read()) !== false) {
+				print $data;
+				flush();
+			}
+			$this->archive->Close();
+		}
 	}
 
 }

--- a/include/git/Archive.class.php
+++ b/include/git/Archive.class.php
@@ -81,6 +81,24 @@
 	protected $prefix = '';
 
 	/**
+	 * handle
+	 *
+	 * Stores the process handle
+	 *
+	 * @access protected
+	 */
+	protected $handle = false;
+
+	/**
+	 * tempfile
+	 *
+	 * Stores the temp file name
+	 *
+	 * @access protected
+	 */
+	protected $tempfile = '';
+
+	/**
 	 * __construct
 	 *
 	 * Instantiates object
@@ -319,54 +337,22 @@
 	}
 
 	/**
-	 * GetHeaders
-	 *
-	 * Gets the headers to send to the browser for this file
-	 *
-	 * @access public
-	 * @return array header strings
-	 */
-	public function GetHeaders()
-	{
-		$headers = array();
-
-		switch ($this->format) {
-			case GITPHP_COMPRESS_TAR:
-				$headers[] = 'Content-Type: application/x-tar';
-				break;
-			case GITPHP_COMPRESS_BZ2:
-				$headers[] = 'Content-Type: application/x-bzip2';
-				break;
-			case GITPHP_COMPRESS_GZ:
-				$headers[] = 'Content-Type: application/x-gzip';
-				break;
-			case GITPHP_COMPRESS_ZIP:
-				$headers[] = 'Content-Type: application/x-zip';
-				break;
-			default:
-				throw new Exception('Unknown compression type');
-		}
-
-		if (count($headers) > 0) {
-			$headers[] = 'Content-Disposition: attachment; filename=' . $this->GetFilename();
-		}
-
-		return $headers;
-	}
-
-	/**
-	 * GetData
-	 *
-	 * Gets the archive data
-	 *
-	 * @access public
-	 * @return string archive data
-	 */
-	public function GetData()
+	 * Open
+	 *
+	 * Opens a descriptor for reading archive data
+	 *
+	 * @access public
+	 * @return boolean true on success
+	 */
+	public function Open()
 	{
 		if (!$this->gitObject)
 		{
 			throw new Exception('Invalid object for archive');
+		}
+
+		if ($this->handle) {
+			return true;
 		}
 
 		$exe = new GitPHP_GitExe($this->GetProject());
@@ -387,16 +373,93 @@
 		$args[] = '--prefix=' . $this->GetPrefix();
 		$args[] = $this->gitObject->GetHash();
 
-		$data = $exe->Execute(GIT_ARCHIVE, $args);
+		$this->handle = $exe->Open(GIT_ARCHIVE, $args);
 		unset($exe);
 
-		switch ($this->format) {
-			case GITPHP_COMPRESS_BZ2:
-				$data = bzcompress($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', 4));
-				break;
-			case GITPHP_COMPRESS_GZ:
-				$data = gzencode($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', -1));
-				break;
+		if ($this->format == GITPHP_COMPRESS_GZ) {
+			// hack to get around the fact that gzip files
+			// can't be compressed on the fly and the php zlib stream
+			// doesn't seem to daisy chain with any non-file streams
+
+			$this->tempfile = tempnam(sys_get_temp_dir(), "GitPHP");
+
+			$compress = GitPHP_Config::GetInstance()->GetValue('compresslevel');
+
+			$mode = 'wb';
+			if (is_int($compress) && ($compress >= 1) && ($compress <= 9))
+				$mode .= $compress;
+
+			$temphandle = gzopen($this->tempfile, $mode);
+			if ($temphandle) {
+				while (!feof($this->handle)) {
+					gzwrite($temphandle, fread($this->handle, 1048576));
+				}
+				gzclose($temphandle);
+
+				$temphandle = fopen($this->tempfile, 'rb');
+			}
+			
+			if ($this->handle) {
+				pclose($this->handle);
+			}
+			$this->handle = $temphandle;
+		}
+
+		return ($this->handle !== false);
+	}
+
+	/**
+	 * Close
+	 *
+	 * Close the archive data descriptor
+	 *
+	 * @access public
+	 * @return boolean true on success
+	 */
+	public function Close()
+	{
+		if (!$this->handle) {
+			return true;
+		}
+
+		if ($this->format == GITPHP_COMPRESS_GZ) {
+			fclose($this->handle);
+			if (!empty($this->tempfile)) {
+				unlink($this->tempfile);
+				$this->tempfile = '';
+			}
+		} else {
+			pclose($this->handle);
+		}
+
+		$this->handle = null;
+		
+		return true;
+	}
+
+	/**
+	 * Read
+	 *
+	 * Read a chunk of the archive data
+	 *
+	 * @access public
+	 * @param int $size size of data to read
+	 * @return string archive data
+	 */
+	public function Read($size = 1048576)
+	{
+		if (!$this->handle) {
+			return false;
+		}
+
+		if (feof($this->handle)) {
+			return false;
+		}
+
+		$data = fread($this->handle, $size);
+
+		if ($this->format == GITPHP_COMPRESS_BZ2) {
+			$data = bzcompress($data, GitPHP_Config::GetInstance()->GetValue('compresslevel', 4));
 		}
 
 		return $data;

--- a/include/git/GitExe.class.php
+++ b/include/git/GitExe.class.php
@@ -110,6 +110,22 @@
 	}
 
 	/**
+	 * Open
+	 *
+	 * Opens a resource to a command
+	 *
+	 * @param string $command the command to execute
+	 * @param array $args arguments
+	 * @return resource process handle
+	 */
+	public function Open($command, $args, $mode = 'r')
+	{
+		$fullCommand = $this->CreateCommand($command, $args);
+
+		return popen($fullCommand, $mode);
+	}
+
+	/**
 	 * BuildCommand
 	 *
 	 * Creates a command

comments