diff --git a/include/ajax.upgrader.php b/include/ajax.upgrader.php
index 331d23d26bef97b89eddbf2e80170947a794b72f..d021086ef1a55c84745b741dcebc70df160613d7 100644
--- a/include/ajax.upgrader.php
+++ b/include/ajax.upgrader.php
@@ -32,7 +32,7 @@ class UpgraderAjaxAPI extends AjaxController {
             exit;
         }
 
-        if($upgrader->getNumPendingTasks() && $upgrader->doTasks()) {
+        if($upgrader->getTask() && $upgrader->doTask()) {
             //More pending tasks - doTasks returns the number of pending tasks
             Http::response(200, $upgrader->getNextAction());
             exit;
diff --git a/include/class.migrater.php b/include/class.migrater.php
index f3d6c775dc50240deaf460b7586d961770422aed..b9dbdca2e663c01b9e7f506319367b4a16548175 100644
--- a/include/class.migrater.php
+++ b/include/class.migrater.php
@@ -102,216 +102,67 @@ class DatabaseMigrater {
     }
 }
 
-
-/*
-    AttachmentMigrater
-
-    Attachment migration from file-based attachments in pre-1.7 to
-    database-backed attachments in osTicket v1.7. This class provides the
-    hardware to find and retrieve old attachments and move them into the new
-    database scheme with the data in the actual database.
-
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-*/
-
-include_once(INCLUDE_DIR.'class.file.php');
-
-class AttachmentMigrater {
-
-
-    function AttachmentMigrater() {
-        $this->queue = &$_SESSION['ost_upgrader']['AttachmentMigrater']['queue'];
-        $this->skipList = &$_SESSION['ost_upgrader']['AttachmentMigrater']['skip'];
-        $this->errorList = array();
-    }
-
+class MigrationTask {
+    var $description = "[Unnamed task]";
+    var $status = "finished";
 
     /**
-     * Process the migration for a unit of time. This will be used to
-     * overcome the execution time restriction of PHP. This instance can be
-     * stashed in a session and have this method called periodically to
-     * process another batch of attachments
+     * Function: run
+     *
+     * (Abstract) method which will perform the migration task. The task
+     * does not have to be completed in one invocation; however, if the
+     * run() method is called again, the task should not be restarted from the
+     * beginning. The ::isFinished() method will be used to determine if the
+     * migration task has more work to be done.
      *
-     * Returns:
-     * Number of pending attachments to migrate.
+     * If ::isFinished() returns boolean false, then the run method will be
+     * called again. Note that the next invocation may be in a separate
+     * request. Ensure that you properly capture the state of the task before
+     * exiting the ::run() method. The entire MigrationTask instance is stored
+     * in the migration session, so all instance variables will be preserved
+     * between calls.
+     *
+     * Parameters:
+     * max_time - (int) number of seconds the task should be allowed to run
      */
-    function do_batch($time=30, $max=0) {
-
-        if(!$this->queueAttachments($max) || !$this->getQueueLength())
-            return 0;
-
-        $count = 0;
-        $start = Misc::micro_time();
-        while ($this->getQueueLength() && (Misc::micro_time()-$start) < $time)
-            if($this->next() && $max && ++$count>=$max)
-                break;
-
-        return $this->queueAttachments($max);
-
-    }
+    /* abstract */
+    function run($max_time) { }
 
-    function getSkipList() {
-        return $this->skipList;
-    }
-
-    function enqueue($fileinfo) {
-        $this->queue[] = $fileinfo;
-    }
-
-    function getQueue() {
-        return $this->queue;
-    }
-
-    function getQueueLength() { return count($this->queue); }
     /**
-     * Processes the next item on the work queue. Emits a JSON messages to
-     * indicate current progress.
+     * Function: isFinished
      *
-     * Returns:
-     * TRUE/NULL if the migration was successful
+     * Returns boolean TRUE if another call to ::run() is required, and
+     * false otherwise
      */
-    function next() {
-        # Fetch next item -- use the last item so the array indices don't
-        # need to be recalculated for every shift() operation.
-        $info = array_pop($this->queue);
-        # Attach file to the ticket
-        if (!($info['data'] = @file_get_contents($info['path']))) {
-            # Continue with next file
-            return $this->skip($info['attachId'],
-                sprintf('%s: Cannot read file contents', $info['path']));
-        }
-        # Get the mime/type of each file
-        # XXX: Use finfo_buffer for PHP 5.3+
-        if(function_exists('mime_content_type')) {
-            //XXX: function depreciated in newer versions of PHP!!!!!
-            $info['type'] = mime_content_type($info['path']);
-        } elseif (function_exists('finfo_file')) { // PHP 5.3.0+
-            $finfo = finfo_open(FILEINFO_MIME_TYPE);
-            $info['type'] = finfo_file($finfo, $info['path']);
-        }
-        # TODO: Add extension-based mime-type lookup
+    /* abstract */
+    function isFinished() { return true; }
 
-        if (!($fileId = AttachmentFile::save($info))) {
-            return $this->skip($info['attachId'],
-                sprintf('%s: Unable to migrate attachment', $info['path']));
-        }
-        # Update the ATTACHMENT_TABLE record to set file_id
-        db_query('update '.TICKET_ATTACHMENT_TABLE
-                .' set file_id='.db_input($fileId)
-                .' where attach_id='.db_input($info['attachId']));
-        # Remove disk image of the file. If this fails, the migration for
-        # this file would not be retried, because the file_id in the
-        # TICKET_ATTACHMENT_TABLE has a nonzero value now
-        if (!@unlink($info['path'])) //XXX: what should we do on failure?
-            $this->error(
-                sprintf('%s: Unable to remove file from disk',
-                $info['path']));
-        # TODO: Log an internal note to the ticket?
-        return true;
-    }
     /**
-     * From (class Ticket::fixAttachments), used to detect the locations of
-     * attachment files
+     * Funciton: sleep
+     *
+     * Called if isFinished() returns false. The data returned is passed to
+     * the ::wakeup() method before the ::run() method is called again
      */
-    /* static */ function queueAttachments($limit=0){
-        global $cfg, $ost;
-
-        # Since the queue is persistent - we want to make sure we get to empty
-        # before we find more attachments.
-        if(($qc=$this->getQueueLength()))
-            return $qc;
-
-        $sql='SELECT attach_id, file_name, file_key, Ti.created'
-            .' FROM '.TICKET_ATTACHMENT_TABLE.' TA'
-            .' INNER JOIN '.TICKET_TABLE.' Ti ON (Ti.ticket_id=TA.ticket_id)'
-            .' WHERE NOT file_id ';
-
-        if(($skipList=$this->getSkipList()))
-            $sql.= ' AND attach_id NOT IN('.implode(',', db_input($skipList)).')';
-
-        if($limit && is_numeric($limit))
-            $sql.=' LIMIT '.$limit;
-
-        //XXX: Do a hard fail or error querying the database?
-        if(!($res=db_query($sql)))
-            return $this->error('Unable to query DB for attached files to migrate!');
-
-        $ost->logDebug("Attachment migration", 'Found '.db_num_rows($res).' attachments to migrate');
-        if(!db_num_rows($res))
-            return 0;  //Nothing else to do!!
+    function sleep() { return null; }
 
-        $dir=$cfg->getUploadDir();
-        if(!$dir || !is_dir($dir)) //XXX: Abort the upgrade??? Attachments are obviously critical!
-            return $this->error("Attachment directory [$dir] is invalid - aborting attachment migration");
-
-        //Clear queue
-        $this->queue = array();
-        while (list($id,$name,$key,$created)=db_fetch_row($res)) {
-            $month=date('my',strtotime($created));
-            $info=array(
-                'name'=>        $name,
-                'attachId'=>    $id,
-            );
-            $filename15=sprintf("%s/%s_%s",rtrim($dir,'/'),$key,$name);
-            $filename16=sprintf("%s/%s/%s_%s",rtrim($dir,'/'),$month,$key,$name); //new destination.
-            if (file_exists($filename15)) {
-                $info['path'] = $filename15;
-            } elseif (file_exists($filename16)) {
-                $info['path'] = $filename16;
-            } else {
-                # XXX Cannot find file for attachment
-                $this->skip($id,
-                        sprintf('%s: Unable to locate attachment file',
-                            $name));
-                # No need to further process this file
-                continue;
-            }
-            # TODO: Get the size and mime/type of each file.
-            #
-            # NOTE: If filesize() fails and file_get_contents() doesn't,
-            # then the AttachmentFile::save() method will automatically
-            # estimate the filesize based on the length of the string data
-            # received in $info['data'] -- ie. no need to do that here.
-            #
-            # NOTE: The size is done here because it should be quick to
-            # lookup out of file inode already loaded. The mime/type may
-            # take a while because it will require a second IO to read the
-            # file data.  To ensure this will finish before the
-            # max_execution_time, perform the type match in the ::next()
-            # method since the entire file content will be read there
-            # anyway.
-            $info['size'] = @filesize($info['path']);
-            # Coroutines would be nice ..
-            $this->enqueue($info);
-        }
-
-        return $this->queueAttachments($limit);
-    }
-
-    function skip($attachId, $error) {
-
-        $this->skipList[] = $attachId;
+    /**
+     * Function: wakeup
+     *
+     * Called before the ::run() method if the migration task was saved in
+     * the session and run in multiple requests
+     */
+    function wakeup($data) { }
 
-        return $this->error($error." (ID #$attachId)");
+    function getDescription() {
+        return $this->description;
     }
 
-    function error($what) {
-        global $ost;
-
-        $this->errors++;
-        $this->errorList[] = $what;
-        $ost->logDebug('Upgrader: Attachment Migrater', $what);
-        # Assist in returning FALSE for inline returns with this method
-        return false;
+    function getStatus() {
+        return $this->status;
     }
-    function getErrors() {
-        return $this->errorList;
+    function setStatus($message) {
+        $this->status = $message;
     }
 }
+
 ?>
diff --git a/include/class.upgrader.php b/include/class.upgrader.php
index b09aabe48d43ba70545556ac10b0c4c5c2199d28..e76f9cb1ecb1b42ae1152851d7668dc6b8981d42 100644
--- a/include/class.upgrader.php
+++ b/include/class.upgrader.php
@@ -102,14 +102,8 @@ class Upgrader {
             return $this->getCurrentStream()->check_mysql();
     }
 
-    function getNumPendingTasks() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getNumPendingTasks();
-    }
-
-    function doTasks() {
-        if ($this->getNumPendingTasks())
-            return $this->getCurrentStream()->doTasks();
+    function doTask() {
+        return $this->getCurrentStream()->doTask();
     }
 
     function getErrors() {
@@ -155,6 +149,8 @@ class StreamUpgrader extends SetupWizard {
     var $state;
     var $mode;
 
+    var $phash;
+
     /**
      * Parameters:
      * schema_signature - (string<hash-hex>) Current database-reflected (via
@@ -181,13 +177,12 @@ class StreamUpgrader extends SetupWizard {
         if(!ini_get('safe_mode'))
             set_time_limit(0);
 
-
         //Init the task Manager.
         if(!isset($_SESSION['ost_upgrader'][$this->getShash()]))
-            $_SESSION['ost_upgrader'][$this->getShash()]['tasks']=array();
+            $_SESSION['ost_upgrader']['task'] = array();
 
         //Tasks to perform - saved on the session.
-        $this->tasks = &$_SESSION['ost_upgrader'][$this->getShash()]['tasks'];
+        $this->phash = &$_SESSION['ost_upgrader']['phash'];
 
         //Database migrater
         $this->migrater = null;
@@ -287,10 +282,8 @@ class StreamUpgrader extends SetupWizard {
     function getNextAction() {
 
         $action='Upgrade osTicket to '.$this->getVersion();
-        if($this->getNumPendingTasks() && ($task=$this->getNextTask())) {
-            $action = $task['desc'];
-            if($task['status']) //Progress report...
-                $action.=' ('.$task['status'].')';
+        if($task=$this->getTask()) {
+            $action = $task->getDescription() .' ('.$task->getStatus().')';
         } elseif($this->isUpgradable() && ($nextversion = $this->getNextVersion())) {
             $action = "Upgrade to $nextversion";
         }
@@ -298,77 +291,62 @@ class StreamUpgrader extends SetupWizard {
         return '['.$this->name.'] '.$action;
     }
 
-    function getNumPendingTasks() {
-
-        return count($this->getPendingTasks());
-    }
-
-    function getPendingTasks() {
+    function getPendingTask() {
 
         $pending=array();
-        if(($tasks=$this->getTasks())) {
-            foreach($tasks as $k => $task) {
-                if(!$task['done'])
-                    $pending[$k] = $task;
-            }
-        }
-
-        return $pending;
-    }
+        if ($task=$this->getTask())
+            return ($task->isFinished()) ? 1 : 0;
 
-    function getTasks() {
-       return $this->tasks;
+        return false;
     }
 
-    function getNextTask() {
-
-        if(!($tasks=$this->getPendingTasks()))
+    function getTask() {
+        $task_file = $this->getSQLDir() . "{$this->phash}.task.php";
+        if (!file_exists($task_file))
             return null;
 
-        return current($tasks);
-    }
-
-    function removeTask($tId) {
-
-        if(isset($this->tasks[$tId]))
-            unset($this->tasks[$tId]);
-
-        return (!$this->tasks[$tId]);
-    }
-
-    function setTaskStatus($tId, $status) {
-        if(isset($this->tasks[$tId]))
-            $this->tasks[$tId]['status'] = $status;
+        if (!isset($this->task)) {
+            $class = (include $task_file);
+            if (!is_string($class) || !class_exists($class))
+                return $ost->logError("{$phash}:{$class}: Bogus migration task");
+            $this->task = new $class();
+            if (isset($_SESSION['ost_upgrader']['task'][$this->phash]))
+                $this->task->wakeup($_SESSION['ost_upgrader']['task'][$this->phash]);
+        }
+        return $this->task;
     }
 
-    function doTasks() {
+    function doTask() {
 
         global $ost;
-        if(!($tasks=$this->getPendingTasks()))
-            return true; //Nothing to do.
+        if(!($task = $this->getTask()))
+            return false; //Nothing to do.
 
-        $c = count($tasks);
         $ost->logDebug(
-                sprintf('Upgrader - %s (%d pending tasks).', $this->getShash(), $c),
-                sprintf('There are %d pending upgrade tasks for %s patch', $c, $this->getShash())
+                sprintf('Upgrader - %s (task pending).', $this->getShash()),
+                sprintf('The %s task reports there is work to do',
+                    get_class($task))
                 );
-        $start_time = Misc::micro_time();
-        foreach($tasks as $k => $task) {
-            //TODO: check time used vs. max execution - break if need be
-            if(call_user_func(array($this, $task['func']), $k)===0) {
-                $this->tasks[$k]['done'] = true;
-            } else { //Task has pending items to process.
-                break;
-            }
-        }
+        if(!($max_time = ini_get('max_execution_time')))
+            $max_time = 30; //Default to 30 sec batches.
 
-        return $this->getPendingTasks();
+        $task->run($max_time);
+        if (!$task->isFinished()) {
+            $_SESSION['ost_upgrader']['task'][$this->phash] = $task->sleep();
+            return true;
+        }
+        // Run the cleanup script, if any, and destroy the task's session
+        // data
+        $this->cleanup();
+        unset($_SESSION['ost_upgrader']['task'][$this->phash]);
+        unset($this->phash);
+        return false;
     }
 
     function upgrade() {
         global $ost;
 
-        if($this->getPendingTasks() || !($patches=$this->getPatches()))
+        if($this->getPendingTask() || !($patches=$this->getPatches()))
             return false;
 
         $start_time = Misc::micro_time();
@@ -394,19 +372,20 @@ class StreamUpgrader extends SetupWizard {
 
             $ost->logDebug("Upgrader - $shash applied", $logMsg);
             $this->signature = $shash; //Update signature to the *new* HEAD
+            $this->phash = $phash;
 
-            //Check if the said patch has scripted tasks
-            if(!($tasks=$this->getTasksForPatch($phash))) {
-                //Break IF elapsed time is greater than 80% max time allowed.
-                if(($elapsedtime=(Misc::micro_time()-$start_time)) && $max_time && $elapsedtime>($max_time*0.80))
+            //Break IF elapsed time is greater than 80% max time allowed.
+            if (!($task=$this->getTask())) {
+                $this->cleanup();
+                if (($elapsedtime=(Misc::micro_time()-$start_time))
+                        && $max_time && $elapsedtime>($max_time*0.80))
                     break;
-
-                continue;
-
+                else
+                    // Apply the next patch
+                    continue;
             }
 
             //We have work to do... set the tasks and break.
-            $_SESSION['ost_upgrader'][$shash]['tasks'] = $tasks;
             $_SESSION['ost_upgrader'][$shash]['state'] = 'upgrade';
             break;
         }
@@ -415,47 +394,13 @@ class StreamUpgrader extends SetupWizard {
         $this->migrater = null;
 
         return true;
-
-    }
-
-    function getTasksForPatch($phash) {
-
-        $tasks=array();
-        switch($phash) { //Add  patch specific scripted tasks.
-            case 'c00511c7-7be60a84': //V1.6 ST- 1.7 * {{MD5('1.6 ST') -> c00511c7c1db65c0cfad04b4842afc57}}
-                $tasks[] = array('func' => 'migrateSessionFile2DB',
-                                 'desc' => 'Transitioning to db-backed sessions');
-                break;
-            case '98ae1ed2-e342f869': //v1.6 RC1-4 -> v1.6 RC5
-                $tasks[] = array('func' => 'migrateAPIKeys',
-                                 'desc' => 'Migrating API keys to a new table');
-                break;
-            case '435c62c3-2e7531a2':
-                $tasks[] = array('func' => 'migrateGroupDeptAccess',
-                                 'desc' => 'Migrating group\'s department access to a new table');
-                break;
-            case '15b30765-dd0022fb':
-                $tasks[] = array('func' => 'migrateAttachments2DB',
-                                 'desc' => 'Migrating attachments to database, it might take a while depending on the number of files.');
-                break;
-        }
-
-        //Check IF SQL cleanup exists.
-        $file=$this->getSQLDir().$phash.'.cleanup.sql';
-        if(file_exists($file))
-            $tasks[] = array('func' => 'cleanup',
-                             'desc' => 'Post-upgrade cleanup!',
-                             'phash' => $phash);
-
-        return $tasks;
     }
 
     /************* TASKS **********************/
-    function cleanup($taskId) {
+    function cleanup() {
         global $ost;
 
-        $phash = $this->tasks[$taskId]['phash'];
-        $file=$this->getSQLDir().$phash.'.cleanup.sql';
+        $file = $this->getSQLDir().$this->phash.'.cleanup.sql';
 
         if(!file_exists($file)) //No cleanup script.
             return 0;
@@ -468,64 +413,5 @@ class StreamUpgrader extends SetupWizard {
                         $phash));
         return 0;
     }
-
-    function migrateAttachments2DB($taskId) {
-        global $ost;
-
-        if(!($max_time = ini_get('max_execution_time')))
-            $max_time = 30; //Default to 30 sec batches.
-
-        $att_migrater = new AttachmentMigrater();
-        if($att_migrater->do_batch(($max_time*0.9), 100)===0)
-            return 0;
-
-        return $att_migrater->getQueueLength();
-    }
-
-    function migrateSessionFile2DB($taskId) {
-        # How about 'dis for a hack?
-        osTicketSession::write(session_id(), session_encode());
-        return 0;
-    }
-
-    function migrateAPIKeys($taskId) {
-
-        $res = db_query('SELECT api_whitelist, api_key FROM '.CONFIG_TABLE.' WHERE id=1');
-        if(!$res || !db_num_rows($res))
-            return 0;  //Reporting success.
-
-        list($whitelist, $key) = db_fetch_row($res);
-
-        $ips=array_filter(array_map('trim', explode(',', $whitelist)));
-        foreach($ips as $ip) {
-            $sql='INSERT INTO '.API_KEY_TABLE.' SET created=NOW(), updated=NOW(), isactive=1 '
-                .',ipaddr='.db_input($ip)
-                .',apikey='.db_input(strtoupper(md5($ip.md5($key))));
-            db_query($sql);
-        }
-
-        return 0;
-    }
-
-    function migrateGroupDeptAccess($taskId) {
-
-        $res = db_query('SELECT group_id, dept_access FROM '.GROUP_TABLE);
-        if(!$res || !db_num_rows($res))
-            return 0;  //No groups??
-
-        while(list($groupId, $access) = db_fetch_row($res)) {
-            $depts=array_filter(array_map('trim', explode(',', $access)));
-            foreach($depts as $deptId) {
-                $sql='INSERT INTO '.GROUP_DEPT_TABLE
-                    .' SET dept_id='.db_input($deptId).', group_id='.db_input($groupId);
-                db_query($sql);
-            }
-        }
-
-        return 0;
-
-
-
-    }
 }
 ?>
diff --git a/include/upgrader/streams/core/15b30765-dd0022fb.task.php b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e2739218ad0e9499fafc14294df6fefd5c4b1e9
--- /dev/null
+++ b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
@@ -0,0 +1,230 @@
+<?php
+/*********************************************************************
+    AttachmentMigrater
+
+    Attachment migration from file-based attachments in pre-1.7 to
+    database-backed attachments in osTicket v1.7. This class provides the
+    hardware to find and retrieve old attachments and move them into the new
+    database scheme with the data in the actual database.
+
+    Copyright (c)  2006-2013 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require_once INCLUDE_DIR.'class.migrater.php';
+require_once(INCLUDE_DIR.'class.file.php');
+
+class AttachmentMigrater extends MigrationTask {
+    var $description = "Attachment migration from disk to database";
+
+    var $queue;
+    var $skipList;
+    var $errorList = array();
+
+    function sleep() {
+        return array('queue'=>$this->queue, 'skipList'=>$this->skipList);
+    }
+    function wakeup($stuff) {
+        $this->queue = $stuff['queue'];
+        $this->skipList = $stuff['skipList'];
+    }
+
+    function run($max_time) {
+        $this->do_batch($max_time * 0.9, 100);
+    }
+
+    function isFinished() {
+        return $this->getQueueLength() == 0;
+    }
+
+    /**
+     * Process the migration for a unit of time. This will be used to
+     * overcome the execution time restriction of PHP. This instance can be
+     * stashed in a session and have this method called periodically to
+     * process another batch of attachments
+     *
+     * Returns:
+     * Number of pending attachments to migrate.
+     */
+    function do_batch($time=30, $max=0) {
+
+        if(!$this->queueAttachments($max) || !$this->getQueueLength())
+            return 0;
+
+        $this->setStatus("{$this->getQueueLength()} attachments remaining");
+
+        $count = 0;
+        $start = Misc::micro_time();
+        while ($this->getQueueLength() && (Misc::micro_time()-$start) < $time)
+            if($this->next() && $max && ++$count>=$max)
+                break;
+
+        return $this->queueAttachments($max);
+
+    }
+
+    function getSkipList() {
+        return $this->skipList;
+    }
+
+    function enqueue($fileinfo) {
+        $this->queue[] = $fileinfo;
+    }
+
+    function getQueue() {
+        return $this->queue;
+    }
+
+    function getQueueLength() { return count($this->queue); }
+    /**
+     * Processes the next item on the work queue. Emits a JSON messages to
+     * indicate current progress.
+     *
+     * Returns:
+     * TRUE/NULL if the migration was successful
+     */
+    function next() {
+        # Fetch next item -- use the last item so the array indices don't
+        # need to be recalculated for every shift() operation.
+        $info = array_pop($this->queue);
+        # Attach file to the ticket
+        if (!($info['data'] = @file_get_contents($info['path']))) {
+            # Continue with next file
+            return $this->skip($info['attachId'],
+                sprintf('%s: Cannot read file contents', $info['path']));
+        }
+        # Get the mime/type of each file
+        # XXX: Use finfo_buffer for PHP 5.3+
+        if(function_exists('mime_content_type')) {
+            //XXX: function depreciated in newer versions of PHP!!!!!
+            $info['type'] = mime_content_type($info['path']);
+        } elseif (function_exists('finfo_file')) { // PHP 5.3.0+
+            $finfo = finfo_open(FILEINFO_MIME_TYPE);
+            $info['type'] = finfo_file($finfo, $info['path']);
+        }
+        # TODO: Add extension-based mime-type lookup
+
+        if (!($fileId = AttachmentFile::save($info))) {
+            return $this->skip($info['attachId'],
+                sprintf('%s: Unable to migrate attachment', $info['path']));
+        }
+        # Update the ATTACHMENT_TABLE record to set file_id
+        db_query('update '.TICKET_ATTACHMENT_TABLE
+                .' set file_id='.db_input($fileId)
+                .' where attach_id='.db_input($info['attachId']));
+        # Remove disk image of the file. If this fails, the migration for
+        # this file would not be retried, because the file_id in the
+        # TICKET_ATTACHMENT_TABLE has a nonzero value now
+        if (!@unlink($info['path'])) //XXX: what should we do on failure?
+            $this->error(
+                sprintf('%s: Unable to remove file from disk',
+                $info['path']));
+        # TODO: Log an internal note to the ticket?
+        return true;
+    }
+    /**
+     * From (class Ticket::fixAttachments), used to detect the locations of
+     * attachment files
+     */
+    /* static */ function queueAttachments($limit=0){
+        global $cfg, $ost;
+
+        # Since the queue is persistent - we want to make sure we get to empty
+        # before we find more attachments.
+        if(($qc=$this->getQueueLength()))
+            return $qc;
+
+        $sql='SELECT attach_id, file_name, file_key, Ti.created'
+            .' FROM '.TICKET_ATTACHMENT_TABLE.' TA'
+            .' INNER JOIN '.TICKET_TABLE.' Ti ON (Ti.ticket_id=TA.ticket_id)'
+            .' WHERE NOT file_id ';
+
+        if(($skipList=$this->getSkipList()))
+            $sql.= ' AND attach_id NOT IN('.implode(',', db_input($skipList)).')';
+
+        if($limit && is_numeric($limit))
+            $sql.=' LIMIT '.$limit;
+
+        //XXX: Do a hard fail or error querying the database?
+        if(!($res=db_query($sql)))
+            return $this->error('Unable to query DB for attached files to migrate!');
+
+        $ost->logDebug("Attachment migration", 'Found '.db_num_rows($res).' attachments to migrate');
+        if(!db_num_rows($res))
+            return 0;  //Nothing else to do!!
+
+        $dir=$cfg->getUploadDir();
+        if(!$dir || !is_dir($dir)) //XXX: Abort the upgrade??? Attachments are obviously critical!
+            return $this->error("Attachment directory [$dir] is invalid - aborting attachment migration");
+
+        //Clear queue
+        $this->queue = array();
+        while (list($id,$name,$key,$created)=db_fetch_row($res)) {
+            $month=date('my',strtotime($created));
+            $info=array(
+                'name'=>        $name,
+                'attachId'=>    $id,
+            );
+            $filename15=sprintf("%s/%s_%s",rtrim($dir,'/'),$key,$name);
+            $filename16=sprintf("%s/%s/%s_%s",rtrim($dir,'/'),$month,$key,$name); //new destination.
+            if (file_exists($filename15)) {
+                $info['path'] = $filename15;
+            } elseif (file_exists($filename16)) {
+                $info['path'] = $filename16;
+            } else {
+                # XXX Cannot find file for attachment
+                $this->skip($id,
+                        sprintf('%s: Unable to locate attachment file',
+                            $name));
+                # No need to further process this file
+                continue;
+            }
+            # TODO: Get the size and mime/type of each file.
+            #
+            # NOTE: If filesize() fails and file_get_contents() doesn't,
+            # then the AttachmentFile::save() method will automatically
+            # estimate the filesize based on the length of the string data
+            # received in $info['data'] -- ie. no need to do that here.
+            #
+            # NOTE: The size is done here because it should be quick to
+            # lookup out of file inode already loaded. The mime/type may
+            # take a while because it will require a second IO to read the
+            # file data.  To ensure this will finish before the
+            # max_execution_time, perform the type match in the ::next()
+            # method since the entire file content will be read there
+            # anyway.
+            $info['size'] = @filesize($info['path']);
+            # Coroutines would be nice ..
+            $this->enqueue($info);
+        }
+
+        return $this->queueAttachments($limit);
+    }
+
+    function skip($attachId, $error) {
+
+        $this->skipList[] = $attachId;
+
+        return $this->error($error." (ID #$attachId)");
+    }
+
+    function error($what) {
+        global $ost;
+
+        $this->errors++;
+        $this->errorList[] = $what;
+        $ost->logDebug('Upgrader: Attachment Migrater', $what);
+        # Assist in returning FALSE for inline returns with this method
+        return false;
+    }
+    function getErrors() {
+        return $this->errorList;
+    }
+}
+
+return 'AttachmentMigrater';
+?>
diff --git a/include/upgrader/streams/core/435c62c3-2e7531a2.task.php b/include/upgrader/streams/core/435c62c3-2e7531a2.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..d80bc23a2e616248409c3c3780e3eedfca975799
--- /dev/null
+++ b/include/upgrader/streams/core/435c62c3-2e7531a2.task.php
@@ -0,0 +1,26 @@
+<?php
+require_once INCLUDE_DIR.'class.migrater.php';
+
+class MigrateGroupDeptAccess extends MigrationTask {
+    var $description = "Migrate department access for groups from v1.6";
+
+    function run($max_time) {
+        $this->setStatus("Migrating department access");
+
+        $res = db_query('SELECT group_id, dept_access FROM '.GROUP_TABLE);
+        if(!$res || !db_num_rows($res))
+            return false;  //No groups??
+
+        while(list($groupId, $access) = db_fetch_row($res)) {
+            $depts=array_filter(array_map('trim', explode(',', $access)));
+            foreach($depts as $deptId) {
+                $sql='INSERT INTO '.GROUP_DEPT_TABLE
+                    .' SET dept_id='.db_input($deptId).', group_id='.db_input($groupId);
+                db_query($sql);
+            }
+        }
+    }
+}
+
+return 'MigrateGroupDeptAccess';
+?>
diff --git a/include/upgrader/streams/core/98ae1ed2-e342f869.task.php b/include/upgrader/streams/core/98ae1ed2-e342f869.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..e479410b55fcbc2662ecab48a95556dc7a1a9cbf
--- /dev/null
+++ b/include/upgrader/streams/core/98ae1ed2-e342f869.task.php
@@ -0,0 +1,25 @@
+<?php
+require_once INCLUDE_DIR.'class.migrater.php';
+
+class APIKeyMigrater extends MigrationTask {
+    var $description = "Migrating v1.6 API keys";
+
+    function run() {
+        $res = db_query('SELECT api_whitelist, api_key FROM '.CONFIG_TABLE.' WHERE id=1');
+        if(!$res || !db_num_rows($res))
+            return 0;  //Reporting success.
+
+        list($whitelist, $key) = db_fetch_row($res);
+
+        $ips=array_filter(array_map('trim', explode(',', $whitelist)));
+        foreach($ips as $ip) {
+            $sql='INSERT INTO '.API_KEY_TABLE.' SET created=NOW(), updated=NOW(), isactive=1 '
+                .',ipaddr='.db_input($ip)
+                .',apikey='.db_input(strtoupper(md5($ip.md5($key))));
+            db_query($sql);
+        }
+    }
+}
+
+return 'APIKeyMigrater';
+?>
diff --git a/include/upgrader/streams/core/c00511c7-7be60a84.task.php b/include/upgrader/streams/core/c00511c7-7be60a84.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..eca25aee2c623a13a956d2d7df13843ba4b7bee0
--- /dev/null
+++ b/include/upgrader/streams/core/c00511c7-7be60a84.task.php
@@ -0,0 +1,14 @@
+<?php
+require_once INCLUDE_DIR.'class.migrater.php';
+
+class MigrateDbSession extends MigrationTask {
+    var $description = "Migrate to database-backed sessions";
+
+    function run() {
+        # How about 'dis for a hack?
+        osTicketSession::write(session_id(), session_encode());
+    }
+}
+
+return 'MigrateDbSession';
+?>