From 1281ebff03bc808606c5602c781f14e30264c26d Mon Sep 17 00:00:00 2001 From: Aerex Date: Sun, 3 May 2020 16:41:59 -0500 Subject: [PATCH] feat: Initial commit --- .gitignore | 3 + composer.json | 14 ++- lib/AbstractConsole.php | 12 ++ lib/Configs/ConfigBuilder.php | 47 ++++++++ lib/Configs/TaskwarriorConfig.php | 17 +++ lib/Configs/Todotxt.php | 21 ++++ lib/Console.php | 39 +++++++ lib/Plugin.php | 156 ++++++++++++++++++++++++++ lib/StorageManager.php | 57 ++++++++++ lib/Storages/IStorage.php | 12 ++ lib/Storages/Taskwarrior.php | 89 +++++++++++++++ lib/Storages/Todotxt.php | 88 +++++++++++++++ phpunit.xml.dist | 29 +++++ tests/Fixtures/taskwarrior_config.yml | 2 + tests/StorageManagerTest.php | 71 ++++++++++++ 15 files changed, 652 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 lib/AbstractConsole.php create mode 100644 lib/Configs/ConfigBuilder.php create mode 100644 lib/Configs/TaskwarriorConfig.php create mode 100644 lib/Configs/Todotxt.php create mode 100644 lib/Console.php create mode 100644 lib/Plugin.php create mode 100644 lib/StorageManager.php create mode 100644 lib/Storages/IStorage.php create mode 100644 lib/Storages/Taskwarrior.php create mode 100644 lib/Storages/Todotxt.php create mode 100644 phpunit.xml.dist create mode 100644 tests/Fixtures/taskwarrior_config.yml create mode 100644 tests/StorageManagerTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..678f3ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +main.uml +composer.lock diff --git a/composer.json b/composer.json index 25b0118..92455eb 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { - "name": "baikal-storage-plugin", - "description": "ABaikal plugin to sync iCal objects into storages such as taskwarrior and todist", + "name": "aerex/baikal-storage-plugin", + "description": "A Baikal plugin to sync iCal objects into storages such as taskwarrior and todist", "type": "library", "keywords": [ "task", @@ -23,10 +23,12 @@ "nesbot/carbon": "^2.0.0", "laminas/laminas-validator": "^2.13", "laminas/laminas-stdlib": "^3.2", - "psr/container": "^1.0" + "psr/container": "^1.0", + "symfony/config": "^5.0", + "symfony/process": "^3.4" }, "require-dev": { - "phpunit/phpunit": "> 4.8, <=6.0.0" + "phpunit/phpunit": "^7.4" }, "authors": [ { @@ -36,7 +38,9 @@ ], "autoload": { "psr-4": { - "Aerex\\Taskwarrior\\": "src/taskwarrior" + "Aerex\\BaikalStorage\\Storages\\": "lib/Storages/", + "Aerex\\BaikalStorage\\Configs\\": "lib/Configs/", + "Aerex\\BaikalStorage\\": "lib/" } } } diff --git a/lib/AbstractConsole.php b/lib/AbstractConsole.php new file mode 100644 index 0000000..ea94232 --- /dev/null +++ b/lib/AbstractConsole.php @@ -0,0 +1,12 @@ +defaultArgs = $defaultArgs; + } + + abstract protected function execute($cmd, $args, $input = null); +} diff --git a/lib/Configs/ConfigBuilder.php b/lib/Configs/ConfigBuilder.php new file mode 100644 index 0000000..05bfa8a --- /dev/null +++ b/lib/Configs/ConfigBuilder.php @@ -0,0 +1,47 @@ +configFile = $configFile; + $this->processor = new Processor(); + } + + public function add($config) { + $this->configs[] = $config; + } + + public function getConfigTreeBuilder() { + $treeBuilder = new TreeBuilder('configs'); + $rootNode = $treeBuilder->getRootNode(); + $ref = $rootNode->children(); + foreach ($this->configs as $config) { + $ref = $ref->append($config->get()); + } + $ref->end(); + return $treeBuilder; + } + + public function readContent() { + if (!is_dir($this->configFile)) { + mkdir($this->configFile); + } + $contents = sprintf('%s/storage.yml', $this->configFile); + return file_get_contents($contents); + } + + public function loadYaml() { + $contents = $this->readContent(); + $parseContents = Yaml::parse($contents); + return $this->processor->processConfiguration($this, [$parseContents]); + } +} diff --git a/lib/Configs/TaskwarriorConfig.php b/lib/Configs/TaskwarriorConfig.php new file mode 100644 index 0000000..4fe47f3 --- /dev/null +++ b/lib/Configs/TaskwarriorConfig.php @@ -0,0 +1,17 @@ +getRootNode(); + $node->children() + ->scalarNode('data_dir') + ->defaultValue('~/.task') + ->end(); + return $node; + } +} diff --git a/lib/Configs/Todotxt.php b/lib/Configs/Todotxt.php new file mode 100644 index 0000000..19abad7 --- /dev/null +++ b/lib/Configs/Todotxt.php @@ -0,0 +1,21 @@ +getRootNode(); + + $rootNode->children + ->scalarNode('storage') + ->isRequired() + ->ifNotInArray(['todotxt']) + ->thenInvalid('Invalid storage %s') + ->end(); + return $treeBuilder; + } +} + diff --git a/lib/Console.php b/lib/Console.php new file mode 100644 index 0000000..02c485c --- /dev/null +++ b/lib/Console.php @@ -0,0 +1,39 @@ +defaultArgs = $defaultArgs; + } + + private function convertToString($input) { + if (is_array($input)) { + return json_encode($input); + } + } + + public function execute($cmd, $args, $input = null) { + $stdin[] = $cmd; + $stdin[] = array_merge($stdin, $this->defaultArgs, $args); + + if (isset($input)) { + $stdin[] = $this->convertToString($input); + $process = new Process($stdin); + } + + try { + $process->mustRun(); + return $process->getOutput(); + } catch (ProcessFailedException $error) { + echo $error->getMessage(); + throw $error; + } + } +} + diff --git a/lib/Plugin.php b/lib/Plugin.php new file mode 100644 index 0000000..0d6c8b2 --- /dev/null +++ b/lib/Plugin.php @@ -0,0 +1,156 @@ +config = $config; + } else { + $this->config = new ConfigBuilder(); + } + $this->storageManager = new StorageManager($this->config); + $this->addStorages(); + } + + /** + * Configure available storages in storage manager + * + */ + + public function addStorages() { + $taskwarrior = new Taskwarrior(new Console(['rc.verbose=nothing', 'rc.hooks=off']), new TaskwarriorConfig()); + $this->storageManager->addStorage(Taskwarrior::NAME, $taskwarrior); + $this->storageManager->init(); + } + + /** + * Sets up the plugin + * + * @param Server $server + * @return void */ + function initialize(Server $server) { + $this->server = $server; + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 15); + + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + function getPluginName() { + + return 'taskwarrior'; + + } + /** + * This method is called before any HTTP method handler. + * + * This method intercepts any GET, DELETE, PUT and PROPFIND calls to + * filenames that are known to match the 'temporary file' regex. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * + * @return bool + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + + switch ($request->getMethod()) { + case 'PUT': + $this->httpPut($request, $response); + } + return; + } + + + + /** + * This method handles the PUT method. + * + * @param RequestInterface $request + * + * @return bool + */ + function httpPut(RequestInterface $request){ + $body = $request->getBodyAsString(); + $vCal = \Sabre\VObject\Reader::read($body); + try { + $this->storageManager->import($vCal); + } catch(BadRequest $e){ + throw new BadRequest($e->getMessage(), null, $e); + } catch(\Exception $e){ + throw new \Exception($e->getMessage(), null, $e); + } + + $request->setBody($body); + + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + function getPluginInfo() { + + return [ + 'name' => $this->getPluginName(), + 'description' => 'The plugin provides synchronization between taskwarrior tasks and iCAL events', + 'link' => null, + ]; + + } +} diff --git a/lib/StorageManager.php b/lib/StorageManager.php new file mode 100644 index 0000000..a8944fa --- /dev/null +++ b/lib/StorageManager.php @@ -0,0 +1,57 @@ +configBuilder = $configBuilder; + } + + public function getStorages() { + return $this->storages; + } + + public function getConfigs() { + return $this->configs; + } + + public function addStorage($name, $storage) { + $this->configBuilder->add($storage->getConfig()); + $this->storages[$name] = $storage; + } + + public function init() { + $this->configs = $this->configBuilder->loadYaml(); + } + + public function import(Calendar $calendar) { + if (!isset($this->configs)) { + throw new \Exception('StorageManger was not initialize or configs are not defined'); + } + foreach ($this->configs as $key => $value) { + $storage = $this->storages[$key]; + if (!isset($storage)){ + throw new \Exception(); + } + $storage->setRawConfigs($this->configs[$key]); + $storage->save($calendar); + } + } +} diff --git a/lib/Storages/IStorage.php b/lib/Storages/IStorage.php new file mode 100644 index 0000000..42c6be4 --- /dev/null +++ b/lib/Storages/IStorage.php @@ -0,0 +1,12 @@ +console = $console; + $this->config = $config; + } + + public function getConfig() { + return $this->config; + } + + public function setRawConfigs($rawConfigs) { + $this->rawConfigs = $rawConfigs; + } + + public function refresh() { + $dataDir = $this->rawConfigs['data_dir']; + $fp = fopen(sprintf('%s/taskwarrior-baikal-storage.lock', $dataDir), 'a'); + + if (!$fp || !flock($fp, LOCK_EX | LOCK_NB, $eWouldBlock) || $eWouldBlock) { + fputs(STDERR, 'Could not get lock'); + } + + $mtime = 0; + $tasksUpdated = false; + foreach (Taskwarrior::DATA_FILES as $dataFile) { + $fmtime = filemtime(sprintf('%s/%s', $this->config['data_dir'], $dataFile)); + if ($fmtime > $mtime) { + $mtime = $fmtime; + $tasksUpdated = true; + } + } + + if ($tasksUpdated) { + $tasks = $this->console->execute('task', ['export']); + foreach ($tasks as $task) { + $this->tasks[$task['uuid']] = $task; + } + } + fclose($fp); + unlink(sprintf('%s/taskwarrior-baikal-storage.lock', $dataDir)); + } + + public function vObjectToTask($vtodo) { + if ($this->tasks['uid'] == $vtodo->UID) { + $task = $this->tasks['uid']; + } else { + $task = []; + $task['uid'] = $vtodo->UID; + } + + + if (!isset($vtodo->DESCRIPTION) && isset($vtodo->SUMMARY)){ + $task['description'] = $vtodo->SUMMARY; + } else { + $task['description'] = $vtodo->DESCRIPTION; + } + + if (isset($vtodo->DTSTAMP)){ + $task['entry'] = new Carbon($vtodo->DTSTAMP->getDateTime()->format(\DateTime::W3C)); + } + + if (isset($vtodo->DUE)){ + $task['due'] = new Carbon($vtodo->DUE->getDateTime()->format(\DateTime::W3C)); + } + + return $task; + } + + public function save(Calendar $c) { + if (!isset($c->VTODO)){ + throw new \Exception('Calendar event does not contain VTODO'); + } + $this->refresh(); + $task = $this->vObjectToTask($c->VTODO); + $this->console->execute('task', ['import'], $task); + } +} diff --git a/lib/Storages/Todotxt.php b/lib/Storages/Todotxt.php new file mode 100644 index 0000000..adcb2d6 --- /dev/null +++ b/lib/Storages/Todotxt.php @@ -0,0 +1,88 @@ +console = $console; + } + + public function setConfig($config) { + $this->config = $config; + array_push($this->dataFiles, $config['todo_file']); + array_push($this->dataFiles, $config['done_file']); + array_push($this->dataFiles, $config['report_file']); + } + + private function parseRaw($rawTodos) { + + + } + + private function refresh() { + $dataDir = $this->config['todo_dir']; + $fp = fopen(sprintf('%s/baikal-todo-storage.lock', $dataDir), 'a'); + + if (!$fp || !flock($fp, LOCK_EX | LOCK_NB, $eWouldBlock) || $eWouldBlock) { + fputs(STDERR, 'Could not get lock'); + } + + $mtime = 0; + $tasksUpdated = false; + foreach ($this->dataFiles as $dataFile) { + $fmtime = filemtime(sprintf('%s/%s', $this->config['data_dir'], $dataFile)); + if ($fmtime > $mtime) { + $mtime = $fmtime; + $tasksUpdated = true; + break; + } + } + + if ($tasksUpdated) { + $rawTodos = $this->console->execute('cat', [$this->config['todo_file']]); + $todos = $this->parseRaw($rawTodos); + foreach ($todos as $todo) { + $this->todos[$todo['line']] = $todos; + } + } + fclose($fp); + unlink(sprintf('%s/baikal-todo-storage.lock', $dataDir)); + } + + public function vObjectToTodo($vtodo) { + $task = []; + + $task['uid'] = $vtodo->UID; + + if(!isset($vtodo->DESCRIPTION) && isset($vtodo->SUMMARY)){ + $task['description'] = $vtodo->SUMMARY; + } else { + $task['description'] = $vtodo->DESCRIPTION; + } + + if (isset($vtodo->DTSTAMP)){ + $task['entry'] = new Carbon($vtodo->DTSTAMP->getDateTime()->format(\DateTime::W3C)); + } + + if (isset($vtodo->DUE)){ + $task['due'] = new Carbon($vtodo->DUE->getDateTime()->format(\DateTime::W3C)); + } + + return $task; + } + + public function save(Calendar $c) { + if (!isset($c->VTODO)){ + throw new Exception('Could not find iCal VTODO event'); + } + $this->refresh(); + $task = $this->vObjectToTodo($c->VTODO); + $this->console->execute('task', ['import'], $task); + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..e25f316 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + + ./lib/tests + + + + + ./lib + + ./lib/tests + ./vendor + + + + + + + + diff --git a/tests/Fixtures/taskwarrior_config.yml b/tests/Fixtures/taskwarrior_config.yml new file mode 100644 index 0000000..ad36eae --- /dev/null +++ b/tests/Fixtures/taskwarrior_config.yml @@ -0,0 +1,2 @@ +taskwarrior: + data_dir: '~/task' diff --git a/tests/StorageManagerTest.php b/tests/StorageManagerTest.php new file mode 100644 index 0000000..f7c8d11 --- /dev/null +++ b/tests/StorageManagerTest.php @@ -0,0 +1,71 @@ +mockConfigBuilder = $this->getMockBuilder(ConfigBuilder::class) + ->setMethods(['readContent']) + ->setConstructorArgs(['']) + ->getMock(); + $this->mockConsole = $this->createMock(AbstractConsole::class); + $this->mockStorage = $this->createMock(IStorage::class); + } + + public function testAddTaskwarriorStorage() { + $this->mockConfigBuilder->expects($this->once()) + ->method('readContent') + ->willReturn(file_get_contents(__DIR__ . '/Fixtures/taskwarrior_config.yml')); + $tw = new Taskwarrior($this->mockConsole, new TaskwarriorConfig()); + $manager = new StorageManager($this->mockConfigBuilder); + $manager->addStorage(Taskwarrior::NAME, $tw); + $storages = $manager->getStorages(); + $manager->init(); + $configs = $manager->getConfigs(); + $this->assertEquals(sizeof(array_keys($storages)), 1, 'Taskwarrior storage was not added'); + $this->assertEquals(sizeof(array_keys($configs)), 1, 'Taskwarrior config was not loaded'); + $this->assertArrayHasKey('taskwarrior', $storages, 'Storages should have taskwarrior'); + $this->assertArrayHasKey('taskwarrior', $configs, 'Configs should have taskwarrior'); + } + + public function testTaskwarriorImport() { + $cal = new Calendar(); + $this->mockConfigBuilder->expects($this->once()) + ->method('readContent') + ->willReturn(file_get_contents(__DIR__ . '/Fixtures/taskwarrior_config.yml')); + $this->mockStorage->expects($this->once()) + ->method('save') + ->with($this->equalTo($cal)); + $this->mockStorage->expects($this->once()) + ->method('setRawConfigs') + ->with($this->equalTo(['data_dir' => '~/.task'])); + $this->mockStorage->expects($this->once()) + ->method('getConfig') + ->willReturn(new TaskwarriorConfig()); + + $manager = new StorageManager($this->mockConfigBuilder); + $manager->addStorage(Taskwarrior::NAME, $this->mockStorage); + $manager->init(); + + $manager->import($cal); + + } +}