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'); +?>