=== templates/attachment.cs
==================================================================
--- templates/attachment.cs  (revision 1147)
+++ templates/attachment.cs  (local)
@@ -14,6 +14,10 @@
    <label for="file">File:</label>
    <input type="file" id="file" name="attachment" />
   </div>
+  <div class="field">
+   <input type="checkbox" id="replace" name="replace" checked="1" />
+   <label for="replace">Replace Existing File</label>
+  </div>
   <fieldset>
    <legend>Attachment Info</legend>
    <div class="field">
=== templates/file.cs
==================================================================
--- templates/file.cs  (revision 1147)
+++ templates/file.cs  (local)
@@ -13,6 +13,15 @@
  <?cs if file.attachment_parent ?>
   <h1><a href="<?cs var:file.attachment_parent_href ?>"><?cs
     var:file.attachment_parent ?></a>: <?cs var:file.filename ?></h1>
+  <table id="info" summary="Attachment info">
+   <tr>
+    <th scope="row">
+     File <a href="<?cs var:attachment.href ?>"><?cs var:attachment.name ?></a>, <?cs var:attachment.size ?>
+     (attached by <?cs var:attachment.author ?>, <?cs var:attachment.time ?>)
+    </th>
+    <td class="message"><?cs var:attachment.descr ?></td>
+   </tr>
+  </table>
 
  <?cs else ?>
   <?cs call:browser_path_links(file.path, file) ?>
=== trac/Environment.py
==================================================================
--- trac/Environment.py  (revision 1147)
+++ trac/Environment.py  (local)
@@ -214,6 +214,13 @@
     def get_attachments_dir(self):
         return os.path.join(self.path, 'attachments')
 
+    def get_attachment(self, cnx, type, id, filename):
+        cursor = cnx.cursor()
+        cursor.execute('SELECT filename,description,type,size,time,author,ipnr '
+                       'FROM attachment '
+                       'WHERE type=%s AND id=%s AND filename=%s ORDER BY time', type, id, filename)
+        return cursor.fetchone()
+    
     def get_attachments(self, cnx, type, id):
         cursor = cnx.cursor()
         cursor.execute('SELECT filename,description,type,size,time,author,ipnr '
@@ -221,26 +228,34 @@
                        'WHERE type=%s AND id=%s ORDER BY time', type, id)
         return cursor.fetchall()
     
+    def get_attachment_hdf(self, cnx, type, id, hdf, prefix, file, idx=None):
+        from Wiki import wiki_to_oneliner
+        if idx:
+            p = '%s.%d' % (prefix, idx)
+        else:
+            p = prefix
+        hdf.setValue(p + '.name', file['filename'])
+        hdf.setValue(p + '.descr',
+                     wiki_to_oneliner(file['description'], hdf, self, cnx))
+        hdf.setValue(p + '.author', util.escape(file['author']))
+        hdf.setValue(p + '.ipnr', file['ipnr'])
+        hdf.setValue(p + '.size', util.pretty_size(file['size']))
+        hdf.setValue(p + '.time',
+                     time.strftime('%c', time.localtime(file['time'])))
+        hdf.setValue(p + '.href',
+                     self.href.attachment(type, id, file['filename']))
+
+
     def get_attachments_hdf(self, cnx, type, id, hdf, prefix):
         from Wiki import wiki_to_oneliner
         files = self.get_attachments(cnx, type, id)
         idx = 0
         for file in files:
-            p = '%s.%d' % (prefix, idx)
-            hdf.setValue(p + '.name', file['filename'])
-            hdf.setValue(p + '.descr',
-                         wiki_to_oneliner(file['description'], hdf, self, cnx))
-            hdf.setValue(p + '.author', util.escape(file['author']))
-            hdf.setValue(p + '.ipnr', file['ipnr'])
-            hdf.setValue(p + '.size', util.pretty_size(file['size']))
-            hdf.setValue(p + '.time',
-                         time.strftime('%c', time.localtime(file['time'])))
-            hdf.setValue(p + '.href',
-                         self.href.attachment(type, id, file['filename']))
+            self.get_attachment_hdf(cnx, type, id, hdf, prefix, file, idx)
             idx += 1
 
     def create_attachment(self, cnx, type, id, attachment,
-                          description, author, ipnr):
+                          description, author, ipnr, replace=0):
         # Maximum attachment size (in bytes)
         max_size = int(self.get_config('attachment', 'max_size', '262144'))
         if hasattr(attachment.file, 'fileno'):
@@ -258,6 +273,14 @@
         filename = attachment.filename.replace('\\', '/').replace(':', '/')
         filename = os.path.basename(filename)
 
+        cursor = cnx.cursor()
+        cursor.execute('SELECT author FROM attachment WHERE '
+                       'type=%s AND id=%s AND filename=%s',
+                       type, id, filename)
+        row = cursor.fetchone()
+        if row and author != row['author']:
+           replace = 0
+
         # We try to normalize the filename to utf-8 NFC if we can.
         # Files uploaded from OS X might be in NFD.
         if sys.version_info[0] > 2 or \
