diff --git a/attachment.php b/attachment.php
index 1c0941d7d545d5bbdfaeaa50029d9900ba6e5b39..73dbfd710b9a58f164034d35239842f01a102f17 100644
--- a/attachment.php
+++ b/attachment.php
@@ -25,7 +25,7 @@ if(!$thisclient
     die('Unknown attachment!');
 
 //Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!!
-$vhash=md5($attachment->getFileId().session_id().$file->getHash());
+$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey()));
 if(strcasecmp(trim($_GET['h']),$vhash)
         || !($ticket=$attachment->getTicket())
         || !$ticket->checkUserAccess($thisclient))
diff --git a/include/ajax.draft.php b/include/ajax.draft.php
index 0caaeeb7bb497cbd06ca7a4e83b860f16232ec0b..3cad72194b839d4b4c26b114c0902c6201d9c727 100644
--- a/include/ajax.draft.php
+++ b/include/ajax.draft.php
@@ -107,7 +107,7 @@ class DraftAjaxAPI extends AjaxController {
             return Http::response(500, 'Unable to attach image');
 
         echo JsonDataEncoder::encode(array(
-            'content_id' => 'cid:'.$f->getHash(),
+            'content_id' => 'cid:'.$f->getKey(),
             'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash())
         ));
     }
