feat: Initial commit
This commit is contained in:
12
lib/AbstractConsole.php
Normal file
12
lib/AbstractConsole.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage;
|
||||
|
||||
abstract class AbstractConsole {
|
||||
|
||||
function __construct($defaultArgs) {
|
||||
$this->defaultArgs = $defaultArgs;
|
||||
}
|
||||
|
||||
abstract protected function execute($cmd, $args, $input = null);
|
||||
}
|
47
lib/Configs/ConfigBuilder.php
Normal file
47
lib/Configs/ConfigBuilder.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Configs;
|
||||
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\Processor;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ConfigBuilder implements ConfigurationInterface {
|
||||
private $configs = [];
|
||||
private $configFile;
|
||||
|
||||
public function __construct($configFile = '~/.config/baikal') {
|
||||
$this->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]);
|
||||
}
|
||||
}
|
17
lib/Configs/TaskwarriorConfig.php
Normal file
17
lib/Configs/TaskwarriorConfig.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Configs;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
|
||||
class TaskwarriorConfig {
|
||||
public function get() {
|
||||
$treeBuilder = new TreeBuilder('taskwarrior');
|
||||
$node = $treeBuilder->getRootNode();
|
||||
$node->children()
|
||||
->scalarNode('data_dir')
|
||||
->defaultValue('~/.task')
|
||||
->end();
|
||||
return $node;
|
||||
}
|
||||
}
|
21
lib/Configs/Todotxt.php
Normal file
21
lib/Configs/Todotxt.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Configs;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
|
||||
class Config extends AbstractConfig {
|
||||
protected function getConfigTree() {
|
||||
$treeBuilder = new TreeBuilder('config');
|
||||
$rootNode = $treeBuilder->getRootNode();
|
||||
|
||||
$rootNode->children
|
||||
->scalarNode('storage')
|
||||
->isRequired()
|
||||
->ifNotInArray(['todotxt'])
|
||||
->thenInvalid('Invalid storage %s')
|
||||
->end();
|
||||
return $treeBuilder;
|
||||
}
|
||||
}
|
||||
|
39
lib/Console.php
Normal file
39
lib/Console.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use Symfony\Component\Process\Exception\ProcessFailedException as ProcessFailedException;
|
||||
|
||||
class Console extends AbstractConsole {
|
||||
private $defaultArgs = [];
|
||||
|
||||
function __construct($defaultArgs) {
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
156
lib/Plugin.php
Normal file
156
lib/Plugin.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage;
|
||||
|
||||
use Aerex\BaikalStorage\Storages\Taskwarrior;
|
||||
use Aerex\BaikalStorage\Configs\ConfigBuilder;
|
||||
use Aerex\BaikalStorage\Configs\TaskwarriorConfig;
|
||||
use Sabre\DAV\Exception\BadRequest;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
use Sabre\HTTP\ResponseInterface;
|
||||
use Sabre\DAV\ServerPlugin;
|
||||
use Sabre\DAV\Server;
|
||||
|
||||
/**
|
||||
* The plugin to interact with Baikal and external storages
|
||||
*
|
||||
*/
|
||||
class Plugin extends ServerPlugin {
|
||||
|
||||
/**
|
||||
* Reference to server object.
|
||||
*
|
||||
* @var Server
|
||||
*/
|
||||
protected $server;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @var StorageManager
|
||||
*/
|
||||
protected $storageManager;
|
||||
|
||||
/**
|
||||
* @var ConfigBuilder
|
||||
*/
|
||||
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Creates the Taskwarrior plugin
|
||||
*
|
||||
* @param CalendarProcessor $TWCalManager
|
||||
*
|
||||
*/
|
||||
function __construct($config = null){
|
||||
if (isset($config)) {
|
||||
$this->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,
|
||||
];
|
||||
|
||||
}
|
||||
}
|
57
lib/StorageManager.php
Normal file
57
lib/StorageManager.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar as Calendar;
|
||||
use Aerex\BaikalStorage\Configs\ConfigBuilder;
|
||||
|
||||
class StorageManager {
|
||||
|
||||
/**
|
||||
* @var Storage[]
|
||||
*/
|
||||
|
||||
private $storages = [];
|
||||
|
||||
|
||||
/**
|
||||
* @var Config
|
||||
*/
|
||||
private $configBuilder;
|
||||
private $configs;
|
||||
|
||||
public function __construct($configBuilder){
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
}
|
12
lib/Storages/IStorage.php
Normal file
12
lib/Storages/IStorage.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Storages;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar as Calendar;
|
||||
|
||||
interface IStorage {
|
||||
public function save(Calendar $c);
|
||||
public function refresh();
|
||||
public function getConfig();
|
||||
public function setRawConfigs();
|
||||
}
|
89
lib/Storages/Taskwarrior.php
Normal file
89
lib/Storages/Taskwarrior.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Storages;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar as Calendar;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Taskwarrior implements IStorage {
|
||||
|
||||
private const DATA_FILES = ['pending.data', 'completed.data', 'undo.data'];
|
||||
private $rawConfigs;
|
||||
public const NAME = 'taskwarrior';
|
||||
private $tasks = [];
|
||||
public function __construct($console, $config) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
88
lib/Storages/Todotxt.php
Normal file
88
lib/Storages/Todotxt.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Aerex\BaikalStorage\Storages;
|
||||
|
||||
use Sabre\VObject\Component\VCalendar as Calendar;
|
||||
use Carbon\Carbon;
|
||||
class Todotxt implements IStorage {
|
||||
|
||||
private $dataFiles = [];
|
||||
public const name = 'taskwarrior';
|
||||
private $todos = [];
|
||||
public function __construct($console) {
|
||||
$this->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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user