diff --git a/include/class.file.php b/include/class.file.php
index 9c8eb9c53da2a4c450421c4e8261c1efcc511311..cc90630666e59c831f144bb5743ed5f41611e74a 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -386,7 +386,7 @@ class AttachmentFile {
         $after = hash_init('sha1');
 
         // Copy the file to the new backend and hash the contents
-        $target = AttachmentStorageBackend::lookup($bk, $this->ht);
+        $target = FileStorageBackend::lookup($bk, $this);
         $source = $this->open();
         // TODO: Make this resumable so that if the file cannot be migrated
         //      in the max_execution_time, the migration can be continued
@@ -398,7 +398,7 @@ class AttachmentFile {
 
         // Verify that the hash of the target file matches the hash of the
         // source file
-        $target = AttachmentStorageBackend::lookup($bk, $this->ht);
+        $target = FileStorageBackend::lookup($bk, $this);
         while ($block = $target->read())
             hash_update($after, $block);
 
@@ -575,6 +575,10 @@ class FileStorageBackend {
                 return $tc;
     }
 
+    static function isRegistered($type) {
+        return isset(self::$registry[$type]);
+    }
+
     static function lookup($type, $file=null) {
         if (!isset(self::$registry[$type]))
             throw new Exception("No such backend registered");
diff --git a/include/class.orm.php b/include/class.orm.php
index dee287cea77adc73d323db573e79a83151a998de..17f37679dca6726c3cbc86816401d3d4d4030541 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -693,6 +693,8 @@ class MySqlCompiler extends SqlCompiler {
         'contains' => array('self', '__contains'),
         'gt' => '%1$s > %2$s',
         'lt' => '%1$s < %2$s',
+        'gte' => '%1$s >= %2$s',
+        'lte' => '%1$s <= %2$s',
         'isnull' => '%1$s IS NULL',
         'like' => '%1$s LIKE %2$s',
         'in' => array('self', '__in'),
diff --git a/setup/cli/modules/class.module.php b/setup/cli/modules/class.module.php
index 421e49bd11a22f115af0e9d3c288099f07859e3f..a1647ce3cac8d2be9a57e29ab0fe60fe1eb82ce5 100644
--- a/setup/cli/modules/class.module.php
+++ b/setup/cli/modules/class.module.php
@@ -157,8 +157,18 @@ class Module {
         if ($this->arguments) {
             echo "\nArguments:\n";
             foreach ($this->arguments as $name=>$help)
+                $extra = '';
+                if (is_array($help)) {
+                    if (isset($help['options']) && is_array($help['options'])) {
+                        foreach($help['options'] as $op=>$desc)
+                            $extra .= wordwrap(
+                                "\n        $op - $desc", 76, "\n            ");
+                    }
+                    $help = $help['help'];
+                }
                 echo $name . "\n    " . wordwrap(
-                    preg_replace('/\s+/', ' ', $help), 76, "\n    ");
+                    preg_replace('/\s+/', ' ', $help), 76, "\n    ")
+                        .$extra."\n";
         }
 
         if ($this->epilog) {
@@ -198,6 +208,10 @@ class Module {
         foreach (array_keys($this->arguments) as $idx=>$name)
             if (!isset($this->_args[$idx]))
                 $this->optionError($name . " is a required argument");
+            elseif (is_array($this->arguments[$name])
+                    && isset($this->arguments[$name]['options'])
+                    && !isset($this->arguments[$name]['options'][$this->_args[$idx]]))
+                $this->optionError($name . " does not support such a value");
             else
                 $this->_args[$name] = &$this->_args[$idx];
 
diff --git a/setup/cli/modules/file.php b/setup/cli/modules/file.php
new file mode 100644
index 0000000000000000000000000000000000000000..3c9f434b745aee1e50dd3764136510cbfa4e1e47
--- /dev/null
+++ b/setup/cli/modules/file.php
@@ -0,0 +1,200 @@
+<?php
+require_once dirname(__file__) . "/class.module.php";
+require_once dirname(__file__) . "/../cli.inc.php";
+
+class FileManager extends Module {
+    var $prologue = 'CLI file manager for osTicket';
+
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'list' => 'List files matching criteria',
+                'export' => 'Export files from the system',
+                'dump' => 'Dump file content to stdout',
+                'migrate' => 'Migrate a file to another backend',
+                'backends' => 'List configured storage backends',
+            ),
+        ),
+    );
+
+    var $options = array(
+        'ticket' => array('-T', '--ticket', 'metavar'=>'id',
+            'help' => 'Search by internal ticket id'),
+        'file' => array('-F', '--file', 'metavar'=>'id',
+            'help' => 'Search by file id'),
+        'name' => array('-N', '--name', 'metavar'=>'name',
+            'help' => 'Search by file name (subsring match)'),
+        'backend' => array('-b', '--backend', 'metavar'=>'BK',
+            'help' => 'Search by file backend. See `backends` action
+                for a list of available backends'),
+        'status' => array('-S', '--status', 'metavar'=>'STATUS',
+            'help' => 'Search on ticket state (`open` or `closed`)'),
+        'min-size' => array('-z', '--min-size', 'metavar'=>'SIZE',
+            'help' => 'Search for files larger than this. k, M, G are welcome'),
+        'max-size' => array('-Z', '--max-size', 'metavar'=>'SIZE',
+            'help' => 'Search for files smaller than this. k, M, G are welcome'),
+
+        'limit' => array('-L', '--limit', 'metavar'=>'count',
+            'help' => 'Limit search results to this count'),
+
+        'to' => array('-m', '--to', 'metavar'=>'BK',
+            'help' => 'Target backend for migration. See `backends` action
+                for a list of available backends'),
+
+        'verbose' => array('-v', '--verbose', 'action'=>'store_true',
+            'help' => 'Be more verbose'),
+    );
+
+
+    function run($args, $options) {
+        Bootstrap::connect();
+        osTicket::start();
+
+        switch ($args['action']) {
+        case 'backends':
+            // List configured backends
+            foreach (FileStorageBackend::allRegistered() as $char=>$bk) {
+                print "$char -- {$bk::$desc} ($bk)\n";
+            }
+            break;
+
+        case 'list':
+            // List files matching criteria
+            // ORM would be nice!
+            $files = FileModel::objects();
+            $this->_applyCriteria($options, $files);
+            foreach ($files as $f) {
+                printf("% 5d %s % 8d %s % 12s %s\n", $f->id, $f->bk,
+                    $f->size, $f->created, $f->type, $f->name);
+            }
+            break;
+
+        case 'dump':
+            $files = FileModel::objects();
+            $this->_applyCriteria($options, $files);
+            if ($files->count() != 1)
+                $this->fail('Criteria must select exactly 1 file');
+
+            $f = AttachmentFile::lookup($files[0]->id);
+            $f->sendData();
+            break;
+
+        case 'migrate':
+            if (!$options['to'])
+                $this->fail('Please specify a target backend for migration');
+
+            if (!FileStorageBackend::isRegistered($options['to']))
+                $this->fail('Target backend is not installed. See `backends` action');
+
+            $files = FileModel::objects();
+            $this->_applyCriteria($options, $files);
+
+            $count = 0;
+            foreach ($files as $m) {
+                $f = AttachmentFile::lookup($m->id);
+                if ($f->getBackend() == $options['to'])
+                    continue;
+                if ($options['verbose'])
+                    $this->stdout->write('Migrating '.$m->name."\n");
+                try {
+                    if (!$f->migrate($options['to']))
+                        $this->stderr->write('Unable to migrate '.$m->name."\n");
+                    else
+                        $count++;
+                }
+                catch (IOException $e) {
+                    $this->stderr->write('IOError: '.$e->getMessage());
+                }
+            }
+            $this->stdout->write("Migrated $count files\n");
+            break;
+        }
+
+
+    }
+
+    function _applyCriteria($options, $qs) {
+        foreach ($options as $name=>$val) {
+            if (!$val) continue;
+            switch ($name) {
+            case 'ticket':
+                $qs->filter(array('tickets__ticket_id'=>$val));
+                break;
+            case 'file':
+                $qs->filter(array('id'=>$val));
+                break;
+            case 'name':
+                $qs->filter(array('name__contains'=>$val));
+                break;
+            case 'backend':
+                $qs->filter(array('bk'=>$val));
+                break;
+            case 'status':
+                if (!in_array($val, array('open','closed')))
+                    $this->fail($val.': Unknown ticket status');
+
+                $qs->filter(array('tickets__ticket__status'=>$val));
+                break;
+
+            case 'min-size':
+            case 'max-size':
+                $info = array();
+                if (!preg_match('/([\d.]+)([kmgbi]+)?/i', $val, $info))
+                    $this->fail($val.': Invalid file size');
+                if ($info[2]) {
+                    $info[2] = str_replace(array('b','i'), array('',''), $info[2]);
+                    $sizes = array('k'=>1<<10,'m'=>1<<20,'g'=>1<<30);
+                    $val = (float) $val * $sizes[strtolower($info[2])];
+                }
+                if ($name == 'min-size')
+                    $qs->filter(array('size__gte'=>$val));
+                else
+                    $qs->filter(array('size__lte'=>$val));
+                break;
+
+            case 'limit':
+                if (!is_numeric($val))
+                    $this->fail('Provide an result count number to --limit');
+                $qs->limit($val);
+                break;
+            }
+        }
+    }
+}
+
+require_once INCLUDE_DIR . 'class.orm.php';
+
+class FileModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILE_TABLE,
+        'pk' => 'id',
+        'joins' => array(
+            'tickets' => array(
+                'null' => true,
+                'constraint' => array('id' => 'TicketAttachmentModel.file_id')
+            ),
+        ),
+    );
+}
+class TicketAttachmentModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_ATTACHMENT_TABLE,
+        'pk' => 'attach_id',
+        'joins' => array(
+            'ticket' => array(
+                'null' => false,
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+        ),
+    );
+}
+class TicketModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_TABLE,
+        'pk' => 'ticket_id',
+    );
+}
+
+Module::register('file', 'FileManager');
+?>