diff --git a/src/IDF/Form/UploadArchive.php b/src/IDF/Form/UploadArchive.php new file mode 100644 index 0000000..671a0d3 --- /dev/null +++ b/src/IDF/Form/UploadArchive.php @@ -0,0 +1,171 @@ +user = $extra['user']; + $this->project = $extra['project']; + + $this->fields['archive'] = new Pluf_Form_Field_File( + array('required' => true, + 'label' => __('Archive file'), + 'initial' => '', + 'max_size' => Pluf::f('max_upload_archive_size', 20971520), + )); + } + + + public function clean_archive() + { + $extra = strtolower(implode('|', explode(' ', Pluf::f('idf_extra_upload_ext')))); + if (strlen($extra)) $extra .= '|'; + if (!preg_match('/\.('.$extra.'png|jpg|jpeg|gif|bmp|psd|tif|aiff|asf|avi|bz2|css|doc|eps|gz|jar|mdtext|mid|mov|mp3|mpg|ogg|pdf|ppt|ps|qt|ra|ram|rm|rtf|sdd|sdw|sit|sxi|sxw|swf|tgz|txt|wav|xls|xml|war|wmv|zip)$/i', $this->cleaned_data['file'])) { + @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file']); + throw new Pluf_Form_Invalid(__('For security reasons, you cannot upload a file with this extension.')); + } + return $this->cleaned_data['file']; + } + + /** + * Validate the interconnection in the form. + */ + public function clean() + { + $conf = new IDF_Conf(); + $conf->setProject($this->project); + $onemax = array(); + foreach (explode(',', $conf->getVal('labels_download_one_max', IDF_Form_UploadConf::init_one_max)) as $class) { + if (trim($class) != '') { + $onemax[] = mb_strtolower(trim($class)); + } + } + $count = array(); + for ($i=1;$i<7;$i++) { + $this->cleaned_data['label'.$i] = trim($this->cleaned_data['label'.$i]); + if (strpos($this->cleaned_data['label'.$i], ':') !== false) { + list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2); + list($class, $name) = array(mb_strtolower(trim($class)), + trim($name)); + } else { + $class = 'other'; + $name = $this->cleaned_data['label'.$i]; + } + if (!isset($count[$class])) $count[$class] = 1; + else $count[$class] += 1; + if (in_array($class, $onemax) and $count[$class] > 1) { + if (!isset($this->errors['label'.$i])) $this->errors['label'.$i] = array(); + $this->errors['label'.$i][] = sprintf(__('You cannot provide more than label from the %s class to an issue.'), $class); + throw new Pluf_Form_Invalid(__('You provided an invalid label.')); + } + } + return $this->cleaned_data; + } + + /** + * If we have uploaded a file, but the form failed remove it. + * + */ + function failed() + { + if (!empty($this->cleaned_data['file']) + and file_exists(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file'])) { + @unlink(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file']); + } + } + + /** + * Save the model in the database. + * + * @param bool Commit in the database or not. If not, the object + * is returned but not saved in the database. + * @return Object Model with data set from the form. + */ + function save($commit=true) + { + if (!$this->isValid()) { + throw new Exception(__('Cannot save the model from an invalid form.')); + } + // Add a tag for each label + $tags = array(); + for ($i=1;$i<7;$i++) { + if (strlen($this->cleaned_data['label'.$i]) > 0) { + if (strpos($this->cleaned_data['label'.$i], ':') !== false) { + list($class, $name) = explode(':', $this->cleaned_data['label'.$i], 2); + list($class, $name) = array(trim($class), trim($name)); + } else { + $class = 'Other'; + $name = trim($this->cleaned_data['label'.$i]); + } + $tags[] = IDF_Tag::add($name, $this->project, $class); + } + } + // Create the upload + $upload = new IDF_Upload(); + $upload->project = $this->project; + $upload->submitter = $this->user; + $upload->summary = trim($this->cleaned_data['summary']); + $upload->changelog = trim($this->cleaned_data['changelog']); + $upload->file = $this->cleaned_data['file']; + $upload->filesize = filesize(Pluf::f('upload_path').'/'.$this->project->shortname.'/files/'.$this->cleaned_data['file']); + $upload->downloads = 0; + $upload->create(); + foreach ($tags as $tag) { + $upload->setAssoc($tag); + } + // Send the notification + $upload->notify($this->project->getConf()); + /** + * [signal] + * + * IDF_Upload::create + * + * [sender] + * + * IDF_Form_Upload + * + * [description] + * + * This signal allows an application to perform a set of tasks + * just after the upload of a file and after the notification run. + * + * [parameters] + * + * array('upload' => $upload); + * + */ + $params = array('upload' => $upload); + Pluf_Signal::send('IDF_Upload::create', 'IDF_Form_Upload', + $params); + return $upload; + } +} + diff --git a/src/IDF/Views.php b/src/IDF/Views.php index 70bbdc3..65f016f 100644 --- a/src/IDF/Views.php +++ b/src/IDF/Views.php @@ -293,6 +293,22 @@ class IDF_Views } /** + * Download archive FAQ. + */ + public function faqArchiveFormat($request, $match) + { + $title = __('InDefero Upload Archive Format'); + $projects = self::getProjects($request->user); + return Pluf_Shortcuts_RenderToResponse('idf/faq-archive-format.html', + array( + 'page_title' => $title, + 'projects' => $projects, + ), + $request); + + } + + /** * API FAQ. */ public function faqApi($request, $match) diff --git a/src/IDF/Views/Download.php b/src/IDF/Views/Download.php index 5fb49e8..1444629 100644 --- a/src/IDF/Views/Download.php +++ b/src/IDF/Views/Download.php @@ -224,11 +224,11 @@ class IDF_Views_Download } /** - * Submit a new file for download. + * Create a new file for download. */ - public $submit_precond = array('IDF_Precondition::accessDownloads', + public $create_precond = array('IDF_Precondition::accessDownloads', 'IDF_Precondition::projectMemberOrOwner'); - public function submit($request, $match) + public function create($request, $match) { $prj = $request->project; $title = __('New Download'); @@ -250,7 +250,7 @@ class IDF_Views_Download array('project' => $prj, 'user' => $request->user)); } - return Pluf_Shortcuts_RenderToResponse('idf/downloads/submit.html', + return Pluf_Shortcuts_RenderToResponse('idf/downloads/create.html', array( 'auto_labels' => self::autoCompleteArrays($prj), 'page_title' => $title, @@ -260,6 +260,39 @@ class IDF_Views_Download } /** + * Create new downloads from an uploaded archive. + */ + public $createFromArchive_precond = array('IDF_Precondition::accessDownloads', + 'IDF_Precondition::projectMemberOrOwner'); + public function createFromArchive($request, $match) + { + $prj = $request->project; + $title = __('New Downloads from Archive'); + if ($request->method == 'POST') { + $form = new IDF_Form_UploadArchive(array_merge($request->POST, $request->FILES), + array('project' => $prj, + 'user' => $request->user)); + if ($form->isValid()) { + $upload = $form->save(); + $request->user->setMessage(__('The archive has been uploaded and processed.')); + $url = Pluf_HTTP_URL_urlForView('IDF_Views_Download::index', + array($prj->shortname)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new IDF_Form_UploadArchive(null, + array('project' => $prj, + 'user' => $request->user)); + } + return Pluf_Shortcuts_RenderToResponse('idf/downloads/createFromArchive.html', + array( + 'page_title' => $title, + 'form' => $form, + ), + $request); + } + + /** * Create the autocomplete arrays for the little AJAX stuff. */ public static function autoCompleteArrays($project) diff --git a/src/IDF/conf/idf.php-dist b/src/IDF/conf/idf.php-dist index 62fe1ce..d3293fe 100644 --- a/src/IDF/conf/idf.php-dist +++ b/src/IDF/conf/idf.php-dist @@ -495,6 +495,11 @@ $cfg['idf_strong_key_check'] = false; # always have precedence. # $cfg['max_upload_size'] = 2097152; // Size in bytes +# If a download archive is uploaded, the size of the archive is limited to 20MB. +# The php.ini upload_max_filesize and post_max_size configuration setting will +# always have precedence. +# $cfg['max_upload_archive_size'] = 20971520; // Size in bytes + # Older versions of Indefero submitted a POST request to a configured # post-commit web hook when new revisions arrived, whereas a PUT request # would have been more appropriate. Also, the payload's HMAC digest was diff --git a/src/IDF/conf/urls.php b/src/IDF/conf/urls.php index 3e2288b..8376683 100644 --- a/src/IDF/conf/urls.php +++ b/src/IDF/conf/urls.php @@ -304,6 +304,11 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/page/(.*)/$#', // ---------- Downloads ------------------------------------ +$ctl[] = array('regex' => '#^/help/archive-format/$#', + 'base' => $base, + 'model' => 'IDF_Views', + 'method' => 'faqArchiveFormat'); + $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/$#', 'base' => $base, 'model' => 'IDF_Views_Download', @@ -332,7 +337,12 @@ $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/get/$#', $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/create/$#', 'base' => $base, 'model' => 'IDF_Views_Download', - 'method' => 'submit'); + 'method' => 'create'); + +$ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/create/archive/$#', + 'base' => $base, + 'model' => 'IDF_Views_Download', + 'method' => 'createFromArchive'); $ctl[] = array('regex' => '#^/p/([\-\w]+)/downloads/(\d+)/delete/$#', 'base' => $base, diff --git a/src/IDF/templates/idf/downloads/base.html b/src/IDF/templates/idf/downloads/base.html index c7e046a..248bf73 100644 --- a/src/IDF/templates/idf/downloads/base.html +++ b/src/IDF/templates/idf/downloads/base.html @@ -2,6 +2,10 @@ {block tabdownloads} class="active"{/block} {block subtabs}
{blocktrans}Each file must have a distinct name and file contents +cannot be changed, so be sure to include release numbers in each file +name.{/blocktrans}
+{assign $url = 'http://daringfireball.net/projects/markdown/syntax'} +{blocktrans}You can use the Markdown syntax for the description.{/blocktrans}
+{blocktrans}The archive must include a manifest.xml
file with meta information about the
+files to process inside the archive. All processed files must be unique or replace existing files explicitely.{/blocktrans}
{blocktrans}You can learn more about the archive format here.{/blocktrans}
+{blocktrans}Each file must have a distinct name and file contents -cannot be changed, so be sure to include release numbers in each file -name.{/blocktrans}
-{assign $url = 'http://daringfireball.net/projects/markdown/syntax'} -{blocktrans}You can use the Markdown syntax for the description.{/blocktrans}
-At the moment, this documentation is only available in English.
+ + + ++Adding multiple, individual downloads to a project for a release can be a tedious task if +one has to select each file manually, and then has to fill in the summary and correct labels +for each of these downloads individually. +
+ ++InDefero therefor supports the upload of "archives" that contain multiple downloadable +files. These archives are standard PKZIP files with only one special property - they +contain an additional manifest file which describes the files that should be published. +
+ ++Once such an archive has been uploaded and validated by InDefero, its files are extracted +and individual downloads are created for each of them. If the archive contains files +that should deprecate existing downloads, then InDefero takes care of this as well - +automatically. +
+ ++An archive file and its manifest file can easily be compiled, either by hand with the help +of a text editor, or through an automated build system with the help of your build tool of +choice, such as Apache Ant. +
+ ++The manifest is an XML file that follows a simple syntax. As it is always easier to look +at an example, here you have one: +
+ ++<?xml version="1.0" encoding="UTF-8" ?> +<manifest> + <file> + <name>foo-1.2.tar.gz</name> + <summary>Tarball</summary> + <replaces>foo-1.1.tar.gz</replaces> + <tags> + <tag>Type:Archive</tag> + </tags> + </file> + <file> + <name>foo-1.2-installer.exe</name> + <summary>Windows MSI Installer</summary> + <description>This installer needs Windows XP SP2 or later.</description> + <tags> + <tag>Type:Installer</tag> + <tag>OpSys:Windows</tag> + </tags> + </file> +</manifest> ++ +
+This is the DTD for the format: +
+ ++<!DOCTYPE manifest [ +<!ELEMENT manifest (file+)> +<!ELEMENT file (name,summary,replaces?,description?,tags?)> +<!ELEMENT name (#PCDATA)> +<!ELEMENT summary (#PCDATA)> +<!ELEMENT replaces (#PCDATA)> +<!ELEMENT description (#PCDATA)> +<!ELEMENT tags (tag+)> +<!ELEMENT tag (#PCDATA)> +]> ++ +
+The format is more or less self-explaining, all fields map to properties of a single download.
+One special element has been introduced though, replaces
. If this optional element
+is given, InDefero looks for a file with that name in the project and deprecates it by attaching
+the label Other:Deprecated
to it. If no such file is found, the element is simply
+ignored.
+
{trans 'Here we are, just to help you.'}
+{blocktrans}You need to create an account on Gravatar, this takes about 5 minutes and is free.{/blocktrans}
+ +If you have to publish many files at once for a new release, it is a very tedious task +to upload them one after another and enter meta information like a summary, a description or additional +labels for each of them.
+InDefero therefor supports a special archive format that is basically a standard zip file which comes with +some meta information. These meta information are kept in a special manifest file, which is distinctly kept from +the rest of the files in the archive that should be published.
+Once this archive has been uploaded, InDefero reads in the meta information, unpacks the other files from +the archive and creates new individual downloads for each of them.
{/blocktrans} + +{aurl 'url', 'IDF_Views::faqArchiveFormat'} +{blocktrans}Learn more about the archive format.{/blocktrans}
+{blocktrans}The API (Application Programming Interface) is used to interact with InDefero with another program. For example, this can be used to create a desktop program to submit new tickets easily.{/blocktrans}
{aurl 'url', 'IDF_Views::faqApi'} +{blocktrans}The API (Application Programming Interface) is used to interact with InDefero with another program. For example, this can be used to create a desktop program to submit new tickets easily.{/blocktrans}
+{aurl 'url', 'IDF_Views::faqApi'}{blocktrans}Learn more about the API.{/blocktrans}
- + {/block} {block context}{trans 'Here we are, just to help you.'}