diff --git a/CHANGELOG.md b/CHANGELOG.md index 67cf0514..143e773e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # PrivateBin version history * **1.5.1 (not yet released)** + * ADDED: script for administrative tasks: deleting pastes (#274), removing empty directories (#277), purging expired pastes (#276) & statistics (#319) * FIXED: Revert Filesystem purge to limited and randomized lookup (#1030) * FIXED: Catch JSON decode errors when invalid data gets sent to the API (#1030) * FIXED: Support sorting v1 format in mixed version comments in Filesystem backend (#1030) diff --git a/bin/administration b/bin/administration new file mode 100755 index 00000000..bc82b438 --- /dev/null +++ b/bin/administration @@ -0,0 +1,318 @@ +#!/usr/bin/env php +_store->exists($pasteId)) { + self::_error('given ID does not exist, has expired or was already deleted', 6); + } + $this->_store->delete($pasteId); + if ($this->_store->exists($pasteId)) { + self::_error('paste ID exists after deletion, permission problem?', 7); + } + exit("paste $pasteId successfully deleted" . PHP_EOL); + } + + /** + * removes empty directories, if current storage model uses Filesystem + * + * @access private + */ + private function _empty_dirs() + { + if ($this->_conf->getKey('class', 'model') !== 'Filesystem') { + self::_error('instance not using Filesystem storage, no directories to empty', 4); + } + $dir = $this->_conf->getKey('dir', 'model_options'); + passthru("find $dir -type d -empty -delete", $code); + exit($code); + } + + /** + * display a message on STDERR and exits + * + * @access private + * @static + * @param string $message + * @param int $code optional, defaults to 1 + */ + private static function _error($message, $code = 1) + { + self::_error_echo($message); + exit($code); + } + + /** + * display a message on STDERR + * + * @access private + * @static + * @param string $message + */ + private static function _error_echo($message) + { + fwrite(STDERR, 'Error: ' . $message . PHP_EOL); + } + + /** + * display usage help on STDOUT and exits + * + * @access private + * @static + * @param int $code optional, defaults to 0 + */ + private static function _help($code = 0) + { + echo <<<'EOT' +Usage: + administration [--delete | --empty-dirs | --help | --purge | --statistics] + +Options: + -d, --delete deletes the requested paste ID + -e, --empty-dirs removes empty directories (only if Filesystem storage is + configured) + -h, --help displays this help message + -p, --purge purge all expired pastes + -s, --statistics reads all stored pastes and comments and reports statistics +EOT, PHP_EOL; + exit($code); + } + + /** + * return option for given short or long keyname, if it got set + * + * @access private + * @static + * @param string $short + * @param string $long + * @return string|null + */ + private function _option($short, $long) + { + foreach (array($short, $long) as $key) { + if (array_key_exists($key, $this->_opts)) { + return $this->_opts[$key]; + } + } + return null; + } + + /** + * initialize options from given argument array + * + * @access private + * @static + * @param array $arguments + */ + private function _options_initialize($arguments) + { + if ($arguments > 3) { + self::_error_echo('too many arguments given'); + echo PHP_EOL; + self::_help(1); + } + + if ($arguments < 2) { + self::_error_echo('missing arguments'); + echo PHP_EOL; + self::_help(2); + } + + $this->_opts = getopt('hd:eps', array('help', 'delete:', 'empty-dirs', 'purge', 'statistics')); + if (!$this->_opts) { + self::_error_echo('unsupported arguments given'); + echo PHP_EOL; + self::_help(3); + } + } + + /** + * reads all stored pastes and comments and reports statistics + * + * @access public + */ + private function _statistics() + { + $counters = array( + 'burn' => 0, + 'discussion' => 0, + 'expired' => 0, + 'md' => 0, + 'percent' => 1, + 'plain' => 0, + 'progress' => 0, + 'syntax' => 0, + 'total' => 0, + 'unknown' => 0, + ); + $time = time(); + $ids = $this->_store->getAllPastes(); + $counters['total'] = count($ids); + $dots = $counters['total'] < 100 ? 10 : ( + $counters['total'] < 1000 ? 50 : 100 + ); + $percentages = $counters['total'] < 100 ? 0 : ( + $counters['total'] < 1000 ? 4 : 10 + ); + + echo "Total:\t\t\t${counters['total']}", PHP_EOL; + foreach ($ids as $pasteid) { + $paste = $this->_store->read($pasteid); + ++$counters['progress']; + + if ( + array_key_exists('expire_date', $paste['meta']) && + $paste['meta']['expire_date'] < $time + ) { + ++$counters['expired']; + } + + if (array_key_exists('adata', $paste)) { + $format = $paste['adata'][1]; + $discussion = $paste['adata'][2]; + $burn = $paste['adata'][3]; + } else { + $format = array_key_exists('formatter', $paste['meta']) ? $paste['meta']['formatter'] : 'plaintext'; + $discussion = array_key_exists('opendiscussion', $paste['meta']) ? $paste['meta']['opendiscussion'] : false; + $burn = array_key_exists('burnafterreading', $paste['meta']) ? $paste['meta']['burnafterreading'] : false; + } + + if ($format === 'plaintext') { + ++$counters['plain']; + } elseif ($format === 'syntaxhighlighting') { + ++$counters['syntax']; + } elseif ($format === 'markdown') { + ++$counters['md']; + } else { + ++$counters['unknown']; + } + + $counters['discussion'] += (int) $discussion; + $counters['burn'] += (int) $burn; + + // display progress + if ($counters['progress'] % $dots === 0) { + echo '.'; + if ($percentages) { + $progress = $percentages / $counters['total'] * $counters['progress']; + if ($progress >= $counters['percent']) { + printf(' %d%% ', 100 / $percentages * $progress); + ++$counters['percent']; + } + } + } + } + + echo PHP_EOL, << 0) { + echo "Unknown format:\t\t${counters['unknown']}", PHP_EOL; + } + } + + /** + * constructor + * + * initializes and runs administrative tasks + * + * @access public + */ + public function __construct() + { + $this->_options_initialize($_SERVER['argc']); + + if ($this->_option('h', 'help') !== null) { + self::_help(); + } + + $this->_conf = new Configuration; + + if ($this->_option('e', 'empty-dirs') !== null) { + $this->_empty_dirs(); + } + + $class = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model'); + $this->_store = new $class($this->_conf->getSection('model_options')); + + if (($pasteId = $this->_option('d', 'delete')) !== null) { + $this->_delete($pasteId); + } + + if ($this->_option('p', 'purge') !== null) { + $this->_store->purge(PHP_INT_MAX); + exit('purging of expired pastes concluded' . PHP_EOL); + } + + if ($this->_option('s', 'statistics') !== null) { + $this->_statistics(); + } + } +} + +new Administration(); \ No newline at end of file diff --git a/bin/migrate b/bin/migrate index 515a5e80..76539ab6 100755 --- a/bin/migrate +++ b/bin/migrate @@ -17,13 +17,14 @@ if (version_compare(PHP_VERSION, '7.1.0') < 0) { $longopts = array( "delete-after", - "delete-during" + "delete-during", + "help" ); $opts_arr = getopt("fhnv", $longopts, $rest); if ($opts_arr === false) { - dieerr("Erroneous command line options. Please use -h"); + dieerr("Erroneous command line options. Please use --help"); } -if (array_key_exists("h", $opts_arr)) { +if (array_key_exists("h", $opts_arr) || array_key_exists("help", $opts_arr)) { helpexit(); } @@ -173,12 +174,12 @@ function debug ($text) { function helpexit () { - print("migrate.php - Copy data between PrivateBin backends + print("migrate - Copy data between PrivateBin backends Usage: migrate [--delete-after] [--delete-during] [-f] [-n] [-v] srcconfdir [] - migrate [-h] + migrate [-h|--help] Options: --delete-after delete data from source after all pastes and comments have @@ -187,6 +188,7 @@ Options: comments have successfully been copied to the destination -f forcefully overwrite data which already exists at the destination + -h, --help displays this help message -n dry run, do not copy data -v be verbose use storage backend configration from conf.php found in