[Fusionforge-commits] r15660 - in trunk/src: . common/docman common/docman/actions common/docman/views common/include db

Franck VILLAUME nerville at fusionforge.org
Sun Jun 3 17:51:50 CEST 2012


Author: nerville
Date: 2012-06-03 17:51:50 +0200 (Sun, 03 Jun 2012)
New Revision: 15660

Added:
   trunk/src/common/docman/DocumentStorage.class.php
   trunk/src/db/20120603-docman-file-moved-in-fs.php
Modified:
   trunk/src/CHANGES
   trunk/src/common/docman/Document.class.php
   trunk/src/common/docman/DocumentFactory.class.php
   trunk/src/common/docman/DocumentManager.class.php
   trunk/src/common/docman/actions/addfile.php
   trunk/src/common/docman/actions/editfile.php
   trunk/src/common/docman/views/editfile.php
   trunk/src/common/include/Group.class.php
   trunk/src/common/include/Storage.class.php
   trunk/src/db/20120409-tracker-attachement-moved-in-fs.php
Log:
docman: move files to fs using the storage generic class

Modified: trunk/src/CHANGES
===================================================================
--- trunk/src/CHANGES	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/CHANGES	2012-06-03 15:51:50 UTC (rev 15660)
@@ -6,6 +6,7 @@
 * headermenu: new plugin to add links in login/logout menu link (TrivialDev)
 * scmgit: basic activity support (TrivialDev).
 * scmhg: merge patch from Denise Patzker: add http support, online browse, stats (TrivialDev)
+* Docman: Files moves to filesystem using the Storage generic class (TrivialDev)
 
 FusionForge-5.2:
 * Docman: inject zip as a tree (Capgemini)

Modified: trunk/src/common/docman/Document.class.php
===================================================================
--- trunk/src/common/docman/Document.class.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/Document.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -30,6 +30,7 @@
 require_once $gfcommon.'docman/Parsedata.class.php';
 require_once $gfcommon.'docman/DocumentManager.class.php';
 require_once $gfcommon.'docman/DocumentGroup.class.php';
+require_once $gfcommon.'docman/DocumentStorage.class.php';
 
 
 class Document extends Error {
@@ -159,52 +160,38 @@
 		} else {
 			$kwords ='';
 		}
-
 		$filesize = strlen($data);
 
 		db_begin();
-		$result = db_query_params('INSERT INTO doc_data (group_id,title,description,createdate,doc_group,
-						stateid,filename,filetype,filesize,data_words,created_by)
-						VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)',
+		$result = db_query_params('INSERT INTO doc_data (group_id, title, description, createdate, doc_group,
+						stateid, filename, filetype, filesize, data_words, created_by)
+						VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)',
 						array($this->Group->getId(),
-						htmlspecialchars($title),
-						htmlspecialchars($description),
-						time(),
-						$doc_group,
-						$doc_initstatus,
-						$filename,
-						$filetype,
-						$filesize,
-						$kwords,
-						$user_id));
-		if (!$result) {
+							htmlspecialchars($title),
+							htmlspecialchars($description),
+							time(),
+							$doc_group,
+							$doc_initstatus,
+							$filename,
+							$filetype,
+							$filesize,
+							$kwords,
+							$user_id)
+					);
+
+		$docid = db_insertid($result, 'doc_data', 'docid');
+		DocumentStorage::instance()->store($docid, $data);
+
+		if (!$result || !$docid) {
 			$this->setError(_('Error Adding Document:').' '.db_error().$result);
+			DocumentStorage::instance()->rollback();
 			db_rollback();
 			return false;
 		}
 
-		$docid = db_insertid($result,'doc_data','docid');
-
-		switch ($this->Group->getStorageAPI()) {
-			case 'DB': {
-				$result = db_query_params('UPDATE doc_data set data = $1 where docid = $2',
-								array(base64_encode($data),$docid));
-				if (!$result) {
-					$this->setError(_('Error Adding Document:').' '.db_error().$result);
-					db_rollback();
-					return false;
-				}
-				break;
-			}
-			default: {
-				$this->setError(_('Error Adding Document: No Storage API'));
-				db_rollback();
-				return false;
-			}
-		}
-
 		if (!$this->fetchData($docid)) {
 			$this->setError(_('Error fetching Document'));
+			DocumentStorage::instance()->rollback();
 			db_rollback();
 			return false;
 		}