@@ -265,11 +288,15 @@
             filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8')
             
         filename = urllib.quote(filename)
-        path, fd = util.create_unique_file(os.path.join(dir, filename))
+        path, fd = util.create_unique_file(os.path.join(dir, filename), replace)
         filename = os.path.basename(path)
         filename = urllib.unquote(filename)
-        cursor = cnx.cursor()
-        cursor.execute('INSERT INTO attachment VALUES(%s,%s,%s,%s,%s,%s,%s,%s)',
+
+        if replace:
+            action = "REPLACE"
+        else:
+            action = "INSERT"
+        cursor.execute(action + ' INTO attachment VALUES(%s,%s,%s,%s,%s,%s,%s,%s)',
                        type, id, filename, length, int(time.time()),
                        description, author, ipnr)
         shutil.copyfileobj(attachment.file, fd)
=== trac/File.py
==================================================================
--- trac/File.py  (revision 1147)
+++ trac/File.py  (local)
@@ -104,22 +104,39 @@
                     self.env.href.wiki(self.attachment_id))
         assert 0
 
+    def can_delete(self, do_assert=None):
+        print 'author', self.file['author'], 'authname', self.req.authname
+        if self.file and self.file['author'] and self.req.authname:
+            if self.file['author'] == self.req.authname:
+                return 1
+        perm_map = {'ticket': perm.TICKET_ADMIN, 'wiki': perm.WIKI_DELETE}
+        if do_assert:
+            self.perm.assert_permission(perm_map[self.attachment_type])
+        else:
+            return self.perm.has_permission(perm_map[self.attachment_type])
+                
     def render(self):
         FileCommon.render(self)
         self.view_form = 0
         self.attachment_type = self.args.get('type', None)
         self.attachment_id = self.args.get('id', None)
         self.filename = self.args.get('filename', None)
-        if self.filename:
-            self.filename = os.path.basename(self.filename)
 
         if not self.attachment_type or not self.attachment_id:
             raise util.TracError('Unknown request')
 
+        if self.filename:
+            self.filename = os.path.basename(self.filename)
+            self.file = self.env.get_attachment(self.db, self.attachment_type,
+                                                self.attachment_id, self.filename)
+            if self.file:
+                self.env.get_attachment_hdf(self.db, self.attachment_type,
+                                            self.attachment_id, self.req.hdf,
+                                            'attachment', self.file)
+
         if self.filename and len(self.filename) > 0 and \
                self.args.has_key('delete'):
-            perm_map = {'ticket': perm.TICKET_ADMIN, 'wiki': perm.WIKI_DELETE}
-            self.perm.assert_permission(perm_map[self.attachment_type])
+            self.can_delete(do_assert=1)
             self.env.delete_attachment(self.db,
                                        self.attachment_type,
                                        self.attachment_id,
@@ -160,8 +177,7 @@
                                                    self.filename, 'raw'),
                 'Original Format', self.mime_type)
 
-            perm_map = {'ticket': perm.TICKET_ADMIN, 'wiki': perm.WIKI_DELETE}
-            if self.perm.has_permission(perm_map[self.attachment_type]):
+            if self.can_delete():
                 self.req.hdf.setValue('attachment.delete_href', '?delete=yes')
 
             return
@@ -187,7 +203,9 @@
                                                   self.args['attachment'],
                                                   self.args.get('description'),
                                                   self.args.get('author'),
-                                                  self.req.remote_addr)
+                                                  self.req.remote_addr,
+                                                  self.args.get('replace'))
+
             # Redirect the user to the newly created attachment
             self.req.redirect(self.env.href.attachment(self.attachment_type,
                                                        self.attachment_id,
@@ -210,6 +228,7 @@
             self.req.hdf.setValue('attachment.author', util.get_reporter_id(self.req))
             self.req.display('attachment.cs')
             return
+
         self.req.hdf.setValue('file.filename', urllib.unquote(self.filename))
         self.req.hdf.setValue('trac.active_module', self.attachment_type) # Kludge
         FileCommon.display(self)
=== trac/util.py
==================================================================
--- trac/util.py  (revision 1147)
+++ trac/util.py  (local)
@@ -235,17 +235,21 @@
             return '%i %s' % (r, r == 1 and unit or unit_plural)
     return ''
 
-def create_unique_file(path):
+def create_unique_file(path,replace=0):
     """Create a new file. An index is added if the path exists"""
     parts = os.path.splitext(path)
     idx = 1
     while 1:
         try:
-            flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
+            flags = os.O_CREAT + os.O_WRONLY
+            if not replace:
+                flags += os.O_EXCL
             if hasattr(os, 'O_BINARY'):
                 flags += os.O_BINARY
             return path, os.fdopen(os.open(path, flags), 'w')
-        except OSError:
+        except OSError, e:
+            if replace:
+                raise e # propagate failure
             idx += 1
             # A sanity check
             if idx > 100:
