Index: trac/attachment.py
===================================================================
--- trac/attachment.py	(revision 6165)
+++ trac/attachment.py	(working copy)
@@ -505,14 +505,10 @@
 
     # Internal methods
 
-    def _do_save(self, req, attachment):
+    def _do_save_one(self, req, attachment, upload, description):
         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
 
-        if 'cancel' in req.args:
-            req.redirect(get_resource_url(self.env, attachment.resource.parent,
-                                          req.href))
 
-        upload = req.args['attachment']
         if not hasattr(upload, 'filename') or not upload.filename:
             raise TracError(_('No file uploaded'))
         if hasattr(upload.file, 'fileno'):
@@ -540,7 +536,7 @@
             raise TracError(_('No file uploaded'))
         # Now the filename is known, update the attachment resource
         # attachment.filename = filename
-        attachment.description = req.args.get('description', '')
+        attachment.description = description
         attachment.author = get_reporter_id(req, 'author')
         attachment.ipnr = req.remote_addr
 
@@ -569,8 +565,31 @@
             attachment.filename = None
         attachment.insert(filename, upload.file, size)
 
-        req.redirect(get_resource_url(self.env, attachment.resource(id=None),
+    def _do_save_many(self,req, attachment):
+	curr_attach = 0
+	while curr_attach < int(req.args['multiple_attachments']):
+	    upload=req.args['attachment-%d' % curr_attach]
+	    if hasattr(upload, 'filename') and upload.filename:
+		attachment.filename = None
+		self._do_save_one(req, attachment, upload, req.args.get('description-%d' % curr_attach, ''))
+	    curr_attach=curr_attach+1
+
+    def _do_save(self, req, attachment):
+        req.perm(attachment.resource).require('ATTACHMENT_CREATE')
+
+        if 'cancel' in req.args:
+            req.redirect(get_resource_url(self.env, attachment.resource.parent,
+                                          req.href))
+
+	if 'multiple_attachments' in req.args:
+	    self._do_save_many(req, attachment)
+	    req.redirect(get_resource_url(self.env, attachment.resource(id=None),
                                       req.href))
+	else:
+	    self._do_save_one(req, attachment, req.args['attachment'], req.args.get('description', ''))
+	    # Redirect the user to list of attachments (must add a trailing '/')
+	    req.redirect(get_resource_url(self.env, attachment.resource(id=None),
+                                      req.href))
 
     def _do_delete(self, req, attachment):
         req.perm(attachment.resource).require('ATTACHMENT_DELETE')
Index: trac/htdocs/js/trac.js
===================================================================
--- trac/htdocs/js/trac.js	(revision 6165)
+++ trac/htdocs/js/trac.js	(working copy)
@@ -69,3 +69,35 @@
 function getAncestorByTagName(elem, tagName) {
   return $(elem).parents(tagName).get(0);
 }
+var ATTACHFILE_COUNTER=0;
+function manageMultipleAttachFields(_obj){
+    if (_obj.value == '') {
+        if($("#multiAttach_tbody").get(0).rows.length == 1) return;
+	$("#multiAttach_tbody").get(0).deleteRow($(_obj).attr("attachnum")*1);
+	ATTACHFILE_COUNTER=0;
+	$("#multiAttach_tbody tr").each(function(index, element){
+	    $("input", $("td", element).get(0))
+		.attr("attachnum",ATTACHFILE_COUNTER)
+		.get(0).name = "attachment-"+ATTACHFILE_COUNTER;
+	    $("input", $("td", element).get(1))
+		.get(0).name = "description-"+ATTACHFILE_COUNTER;
+	    ATTACHFILE_COUNTER++;
+	});
+	$("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1-1;
+    } else {
+	if ($(_obj).attr("attachnum") != $("#multiAttach_count").val()-1) return;
+	var tr = $("#multiAttach_tbody").get(0).insertRow(-1);
+	var td = tr.insertCell(-1);
+	$('<input type="file" onchange="manageMultipleAttachFields(this)">')
+	   .attr("attachnum",$("#multiAttach_count").val())
+	   .appendTo(td)
+	   .get(0).name = "attachment-"+$("#multiAttach_count").val();
+
+	td = tr.insertCell(-1);
+	$('<input type="text">')
+	    .attr("size",60)
+	    .appendTo(td)
+	    .get(0).name = "description-"+$("#multiAttach_count").val();
+	$("#multiAttach_count").get(0).value = $("#multiAttach_count").val()*1+1;
+    }
+}
Index: trac/templates/attachment.html
===================================================================
--- trac/templates/attachment.html	(revision 6165)
+++ trac/templates/attachment.html	(working copy)
@@ -13,19 +13,7 @@
   <body py:with="parent = attachments and attachments.parent or
                           attachment.resource.parent">
 
-    <div id="ctxtnav" class="nav">
-      <h2>Attachment Navigation</h2>
-      <ul>
-        <li class="last">
-          <a href="${chrome.links.up[0].href}">${_("Back to %(parent)s", parent=name_of(parent))}</a>
-        </li>
-      </ul>
-    </div>
-
-    <div py:choose="mode" id="content" class="attachment">
-      <py:when test="'new'">
-        <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
-        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+    <py:def function="attach_oneFile()">
           <div class="field">
             <label>File:<br /><input type="file" name="attachment" /></label>
           </div>
@@ -49,6 +37,53 @@
             </div>
             <br />
           </fieldset>
+    
+    </py:def>
+    <py:def function="attach_multiFile()">
+	 <table><thead>
+          <tr><th align="left">File</th><th align="left">Description</th></tr>
+	 </thead>
+	<tbody id="multiAttach_tbody">
+    	<tr>
+    	    <td><input type="file" attachnum="0" name="attachment-0" onchange="manageMultipleAttachFields(this);"/></td>
+    	    <td><input type="text" name="description-0" size="60" /></td>
+    	</tr>
+    	</tbody></table>
+        <input type="hidden" id="multiAttach_count" name="multiple_attachments" value="1" />
+
+          <fieldset>
+            <legend>Attachment Info</legend>
+            <py:if test="authname == 'anonymous'">
+              <div class="field">
+                <label>Your email or username:<br />
+                  <input type="text" name="author" size="30" value="$author" />
+                </label>
+              </div>
+              </py:if>
+            <div class="options">
+              <label><input type="checkbox" name="replace" />
+                Replace existing attachments of the same name</label>
+            </div>
+            <br />
+          </fieldset>
+    
+    </py:def>
+
+    <div id="ctxtnav" class="nav">
+      <h2>Attachment Navigation</h2>
+      <ul>
+        <li class="last">
+          <a href="${chrome.links.up[0].href}">${_("Back to %(parent)s", parent=name_of(parent))}</a>
+        </li>
+      </ul>
+    </div>
+
+    <div py:choose="mode" id="content" class="attachment">
+      <py:when test="'new'">
+        <h1>Add Attachment to <a href="${url_of(parent)}">${name_of(parent)}</a></h1>
+        <form id="attachment" method="post" enctype="multipart/form-data" action="">
+        
+        ${attach_multiFile()}
           <div class="buttons">
             <input type="hidden" name="action" value="new" />
             <input type="hidden" name="realm" value="$parent.realm" />