@@ -212,11 +199,13 @@
 		$localDg = new DocumentGroup($this->Group, $doc_group);
 		if (!$localDg->update($localDg->getName(), $localDg->getParentID(), 1)) {
 			$this->setError(_('Error updating document group:').$localDg->getErrorMessage());
+			DocumentStorage::instance()->rollback();
 			db_rollback();
 			return false;
 		}
 		$this->sendNotice(true);
 		db_commit();
+		DocumentStorage::instance()->commit();
 		return true;
 	}
 
@@ -414,11 +403,7 @@
 	 * @return	string	The filedata.
 	 */
 	function getFileData() {
-		//
-		//	Because this could be a large string, we only fetch if we actually need it
-		//
-		$res = db_query_params('SELECT data FROM doc_data WHERE docid=$1', array($this->getID()));
-		return base64_decode(db_result($res, 0, 'data'));
+		return file_get_contents(DocumentStorage::instance()->get($this->getID()));
 	}
 
 	/**
@@ -857,25 +842,10 @@
 				return false;
 			}
 
-			switch ($this->Group->getStorageAPI()) {
-				case 'DB': {
-					$res = db_query_params('UPDATE doc_data SET data = $1 where group_id = $2 and docid = $3',
-								array(base64_encode($data),
-									$this->Group->getID(),
-									$this->getID())
-								);
+			DocumentStorage::instance()->delete($this->getID())->commit();
+			DocumentStorage::instance()->store($this->getID(), $data);
 
-					if (!$res || db_affected_rows($res) < 1) {
-						$this->setOnUpdateError(db_error());
-						return false;
-					}
-					break;
-				}
-				default: {
-					$this->setOnUpdateError(_('No Storage API'));
-					return false;
-				}
-			}
+
 		}
 
 		$this->sendNotice(false);
@@ -938,16 +908,7 @@
 			return false;
 		}
 
-		switch ($this->Group->getStorageAPI()) {
-			case 'DB': {
-				break;
-			}
-			default: {
-				$this->setError(_('Error Deleting Document: No Storage API'));
-				db_rollback();
-				return false;
-			}
-		}
+		DocumentStorage::instance()->delete($this->getID())->commit();
 
 		/** we should be able to send a notice that this doc has been deleted .... but we need to rewrite sendNotice
 		 * $this->sendNotice(false);

Modified: trunk/src/common/docman/DocumentFactory.class.php
===================================================================
--- trunk/src/common/docman/DocumentFactory.class.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/DocumentFactory.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -7,6 +7,7 @@
  * Copyright 2009, Roland Mas
  * Copyright 2010-2011, Franck Villaume - Capgemini
  * Copyright (C) 2012 Alain Peyrat - Alcatel-Lucent
+ * Copyright 2012, Franck Villaume - TrivialDev
  * http://fusionforge.org
  *
  * This file is part of FusionForge. FusionForge is free software;
@@ -319,35 +320,13 @@
 	}
 
 	/**
-	 * __getFromStorage - Retrieve documents from storage API
-	 *
-	 * @return	boolean	success or not
-	 * @access	private
-	 */
-	private function __getFromStorage() {
-		$returned = false;
-		switch ($this->Group->getStorageAPI()) {
-			case 'DB': {
-				if ($this->__getFromDB())
-					$returned = true;
-				break;
-			}
-			default: {
-				$this->setError(_('No Storage API Found'));
-				break;
-			}
-		}
-		return $returned;
-	}
-
-	/**
-	 * __getFromDB - Retrieve documents from database.
+	 * __getFromStorage - Retrieve documents from storage (database for all informations).
 	 * you can limit query to speed up: warning, once $this->documents is retrieve, it's cached.
 	 *
 	 * @return	boolean	success or not
 	 * @access	private
 	 */
-	private function __getFromDB() {
+	private function __getFromStorage() {
 		$this->Documents = array();
 		$qpa = db_construct_qpa();
 		$qpa = db_construct_qpa($qpa, 'SELECT * FROM docdata_vw WHERE group_id = $1 ',
@@ -375,7 +354,7 @@
 
 		$result = db_query_qpa($qpa);
 		if (!$result) {
-			$this->setError('getFromDB::'.db_error());
+			$this->setError('__getFromStorage::'.db_error());
 			return false;
 		}
 

Modified: trunk/src/common/docman/DocumentManager.class.php
===================================================================
--- trunk/src/common/docman/DocumentManager.class.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/DocumentManager.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -107,8 +107,9 @@
 		$trashId = $this->getTrashID();
 		if ($trashId !== -1) {
 			db_begin();
+			$result = db_query_params('select docid FROM doc_data WHERE stateid=$1 and group_id=$2', array('2', $this->Group->getID()));
 			$emptyFile = db_query_params('DELETE FROM doc_data WHERE stateid=$1 and group_id=$2', array('2', $this->Group->getID()));
-			if (!$emptyFile)	{
+			if (!$emptyFile) {
 				db_rollback();
 				return false;
 			}
@@ -117,6 +118,9 @@
 				db_rollback();
 				return false;
 			}
+			while ($arr = db_fetch_array($result)) {
+				DocumentStorage::instance()->delete($arr['docid'])->commit();
+			}
 			db_commit();
 			return true;
 		}

Added: trunk/src/common/docman/DocumentStorage.class.php
===================================================================
--- trunk/src/common/docman/DocumentStorage.class.php	                        (rev 0)
+++ trunk/src/common/docman/DocumentStorage.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -0,0 +1,60 @@
+<?php
+/**
+ * FusionForge Document Storage Class
+ *
+ * Copyright (C) 2011 Alain Peyrat - Alcatel-Lucent
+ * Copyright 2012, Franck Villaume - TrivialDev
+ *
+ * This file is part of FusionForge. FusionForge is free software;
+ * you can redistribute it and/or modify it under the terms of the
+ * GNU General Public License as published by the Free Software
+ * Foundation; either version 2 of the Licence, or (at your option)
+ * any later version.
+ *
+ * FusionForge 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with FusionForge; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/*
+ * Standard Alcatel-Lucent disclaimer for contributing to open source
+ *
+ * "The Artifact ("Contribution") has not been tested and/or
+ * validated for release as or in products, combinations with products or
+ * other commercial use. Any use of the Contribution is entirely made at
+ * the user's own responsibility and the user can not rely on any features,
+ * functionalities or performances Alcatel-Lucent has attributed to the
+ * Contribution.
+ *
+ * THE CONTRIBUTION BY ALCATEL-LUCENT IS PROVIDED AS IS, WITHOUT WARRANTY
+ * OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+ * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, COMPLIANCE,
+ * NON-INTERFERENCE AND/OR INTERWORKING WITH THE SOFTWARE TO WHICH THE
+ * CONTRIBUTION HAS BEEN MADE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+ * ALCATEL-LUCENT BE LIABLE FOR ANY DAMAGES OR OTHER LIABLITY, WHETHER IN
+ * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * CONTRIBUTION OR THE USE OR OTHER DEALINGS IN THE CONTRIBUTION, WHETHER
+ * TOGETHER WITH THE SOFTWARE TO WHICH THE CONTRIBUTION RELATES OR ON A STAND
+ * ALONE BASIS."
+ */
+
+require_once $gfcommon.'include/Storage.class.php';
+
+class DocumentStorage extends Storage {
+    public static function instance() {
+        if (!isset(self::$_instance)) {
+            $c = __CLASS__;
+            self::$_instance = new $c;
+        }
+        return self::$_instance;
+    }
+
+	function get_storage_path() {
+		return forge_get_config('data_path').'/docman';
+	}
+}

Modified: trunk/src/common/docman/actions/addfile.php
===================================================================
--- trunk/src/common/docman/actions/addfile.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/actions/addfile.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -96,14 +96,18 @@
 
 switch ($type) {
 	case 'editor' : {
-		$data = getStringFromRequest('details');
+		$filecontent = getStringFromRequest('details');
 		$uploaded_data_name = $name;
 		$sanitizer = new TextSanitizer();
-		$data = $sanitizer->SanitizeHtml($data);
-		if (strlen($data)<1) {
+		$filecontent = $sanitizer->SanitizeHtml($filecontent);
+		if (strlen($filecontent) < 1) {
 			$return_msg = _('Error getting blank document.');
 			session_redirect($baseurl.'&error_msg='.urlencode($return_msg));
 		}
+		$data = tempnam("/tmp", "docman");
+		$fh = fopen($data, 'w');
+		fwrite($fh, $filecontent);
+		fclose($fh);
 		$uploaded_data_type = 'text/html';
 		break;
 	}
@@ -124,7 +128,7 @@
 		} else {
 			$uploaded_data_type = $uploaded_data['type'];
 		}
-		$data = fread(fopen($uploaded_data['tmp_name'], 'r'), $uploaded_data['size']);
+		$data = $uploaded_data['tmp_name'];
 		$file_url = '';
 		$uploaded_data_name = $uploaded_data['name'];
 		break;
@@ -149,8 +153,7 @@
 		} else {
 			$uploaded_data_type = 'application/binary';
 		}
-		$stat = stat($filename);
-		$data = fread(fopen($filename, 'r'), $stat['size']);
+		$data = $filename;
 		$file_url = '';
 		$uploaded_data_name = $manual_path;
 		break;
@@ -173,7 +176,7 @@
 		setcookie("gforgecurrentdocdata", "", time() - 3600);
 	}
 	if (forge_check_perm('docman', $group_id, 'approve')) {
-		$return_msg = sprintf(_('Document %s submitted successfully.'),$d->getFilename());
+		$return_msg = sprintf(_('Document %s submitted successfully.'), $d->getFilename());
 		session_redirect($redirecturl.'&feedback='.urlencode($return_msg));
 	} else {
 		$return_msg = sprintf(_('Document %s has been successfully uploaded and is waiting to be approved.'),$d->getFilename());

Modified: trunk/src/common/docman/actions/editfile.php
===================================================================
--- trunk/src/common/docman/actions/editfile.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/actions/editfile.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -58,7 +58,7 @@
 $docid = getIntFromRequest('docid');
 $title = getStringFromRequest('title');
 $description = getStringFromRequest('description');
-$data = getStringFromRequest('details');
+$details = getStringFromRequest('details');
 $file_url = getStringFromRequest('file_url');
 $uploaded_data = getUploadedFile('uploaded_data');
 $stateid = getIntFromRequest('stateid');
@@ -73,19 +73,23 @@
 	session_redirect($urlparam.'&error_msg='.urlencode($d->getErrorMessage()));
 
 $sanitizer = new TextSanitizer();
-$data = $sanitizer->SanitizeHtml($data);
-if (($editor) && ($d->getFileData()!=$data) && (!$uploaded_data['name'])) {
+$details = $sanitizer->SanitizeHtml($details);
+if (($editor) && ($d->getFileData() != $details) && (!$uploaded_data['name'])) {
 	$filename = $d->getFileName();
+	$datafile = tempnam("/tmp", "docman");
+	$fh = fopen($datafile, 'w');
+	fwrite($fh, $details);
+	fclose($fh);
+	$data = $datafile;
 	if (!$filetype)
 		$filetype = $d->getFileType();
 
 } elseif (!empty($uploaded_data) && $uploaded_data['name']) {
-	var_dump($uploaded_data);
 	if (!is_uploaded_file($uploaded_data['tmp_name'])) {
 		$return_msg = sprintf(_('Invalid file attack attempt %1$s.'), $uploaded_data['name']);
 		session_redirect($urlparam.'&error_msg='.urlencode($return_msg));
 	}
-	$data = fread(fopen($uploaded_data['tmp_name'], 'r'), $uploaded_data['size']);
+	$data = $uploaded_data['tmp_name'];
 	$filename = $uploaded_data['name'];
 	if (function_exists('finfo_open')) {
 		$finfo = finfo_open(FILEINFO_MIME_TYPE);
@@ -101,6 +105,7 @@
 	$filename = $d->getFileName();
 	$filetype = $d->getFileType();
 }
+
 if (!$d->update($filename, $filetype, $data, $doc_group, $title, $description, $stateid))
 	session_redirect($urlparam.'&error_msg='.urlencode($d->getErrorMessage()));
 

Modified: trunk/src/common/docman/views/editfile.php
===================================================================
--- trunk/src/common/docman/views/editfile.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/docman/views/editfile.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -57,6 +57,7 @@
 	echo '		<td>'. _('Edit the contents to your desire or leave them as they are to remain unmodified.') .'<br />';
 	echo '			<textarea id="defaulteditzone" name="details" rows="15" cols="70"></textarea><br />';
 	echo '			<input id="defaulteditfiletype" type="hidden" name="filetype" value="text/plain" />';
+	echo '			<input type="hidden" name="editor" value="online" />';
 	echo '		</td>';
 	echo '	</tr>';
 }

Modified: trunk/src/common/include/Group.class.php
===================================================================
--- trunk/src/common/include/Group.class.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/include/Group.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -2848,13 +2848,6 @@
 		}
 	}
 
-	function setStorageAPI($type) {
-		return true;
-	}
-
-	function getStorageAPI() {
-		return 'DB';
-	}
 }
 
 /**

Modified: trunk/src/common/include/Storage.class.php
===================================================================
--- trunk/src/common/include/Storage.class.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/common/include/Storage.class.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -47,7 +47,7 @@
 	var $pending_store = array();
 	var $pending_delete = array();
 
-	function store ($key, $file) {
+	function store($key, $file) {
 		$storage = $this->get_storage($key);
 		$dir     = dirname($storage);
 		if (!is_dir($dir)) {
@@ -102,9 +102,9 @@
 
 	function get_storage($key) {
 		$key = dechex($key);
-		$pre = substr( $key, strlen($key)-2);
-		$last = substr( $key, 0, strlen($key)-2);
-		if (!$last) $last='0';
+		$pre = substr($key, strlen($key)-2);
+		$last = substr($key, 0, strlen($key)-2);
+		if (!$last) $last = '0';
 		return $this->get_storage_path().'/'.$pre.'/'.$last;
 	}
 }

Modified: trunk/src/db/20120409-tracker-attachement-moved-in-fs.php
===================================================================
--- trunk/src/db/20120409-tracker-attachement-moved-in-fs.php	2012-06-03 15:10:29 UTC (rev 15659)
+++ trunk/src/db/20120409-tracker-attachement-moved-in-fs.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -1,9 +1,8 @@
 #! /usr/bin/php
 <?php
 /**
- * GForge Group Role Generator
  *
- * Copyright 2004 GForge, LLC
+ * Copyright 2012, Alain Peyrat
  * http://fusionforge.org/
  *
  * This file is part of FusionForge.
@@ -30,7 +29,7 @@
 ini_set('memory_limit', -1);
 ini_set('max_execution_time', 0);
 
-$res = db_query_params ('SELECT id FROM artifact_file WHERE bin_data !=$1', array('')) ;
+$res = db_query_params('SELECT id FROM artifact_file WHERE bin_data !=$1', array(''));
 if (!$res) {
 	echo 'UPGRADE ERROR: '.db_error();
 	exit(1);

Added: trunk/src/db/20120603-docman-file-moved-in-fs.php
===================================================================
--- trunk/src/db/20120603-docman-file-moved-in-fs.php	                        (rev 0)
+++ trunk/src/db/20120603-docman-file-moved-in-fs.php	2012-06-03 15:51:50 UTC (rev 15660)
@@ -0,0 +1,78 @@
+#! /usr/bin/php
+<?php
+/**
+ *
+ * Copyright 2012, Alain Peyrat
+ * Copyright 2012, Franck Villaume - TrivialDev
+ * http://fusionforge.org/
+ *
+ * This file is part of FusionForge.
+ *
+ * FusionForge is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * FusionForge 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+require_once dirname(__FILE__).'/../common/include/env.inc.php';
+require_once $gfcommon.'include/pre.php';
+require_once $gfcommon.'docman/DocumentStorage.class.php';
+
+ini_set('memory_limit', -1);
+ini_set('max_execution_time', 0);
+
+$res = db_query_params('SELECT docid FROM doc_data WHERE data !=$1', array(''));
+if (!$res) {
+	echo 'UPGRADE ERROR: '.db_error();
+	exit(1);
+}
+
+$data = forge_get_config('data_path');
+if (!is_dir($data)) {
+	system("mkdir -p $data");
+	system("chown ".forge_get_config('apache_user').':'.forge_get_config('apache_group')." $data");
+	system("chmod 0700 $data");
+}
+
+$ds = new DocumentStorage();
+$tmp = tempnam('/tmp', 'docman');
+
+while($row = db_fetch_array($res)) {
+	$res2 = db_query_params ('SELECT filesize, data FROM doc_data WHERE docid=$1', 
+		array($row['docid'])) ;
+	$row2 = db_fetch_array($res2);
+	$ret = file_put_contents($tmp, base64_decode($row2['data']));
+	if ($ret === false) {
+		echo "UPGRADE ERROR: file_put_contents($tmp) error: returned false\n";
+		$ds->rollback();
+		exit(1);
+	}
+	if ($ret != $row2['filesize']) {
+		echo "UPGRADE ERROR: file_put_contents($tmp) size error: ($ret != ".$row2['filesize'].")\n";
+		$ds->rollback();
+		exit(1);
+	}
+	$ret = $ds->store($row['docid'], $tmp);
+	if (!$ret) {
+		echo "UPGRADE ERROR: $ret: ".$as->getErrorMessage()."\n";
+		$ds->rollback();
+		exit(1);
+	}
+}
+
+$ds->commit();
+
+db_query_params ('UPDATE doc_data SET data=$1', array(''));
+
+echo "SUCCESS\n";
+
+?> 




More information about the Fusionforge-commits mailing list