diff --git a/include/class.config.php b/include/class.config.php
index f5c7077d7dcbe07cddca31d10e83d1e63a7cb32e..54928de8f28a5e49934d516df8cda0bd5b52e2a2 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -149,6 +149,7 @@ class OsticketConfig extends Config {
         'name_format' =>        'full', # First Last
         'auto_claim_tickets'=>  true,
         'system_language' =>    'en_US',
+        'default_storage_bk' => 'D',
     );
 
     function OsticketConfig($section=null) {
@@ -725,6 +726,10 @@ class OsticketConfig extends Config {
         return $this->get('upload_dir');
     }
 
+    function getDefaultStorageBackendChar() {
+        return $this->get('default_storage_bk');
+    }
+
     function getVar($name) {
         return $this->get($name);
     }
@@ -855,6 +860,9 @@ class OsticketConfig extends Config {
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
 
+        if (isset($vars['default_storage_bk']))
+            $this->update('default_storage_bk', $vars['default_storage_bk']);
+
         return $this->updateAll(array(
             'random_ticket_ids'=>$vars['random_ticket_ids'],
             'default_priority_id'=>$vars['default_priority_id'],
diff --git a/include/class.cron.php b/include/class.cron.php
index 2dcfc1b4ba544dbedbca90ce76895e70f288828b..06d992cdcea53dcca5f02891014be3c6ce27a37a 100644
--- a/include/class.cron.php
+++ b/include/class.cron.php
@@ -102,7 +102,7 @@ class Cron {
         self::PurgeDrafts();
         self::MaybeOptimizeTables();
 
-        Signal::send('cron');
+        Signal::send('cron', null);
     }
 }
 ?>
diff --git a/include/class.file.php b/include/class.file.php
index a5cc07940e67f7e62bfcfcd8acc9bc9cdf5f133c..52451de550c6400e91c23870de0b1ec3243a505c 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -29,7 +29,7 @@ class AttachmentFile {
         if(!$id && !($id=$this->getId()))
             return false;
 
-        $sql='SELECT id, f.type, size, name, hash, ft, f.created, '
+        $sql='SELECT id, f.type, size, name, `key`, signature, ft, bk, 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) '
@@ -78,7 +78,7 @@ class AttachmentFile {
     }
 
     function getBackend() {
-        return $this->ht['ft'];
+        return $this->ht['bk'];
     }
 
     function getMime() {
@@ -93,8 +93,14 @@ class AttachmentFile {
         return $this->ht['name'];
     }
 
-    function getHash() {
-        return $this->ht['hash'];
+    function getKey() {
+        return $this->ht['key'];
+    }
+
+    function getSignature() {
+        $sig = $this->ht['signature'];
+        if (!$sig) return $this->getKey();
+        return $sig;
     }
 
     function lastModified() {
@@ -102,15 +108,15 @@ class AttachmentFile {
     }
 
     /**
-     * Retrieve a hash that can be sent to scp/file.php?h= in order to
+     * Retrieve a signature that can be sent to scp/file.php?h= in order to
      * download this file
      */
     function getDownloadHash() {
-        return strtolower($this->getHash() . md5($this->getId().session_id().$this->getHash()));
+        return strtolower($this->getKey() . md5($this->getId().session_id().$this->getKey()));
     }
 
     function open() {
-        return AttachmentStorageBackend::getInstance($this);
+        return FileStorageBackend::getInstance($this);
     }
 
     function sendData($redirect=true) {
@@ -155,7 +161,7 @@ class AttachmentFile {
     }
 
     function makeCacheable($ttl=86400) {
-        Http::cacheable($this->getHash(), $this->lastModified(), $ttl);
+        Http::cacheable($this->getSignature(), $this->lastModified(), $ttl);
     }
 
     function display($scale=false) {
@@ -242,29 +248,29 @@ class AttachmentFile {
         $hash = str_replace(
             array('=','+','/'),
             array('','-','_'),
-            substr($sha1, -16) . substr($md5, -16));
+            substr($sha1, 0, 16) . substr($md5, 0, 16));
 
         return array($key, $hash);
     }
 
     /* Function assumes the files types have been validated */
-    function upload($file, $ft=false) {
+    function upload($file, $ft='T') {
 
         if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
             return false;
 
-        list($key, $hash) = static::_getKeyAndHash($file['tmp_name'], true);
+        list($key, $sig) = self::_getKeyAndHash($file['tmp_name'], true);
 
         $info=array('type'=>$file['type'],
                     'filetype'=>$ft,
                     'size'=>$file['size'],
                     'name'=>$file['name'],
                     'key'=>$key,
-                    'hash'=>$hash,
-                    'tmp_nape'=>$file['tmp_name'],
+                    'signature'=>$sig,
+                    'tmp_name'=>$file['tmp_name'],
                     );
 
-        return AttachmentFile::save($info);
+        return AttachmentFile::save($info, $ft);
     }
 
     function uploadLogo($file, &$error, $aspect_ratio=3) {
@@ -298,7 +304,7 @@ class AttachmentFile {
         return false;
     }
 
-    function save($file, $save_bk=true) {
+    function save($file, $ft=false) {
 
         if (isset($file['data'])) {
             // Allow a callback function to delay or avoid reading or
@@ -306,7 +312,7 @@ class AttachmentFile {
             if (is_callable($file['data']))
                 $file['data'] = $file['data']();
 
-            list($file['key'], $file['hash'])
+            list($file['key'], $file['signature'])
                 = static::_getKeyAndHash($file['data']);
 
             if (!isset($file['size']))
@@ -315,7 +321,7 @@ class AttachmentFile {
 
         // Check and see if the file is already on record
         $sql = 'SELECT id FROM '.FILE_TABLE
-            .' WHERE hash='.db_input($file['hash'])
+            .' WHERE signature='.db_input($file['signature'])
             .' AND size='.db_input($file['size']);
 
         // If the record exists in the database already, a file with the
@@ -327,7 +333,8 @@ class AttachmentFile {
             .',type='.db_input(strtolower($file['type']))
             .',size='.db_input($file['size'])
             .',name='.db_input($file['name'])
-            .',hash='.db_input($file['hash']);
+            .',`key`='.db_input($file['key'])
+            .',signature='.db_input($file['signature']);
 
         if (!(db_query($sql) && ($file['id']=db_insert_id())))
             return false;
@@ -343,10 +350,11 @@ class AttachmentFile {
         }
 
         # XXX: ft does not exists during the upgrade when attachments are
-        #      migrated!
-        if ($save_bk) {
+        #      migrated! Neither does `bk`
+        if ($ft) {
             $sql .= 'UPDATE '.FILE_TABLE.' SET ft='
-                .db_input(AttachmentStorageBackend::getTypeChar($bk))
+                .db_input(FileStorageBackend::getBkChar($bk))
+                .', ft='.db_input($ft)
                 .' WHERE id='.db_input($file->getId());
             db_query($sql);
         }
@@ -397,8 +405,8 @@ class AttachmentFile {
         }
 
         print("Updating file meta table\n");
-        $sql = 'UPDATE '.FILE_TABLE.' SET ft='
-            .db_input($target->getTypeChar())
+        $sql = 'UPDATE '.FILE_TABLE.' SET bk='
+            .db_input($target->getBkChar())
             .' WHERE id='.db_input($this->getId());
         if (!db_query($sql) || db_affected_rows()!=1)
             return false;
@@ -407,14 +415,17 @@ class AttachmentFile {
         return $source->unlink();
     }
 
-    static function getBackendForFile($info) {
-        return AttachmentStorageBackend::lookup('T', $info);
+    static function getBackendForFile($file) {
+        global $cfg;
+
+        $char = $cfg->getDefaultStorageBackendChar();
+        return FileStorageBackend::lookup($char, $file);
     }
 
     /* Static functions */
     function getIdByHash($hash) {
 
-        $sql='SELECT id FROM '.FILE_TABLE.' WHERE hash='.db_input($hash);
+        $sql='SELECT id FROM '.FILE_TABLE.' WHERE `key`='.db_input($hash);
         if(($res=db_query($sql)) && db_num_rows($res))
             list($id)=db_fetch_row($res);
 
@@ -497,15 +508,14 @@ class AttachmentFile {
                 .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE
                 .' UNION '
                 .'SELECT file_id FROM '.ATTACHMENT_TABLE
-            .") AND `ft` NOT IN ('L')";
+            .") AND `ft` = 'T'";
 
         if (!($res = db_query($sql)))
             return false;
 
         while (list($id) = db_fetch_row($res))
-            if (($file = self::lookup($id))
-                    && ($bk = $file->open()))
-                $bk->unlink();
+            if (($file = self::lookup($id)) && !$file->delete())
+                break;
 
         return true;
     }
@@ -522,7 +532,7 @@ class AttachmentFile {
     }
 }
 
-class AttachmentStorageBackend {
+class FileStorageBackend {
     var $meta;
     static $desc = false;
     static $registry;
@@ -543,7 +553,7 @@ class AttachmentStorageBackend {
      * Retrieves the type char registered for this storage backend's class.
      * Null is returned if the backend is not properly registered.
      */
-    function getTypeChar() {
+    function getBkChar() {
         foreach (self::$registry as $tc=>$class)
             if ($this instanceof $class)
                 return $tc;
@@ -593,7 +603,7 @@ class AttachmentStorageBackend {
      * contents into memory.
      */
     function upload($filepath) {
-        return static::write(file_get_contents($filepath));
+        return $this->write(file_get_contents($filepath));
     }
 
     /**
@@ -635,13 +645,14 @@ class AttachmentStorageBackend {
     }
 }
 
+
 /**
  * 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 extends AttachmentStorageBackend {
+class AttachmentChunkedData extends FileStorageBackend {
     static $desc = "In the database";
 
     function __construct($file) {
@@ -685,6 +696,6 @@ class AttachmentChunkedData extends AttachmentStorageBackend {
         return db_affected_rows() > 0;
     }
 }
-AttachmentStorageBackend::register('T', 'AttachmentChunkedData');
+FileStorageBackend::register('D', 'AttachmentChunkedData');
 
 ?>
diff --git a/include/class.mailer.php b/include/class.mailer.php
index 21a7d157d68a28191b88e798dade0633d3c15725..f6b83784edd727361e927d195a1c65702759a26d 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -166,7 +166,7 @@ class Mailer {
                         return $match[0];
                     $mime->addHTMLImage($file->getData(),
                         $file->getType(), $file->getName(), false,
-                        $file->getHash().'@'.$domain);
+                        $file->getKey().'@'.$domain);
                     // Don't re-attach the image below
                     unset($self->attachments[$file->getId()]);
                     return $match[0].'@'.$domain;
diff --git a/include/class.thread.php b/include/class.thread.php
index 6fa0d9c13cc74e85dd8e57dce0c4f90411c1166f..7a1af0738caac9fd54afce0e3a07f99afcec37fc 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -528,7 +528,7 @@ Class ThreadEntry {
             return $this->attachments;
 
         //XXX: inner join the file table instead?
-        $sql='SELECT a.attach_id, f.id as file_id, f.size, f.hash as file_hash, f.name '
+        $sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.name '
             .' FROM '.FILE_TABLE.' f '
             .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
             .' WHERE a.ticket_id='.db_input($this->getTicketId())
diff --git a/include/class.yaml.php b/include/class.yaml.php
index fc340d70abc0153b3e3f8071b8981c3da17f5cbc..63ceb37543d3fe4044172e1a7775c84e6734feb8 100644
--- a/include/class.yaml.php
+++ b/include/class.yaml.php
@@ -37,6 +37,6 @@ class YamlDataParser {
 }
 
 class YamlParserError extends Error {
-    var $title = 'Error parsing YAML document';
+    static $title = 'Error parsing YAML document';
 }
 ?>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index c12a3e29409d401d5036bc94f4f0e23bbf643cbc..b25ad9d597c4daed162e69aa0df78a5f3ef7e32a 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -240,14 +240,16 @@ 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())
+        <?php if (($bks = FileStorageBackend::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
+                    $selected = $config['default_storage_bk'] == $char
+                        ? 'selected="selected"' : '';
+                    ?><option <?php echo $selected; ?> value="<?php echo $char; ?>"
+                    ><?php echo $class::$desc; ?></option><?php
                 } ?>
             </td>
         </tr>
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index 7fe18f3820d783b5fff99a0e154dc131a174f245..a1b180aae2c0c8d47a8819d29595c83d2b6a0489 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -278,7 +278,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                             if($info['cannedattachments']) {
                                 foreach($info['cannedattachments'] as $k=>$id) {
                                     if(!($file=AttachmentFile::lookup($id))) continue;
-                                    $hash=$file->getHash().md5($file->getId().session_id().$file->getHash());
+                                    $hash=$file->getKey().md5($file->getId().session_id().$file->getKey());
                                     echo sprintf('<label><input type="checkbox" name="cannedattachments[]"
                                             id="f%d" value="%d" checked="checked"
                                             <a href="file.php?h=%s">%s</a>&nbsp;&nbsp;</label>&nbsp;',
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index ac8e6b7747418718552fa3ecf14189a652bf9901..9c7611253e6438aec902c1091dd72e2415f2e564 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-934954de8914d9bd2bb8343e805340ae
+227215f11d6a2625e13a528e40a156dd
diff --git a/include/upgrader/streams/core/ed60ba20-ee1f4b27.patch.sql b/include/upgrader/streams/core/ed60ba20-ee1f4b27.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..cbf4e9d0610ccbdedd23a33a5a986bb351a2a770
--- /dev/null
+++ b/include/upgrader/streams/core/ed60ba20-ee1f4b27.patch.sql
@@ -0,0 +1,19 @@
+/**
+ * @version v1.8.1
+ * @signature ee1f4b2752ee7b4be24a4d9dfe96185a
+ * @title Pluggable Storage
+ *
+ * This patch will allow attachments to be stored outside the database (like
+ * on the filesystem)
+ */
+
+ALTER TABLE `%TABLE_PREFIX%file`
+    ADD `bk` CHAR(1) NOT NULL DEFAULT 'D' AFTER `ft`,
+    CHANGE `hash` `key` VARCHAR(125) COLLATE ascii_general_ci,
+    ADD `signature` VARCHAR(125) COLLATE ascii_bin AFTER `key`,
+    ADD INDEX (`signature`);
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = 'ee1f4b2752ee7b4be24a4d9dfe96185a'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/kb/file.php b/kb/file.php
index c411002b8f1ed2f2674f7eb52528dc9355570891..21336765817fa588a82a983af5e8b52dc8da2a85 100644
--- a/kb/file.php
+++ b/kb/file.php
@@ -23,7 +23,7 @@ $h=trim($_GET['h']);
 //basic checks
 if(!$h  || strlen($h)!=64  //32*2
         || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getHash()))) //next 32 is file id + session hash.
+        || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getKey()))) //next 32 is file id + session hash.
     die('Unknown or invalid file. #'.Format::htmlchars($_GET['h']));
 
 $file->download();
diff --git a/scp/attachment.php b/scp/attachment.php
index 44a49d61bd264edef8e54b1c107126a11964aeed..28b9a185b4df488674d9157a13292cefb39bbc60 100644
--- a/scp/attachment.php
+++ b/scp/attachment.php
@@ -23,7 +23,7 @@ if(!$thisstaff || !$_GET['id'] || !$_GET['h']
     die('Unknown attachment!');
 
 //Validate session access hash - we want to make sure the link is FRESH! and the user has access to the parent ticket!!
-$vhash=md5($attachment->getFileId().session_id().$file->getHash());
+$vhash=md5($attachment->getFileId().session_id().strtolower($file->getKey()));
 if(strcasecmp(trim($_GET['h']),$vhash) || !($ticket=$attachment->getTicket()) || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied');
 
 //Download the file..
diff --git a/scp/file.php b/scp/file.php
index c02562eb2a2fb1fe334884f5bd7f66f99800f181..4ccc3b828c04f240de799714319781974a0353bd 100644
--- a/scp/file.php
+++ b/scp/file.php
@@ -23,7 +23,7 @@ $h=trim($_GET['h']);
 //basic checks
 if(!$h  || strlen($h)!=64  //32*2
         || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp(substr($h,-32),md5($file->getId().session_id().$file->getHash()))) //next 32 is file id + session hash.
+        || strcasecmp(substr($h,-32),md5($file->getId().session_id().strtolower($file->getKey())))) //next 32 is file id + session hash.
     die('Unknown or invalid file. #'.Format::htmlchars($_GET['h']));
 
 $file->download();
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 1dbdd3b7794c5ebd17a4ae6d8494d2642fcbcde3..a8592ef317175889f8d7ee6cdd61bcb04a1cd51d 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -317,14 +317,17 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%file`;
 CREATE TABLE `%TABLE_PREFIX%file` (
   `id` int(11) NOT NULL auto_increment,
   `ft` CHAR( 1 ) NOT NULL DEFAULT  'T',
+  `bk` CHAR( 1 ) NOT NULL DEFAULT  'D',
   `type` varchar(255) NOT NULL default '',
   `size` varchar(25) NOT NULL default '',
-  `hash` varchar(125) NOT NULL,
+  `key` varchar(125) collate ascii_bin NOT NULL,
+  `signature` varchar(125) collate ascii_bin NOT NULL,
   `name` varchar(255) NOT NULL default '',
   `created` datetime NOT NULL,
   PRIMARY KEY  (`id`),
   KEY `ft` (`ft`),
-  KEY `hash` (`hash`)
+  KEY `key` (`key`)
+  KEY `signature` (`signature`)
 ) DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%file_chunk`;