Rework the way IDF's SCM interface provides downloadable snapshots.

Instead of returning a command which gets executed and which should
pass through / stream its output data to the client, we're just
returning an instance of Pluf_HTTP_Response. This is needed, because
some SCMs, most noticable monotone, have no locally executable command
to provide a snapshot archive (and probably never will for our kind
of setup).

We therefor added a little BSD-licensed class "ZipArchive" which allows
the creation of pkzip-compatible archives on the fly by letting it eat
the file contents directly feed from the (remote) stdio instance.
Download performance is ok and lies between 15K/s and 110K/s, but at
least we do no longer block the browser while we pre-generate the zip
file server-side.

Thanks to Patrick Georgi for all his work!
This commit is contained in:
Thomas Keller
2010-10-30 21:52:40 +00:00
parent 8a55952204
commit fe001abd26
13 changed files with 3875 additions and 15 deletions

View File

@@ -393,13 +393,13 @@ class IDF_Scm
}
/**
* Generate the command to create a zip archive at a given commit.
* Generate a zip archive at a given commit, wrapped in a HTTP response, suitable for pushing to client.
*
* @param string Commit
* @param string Prefix ('repository/')
* @return string Command
* @return Pluf_HTTP_Response The HTTP Response containing the zip archive
*/
public function getArchiveCommand($commit, $prefix='repository/')
public function getArchiveStream($commit, $prefix='repository/')
{
throw new Pluf_Exception_NotImplemented();
}

View File

@@ -547,13 +547,14 @@ class IDF_Scm_Git extends IDF_Scm
return $res;
}
public function getArchiveCommand($commit, $prefix='repository/')
public function getArchiveStream($commit, $prefix='repository/')
{
return sprintf(Pluf::f('idf_exec_cmd_prefix', '').
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
'GIT_DIR=%s '.Pluf::f('git_path', 'git').' archive --format=zip --prefix=%s %s',
escapeshellarg($this->repo),
escapeshellarg($prefix),
escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
}
/*
@@ -806,4 +807,4 @@ class IDF_Scm_Git extends IDF_Scm
}
return false;
}
}
}

View File

@@ -449,17 +449,18 @@ class IDF_Scm_Mercurial extends IDF_Scm
}
/**
* Generate the command to create a zip archive at a given commit.
* Generate a zip archive at a given commit.
*
* @param string Commit
* @param string Prefix ('git-repo-dump')
* @return string Command
* @return Pluf_HTTP_Response The HTTP response containing the zip archive
*/
public function getArchiveCommand($commit, $prefix='')
protected function getArchiveStream($commit, $prefix='')
{
return sprintf(Pluf::f('idf_exec_cmd_prefix', '').
$cmd = sprintf(Pluf::f('idf_exec_cmd_prefix', '').
Pluf::f('hg_path', 'hg').' archive --type=zip -R %s -r %s -',
escapeshellarg($this->repo),
escapeshellarg($commit));
return new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
}
}

View File

@@ -21,8 +21,8 @@
#
# ***** END LICENSE BLOCK ***** */
require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
//require_once(dirname(__FILE__) . "/Monotone/Stdio.php");
//require_once(dirname(__FILE__) . "/Monotone/BasicIO.php");
/**
* Monotone scm class
@@ -135,6 +135,20 @@ class IDF_Scm_Monotone extends IDF_Scm
return $branch;
}
/**
* @see IDF_Scm::getArchiveStream
*/
public function getArchiveStream($commit, $prefix='repository/')
{
$revs = $this->_resolveSelector($commit);
// sanity: this should actually not happen, because the
// revision is validated before already
if (count($revs) == 0) {
return new Pluf_HTTP_Response_NotFound();
}
return new IDF_Scm_Monotone_ZipRender($this->stdio, $revs[0]);
}
/**
* expands a selector or a partial revision id to zero, one or
* multiple 40 byte revision ids

View File

@@ -0,0 +1,78 @@
<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of InDefero, an open source project management application.
# Copyright (C) 2010 Céondo Ltd and contributors.
#
# Plume Framework is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Plume Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
require_once(IDF_PATH.'/../contrib/zipstream-php-0.2.2/zipstream.php');
/**
* Special response object to output
*
* The Content-Length will not be set as it is not possible to predict it.
*
* Note: The ZipArchive version 0.2.2 has been patched in-tree with this
* patch http://pastebin.ca/1977584 to avoid a couple of PHP notices
*
*/
class IDF_Scm_Monotone_ZipRender extends Pluf_HTTP_Response
{
/**
* The revision argument must be a safe string!
*
* @param Object stdio context
* @param string revision
* @param string Mimetype (null)
*/
private $stdio = null;
private $revision = null;
function __construct($stdio, $revision)
{
parent::__construct($revision, 'application/x-zip');
$this->stdio = $stdio;
$this->revision = $revision;
}
/**
* Render a response object.
*/
function render($output_body=true)
{
$this->outputHeaders();
if ($output_body) {
$manifest = $this->stdio->exec(array('get_manifest_of', $this->revision));
$stanzas = IDF_Scm_Monotone_BasicIO::parse($manifest);
$zip = new ZipStream();
foreach ($stanzas as $stanza) {
if ($stanza[0]['key'] != 'file')
continue;
$content = $this->stdio->exec(array('get_file', $stanza[1]['hash']));
$zip->add_file($stanza[0]['values'][0], $content);
}
$zip->finish();
}
}
}

View File

@@ -434,8 +434,7 @@ class IDF_Views_Source
$commit = trim($match[2]);
$scm = IDF_Scm::get($request->project);
$base = $request->project->shortname.'-'.$commit;
$cmd = $scm->getArchiveCommand($commit, $base.'/');
$rep = new Pluf_HTTP_Response_CommandPassThru($cmd, 'application/x-zip');
$rep = $scm->getArchiveStream($commit, $base.'/');
$rep->headers['Content-Transfer-Encoding'] = 'binary';
$rep->headers['Content-Disposition'] = 'attachment; filename="'.$base.'.zip"';
return $rep;

View File

@@ -48,7 +48,7 @@
</table>
{aurl 'url', 'IDF_Views_Source::download', array($project.shortname, $commit)}
<p class="right soft">
{* <a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'} *}
<a href="{$url}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/package-grey.png'}" alt="{trans 'Archive'}" align="bottom" /></a> <a href="{$url}">{trans 'Download this version'}</a> {trans 'or'}
<kbd>mtn clone {$project.getSourceAccessUrl($user, $commit)}</kbd> <a href="{url 'IDF_Views_Source::help', array($project.shortname)}"><img style="vertical-align: text-bottom;" src="{media '/idf/img/help.png'}" alt="{trans 'Help'}" /></a>
</p>