diff --git a/bootstrap.php b/bootstrap.php
index a82796ebd2d7ecba1eb453884502f10a193ded53..0de2d5d69bc168948d8c25c7b31f5f3241c98dcc 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -172,6 +172,7 @@ class Bootstrap {
 
     function loadCode() {
         #include required files
+        require(INCLUDE_DIR.'class.signal.php');
         require(INCLUDE_DIR.'class.auth.php');
         require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper!
         require(INCLUDE_DIR.'class.log.php');
diff --git a/include/class.file.php b/include/class.file.php
index d3f8393b6be57498183d555130b7b984d88aaaf5..36212ec5d0f48562b4d934fd73c7911940c82390 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -11,6 +11,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once(INCLUDE_DIR.'class.signal.php');
 
 class AttachmentFile {
 
@@ -27,7 +28,7 @@ class AttachmentFile {
         if(!$id && !($id=$this->getId()))
             return false;
 
-        $sql='SELECT id, f.type, size, name, hash, f.created, '
+        $sql='SELECT id, f.type, size, name, hash, ft, f.created, '
             .' count(DISTINCT a.object_id) as canned, count(DISTINCT t.ticket_id) as tickets '
             .' FROM '.FILE_TABLE.' f '
             .' LEFT JOIN '.ATTACHMENT_TABLE.' a ON(a.file_id=f.id) '
@@ -75,6 +76,10 @@ class AttachmentFile {
         return $this->ht['type'];
     }
 
+    function getBackend() {
+        return $this->ht['ft'];
+    }
+
     function getMime() {
         return $this->getType();
     }
@@ -104,21 +109,23 @@ class AttachmentFile {
     }
 
     function open() {
-        return new AttachmentChunkedData($this->id);
+        return AttachmentStorageBackend::getInstance($this);
     }
 
-    function sendData() {
+    function sendData($redirect=true) {
+        $bk = $this->open();
+        if ($redirect && $bk->sendRedirectUrl())
+            return;
+
         @ini_set('zlib.output_compression', 'Off');
-        $file = $this->open();
-        while ($chunk = $file->read())
-            echo $chunk;
+        $bk->passthru();
     }
 
     function getData() {
         # XXX: This is horrible, and is subject to php's memory
         #      restrictions, etc. Don't use this function!
         ob_start();
-        $this->sendData();
+        $this->sendData(false);
         $data = &ob_get_contents();
         ob_end_clean();
         return $data;
@@ -193,18 +200,57 @@ class AttachmentFile {
         exit();
     }
 
+    function _getKeyAndHash($data=false, $file=false) {
+        if ($file) {
+            $sha1 = base64_encode(sha1_file($data, true));
+            $md5 = base64_encode(md5_file($data, true));
+        }
+        else {
+            $sha1 = base64_encode(sha1($data, true));
+            $md5 = base64_encode(md5($data, true));
+        }
+
+        // Use 5 chars from the microtime() prefix and 27 chars from the
+        // sha1 hash. This should make a sufficiently strong unique key for
+        // file content. In the event there is a sha1 collision for data, it
+        // should be unlikely that there will be a collision for the
+        // microtime hash coincidently.  Remove =, change + and / to chars
+        // better suited for URLs and filesystem paths
+        $prefix = base64_encode(sha1(microtime(), true));
+        $key = str_replace(
+            array('=','+','/'),
+            array('','-',','),
+            substr($prefix, 0, 5) . $sha1);
+
+        // The hash is a 32-char value where the first half is from the last
+        // 16 chars from the SHA1 hash and the last 16 chars are the last 16
+        // chars from the MD5 hash. This should provide for better
+        // resiliance against hash collisions and attacks against any one
+        // hash algorithm. Since we're using base64 encoding, with 6-bits
+        // per char, we should have a total hash strength of 192 bits.
+        $hash = str_replace(
+            array('=','+','/'),
+            array('','-',','),
+            substr($sha1, -16) . substr($md5, -16));
+
+        return array($key, $hash);
+    }
+
     /* Function assumes the files types have been validated */
-    function upload($file, $ft='T') {
+    function upload($file, $ft=false) {
 
         if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
             return false;
 
+        list($key, $hash) = static::_getKeyAndHash($file['tmp_name'], true);
+
         $info=array('type'=>$file['type'],
                     'filetype'=>$ft,
                     'size'=>$file['size'],
                     'name'=>$file['name'],
-                    'hash'=>MD5(MD5_FILE($file['tmp_name']).time()),
-                    'data'=>file_get_contents($file['tmp_name'])
+                    'key'=>$key,
+                    'hash'=>$hash,
+                    'tmp_nape'=>$file['tmp_name'],
                     );
 
         return AttachmentFile::save($info);
@@ -241,16 +287,30 @@ class AttachmentFile {
         return false;
     }
 
-    function save($file) {
+    function save($file, $save_bk=true) {
+
+        if (isset($file['data'])) {
+            // Allow a callback function to delay or avoid reading or
+            // fetching ihe file contents
+            if (is_callable($file['data']))
+                $file['data'] = $file['data']();
 
-        if (is_callable($file['data']))
-            $file['data'] = $file['data']();
+            list($file['key'], $file['hash'])
+                = static::_getKeyAndHash($file['data']);
 
-        if(!$file['hash'])
-            $file['hash'] = MD5(MD5($file['data']).time());
+            if (!isset($file['size']))
+                $file['size'] = strlen($file['data']);
+        }
+
+        // Check and see if the file is already on record
+        $sql = 'SELECT id FROM '.FILE_TABLE
+            .' WHERE hash='.db_input($file['hash'])
+            .' AND size='.db_input($file['size']);
 
-        if(!$file['size'])
-            $file['size'] = strlen($file['data']);
+        // If the record exists in the database already, a file with the
+        // same hash and size is already on file -- just return its ID
+        if ($id = db_result(db_query($sql)))
+            return $id;
 
         $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
             .',type='.db_input(strtolower($file['type']))
@@ -258,19 +318,83 @@ class AttachmentFile {
             .',name='.db_input($file['name'])
             .',hash='.db_input($file['hash']);
 
+        if (!(db_query($sql) && ($file['id']=db_insert_id())))
+            return false;
+
+        $bk = self::getBackendForFile($file);
+        if (isset($file['tmp_file'])) {
+            if (!$bk->upload($file['tmp_file']))
+                return false;
+        }
+        elseif (!$bk->write($file['data'])) {
+            // XXX: Fallthrough to default backend if different?
+            return false;
+        }
+
         # XXX: ft does not exists during the upgrade when attachments are
         #      migrated!
-        if(isset($file['filetype']))
-            $sql.=',ft='.db_input($file['filetype']);
+        if ($save_bk) {
+            $sql .= 'UPDATE '.FILE_TABLE.' SET ft='
+                .db_input(AttachmentStorageBackend::getTypeChar($bk))
+                .' WHERE id='.db_input($file->getId());
+            db_query($sql);
+        }
 
-        if (!(db_query($sql) && ($id=db_insert_id())))
+        return $file->getId();
+    }
+
+    /**
+     * Migrate this file from the current backend to the backend specified.
+     *
+     * Parameters:
+     * $bk - (string) type char of the target storage backend. Use
+     *      AttachmentStorageBackend::allRegistered() to get a list of type
+     *      chars and associated class names
+     *
+     * Returns:
+     * True if the migration was successful and false otherwise.
+     */
+    function migrate($bk) {
+        $before = hash_init('sha1');
+        $after = hash_init('sha1');
+
+        // Copy the file to the new backend and hash the contents
+        $target = AttachmentStorageBackend::lookup($bk, $this->ht);
+        print("Opening source\n");
+        $source = $this->open();
+        print("Copying data ");
+        while ($block = $source->read()) {
+            print(".");
+            hash_update($before, $block);
+            $target->write($block);
+        }
+        print(" Done\n");
+
+        // Verify that the hash of the target file matches the hash of the
+        // source file
+        print("Verifying transferred data\n");
+        $target = AttachmentStorageBackend::lookup($bk, $this->ht);
+        while ($block = $target->read())
+            hash_update($after, $block);
+
+        if (hash_final($before) != hash_final($after)) {
+            $target->unlink();
             return false;
+        }
 
-        $data = new AttachmentChunkedData($id);
-        if (!$data->write($file['data']))
+        print("Updating file meta table\n");
+        $sql = 'UPDATE '.FILE_TABLE.' SET ft='
+            .db_input($target->getTypeChar())
+            .' WHERE id='.db_input($this->getId());
+        if (!db_query($sql) || db_affected_rows()!=1)
             return false;
 
-        return $id;
+        print("Unlinking source data\n");
+        return $source->unlink();
+    }
+
+    static function getBackendForFile($info) {
+        return AttachmentStorageBackend::lookup('T', $info);
     }
 
     /* Static functions */
@@ -352,11 +476,14 @@ class AttachmentFile {
      */
     /* static */ function deleteOrphans() {
 
+        // XXX: Allow plugins to define filetypes which do not represent
+        //      files attached to tickets or other things in the attachment
+        //      table and are not logos
         $sql = 'DELETE FROM '.FILE_TABLE.' WHERE id NOT IN ('
                 .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE
                 .' UNION '
                 .'SELECT file_id FROM '.ATTACHMENT_TABLE
-            .") AND `ft` = 'T'";
+            .") AND `ft` NOT IN ('L')";
 
         db_query($sql);
 
@@ -379,22 +506,136 @@ class AttachmentFile {
     }
 }
 
+class AttachmentStorageBackend {
+    var $meta;
+    static $desc = false;
+    static $registry;
+
+    /**
+     * All storage backends should call this function during the request
+     * bootstrap phase.
+     */
+    static function register($typechar, $class) {
+        self::$registry[$typechar] = $class;
+    }
+
+    static function allRegistered() {
+        return self::$registry;
+    }
+
+    /**
+     * Retrieves the type char registered for this storage backend's class.
+     * Null is returned if the backend is not properly registered.
+     */
+    function getTypeChar() {
+        foreach (self::$registry as $tc=>$class)
+            if ($this instanceof $class)
+                return $tc;
+    }
+
+    static function lookup($type, $file=null) {
+        if (!isset(self::$registry[$type]))
+            throw new Exception("No such backend registered");
+
+        $class = self::$registry[$type];
+        return new $class($file);
+    }
+
+    static function getInstance($file) {
+        if (!isset(self::$registry[$file->getBackend()]))
+            throw new Exception("No such backend registered");
+
+        $class = self::$registry[$file->getBackend()];
+        return new $class($file);
+    }
+
+    /**
+     * Create an instance of the storage backend linking the related file.
+     * Information about the file metadata is accessible via the received
+     * filed object.
+     */
+    function __construct($meta) {
+        $this->meta = $meta;
+    }
+
+    /**
+     * Commit file to the storage backend. This method is used if the
+     * backend cannot support writing a file directly. Otherwise, the
+     * ::upload($file) method is preferred.
+     *
+     * Parameters:
+     * $data - (string|binary) file contents to be written to the backend
+     */
+    function write($data) {
+        return false;
+    }
+
+    /**
+     * Upload a file to the backend. This method is preferred over ::write()
+     * for files which are uploaded or are otherwise available out of
+     * memory. The backend is encouraged to avoid reading the entire
+     * contents into memory.
+     */
+    function upload($filepath) {
+        return static::write(file_get_contents($filepath));
+    }
+
+    /**
+     * Returns data from the backend, optionally returning only the number
+     * of bytes indicated at the specified offset. If the data is available
+     * in chunks, one chunk may be returned at a time. The backend should
+     * return boolean false when no more chunks are available.
+     */
+    function read($amount=0, $offset=0) {
+    }
+
+    /**
+     * Convenience method to send all the file to standard output
+     */
+    function passthru() {
+        while ($block = $this->read())
+            echo $block;
+    }
+
+    /**
+     * If the data is not stored or not available locally, a redirect
+     * response can be sent to the user agent indicating the actual HTTP
+     * location of the data.
+     *
+     * If the data is available locally, this method should return boolean
+     * false to indicate that the read() method should be used to retrieve
+     * the data and broker it to the user agent.
+     */
+    function sendRedirectUrl() {
+        return false;
+    }
+
+    /**
+     * Requests the backend to remove the file contents.
+     */
+    function unlink() {
+        return false;
+    }
+}
+
 /**
- * Attachments stored in the database are cut into 256kB chunks and stored
+ * Attachments stored in the database are cut into 500kB chunks and stored
  * in the FILE_CHUNK_TABLE to overcome the max_allowed_packet limitation of
  * LOB fields in the MySQL database
  */
 define('CHUNK_SIZE', 500*1024); # Beware if you change this...
-class AttachmentChunkedData {
-    function AttachmentChunkedData($file) {
-        $this->_file = $file;
+class AttachmentChunkedData extends AttachmentStorageBackend {
+    static $desc = "In the database";
+
+    function __construct($file) {
+        $this->file = $file;
         $this->_pos = 0;
     }
 
     function length() {
         list($length) = db_fetch_row(db_query(
              'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE
-            .' WHERE file_id='.db_input($this->_file)));
+            .' WHERE file_id='.db_input($this->file->getId())));
         return $length;
     }
 
@@ -402,7 +643,7 @@ class AttachmentChunkedData {
         # Read requested length of data from attachment chunks
         list($buffer) = @db_fetch_row(db_query(
             'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id='
-            .db_input($this->_file).' AND chunk_id='.$this->_pos++));
+            .db_input($this->file->getId()).' AND chunk_id='.$this->_pos++));
         return $buffer;
     }
 
@@ -413,7 +654,7 @@ class AttachmentChunkedData {
             if (!$block) break;
             if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE
                     .' SET filedata=0x'.bin2hex($block).', file_id='
-                    .db_input($this->_file).', chunk_id='.db_input($this->_pos++)))
+                    .db_input($this->file->getId()).', chunk_id='.db_input($this->_pos++)))
                 return false;
             $offset += strlen($block);
         }
@@ -421,11 +662,17 @@ class AttachmentChunkedData {
         return $this->_pos;
     }
 
+    function unlink() {
+        db_query('DELETE FROM '.FILE_CHUNK_TABLE
+            .' WHERE file_id='.db_input($this->file->getId()));
+        return db_affected_rows() > 0;
+    }
+
     function deleteOrphans() {
         $deleted = 0;
         $sql = 'SELECT c.file_id, c.chunk_id FROM '.FILE_CHUNK_TABLE.' c '
              . ' LEFT JOIN '.FILE_TABLE.' f ON(f.id=c.file_id) '
-             . ' WHERE f.id IS NULL';
+             . ' WHERE f.id IS NULL AND f.`type`=\'T\'';
 
         $res = db_query($sql);
         while (list($file_id, $chunk_id) = db_fetch_row($res)) {
@@ -437,4 +684,7 @@ class AttachmentChunkedData {
         return $deleted;
     }
 }
+AttachmentStorageBackend::register('T', 'AttachmentChunkedData');
+Signal::connect('cron', array('AttachmentChunkedData', 'deleteOrphans'));
+
 ?>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index ce7489d19ddd15863d7849d3c33930ee93029712..c12a3e29409d401d5036bc94f4f0e23bbf643cbc 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -240,6 +240,18 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <input type="checkbox" name="email_attachments" <?php echo $config['email_attachments']?'checked="checked"':''; ?> >Email attachments to the user
             </td>
         </tr>
+        <?php if (($bks = AttachmentStorageBackend::allRegistered())
+                && count($bks) > 1) { ?>
+        <tr>
+            <td width="180">Store Attachments:</td>
+            <td><select name="default_storage_bk"><?php
+                foreach ($bks as $char=>$class) {
+                    ?><option value="<?php echo $char; ?>
+                    "><?php echo $class::$desc; ?></option><?php
+                } ?>
+            </td>
+        </tr>
+        <?php } ?>
         <tr>
             <th colspan="2">
                 <em><strong>Accepted File Types</strong>: Limit the type of files users are allowed to submit.