Completed
Push — master ( c60c8a...be30c0 )
by Morris
174:28 queued 149:32
created

Notify::logUpdate()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 25
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 20
c 0
b 0
f 0
nc 9
nop 2
dl 0
loc 25
rs 8.439
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016 Robin Appelman <[email protected]>
4
 *
5
 * @author Robin Appelman <[email protected]>
6
 * @author Roeland Jago Douma <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
25
namespace OCA\Files_External\Command;
26
27
use OC\Core\Command\Base;
28
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
29
use OCA\Files_External\Lib\StorageConfig;
30
use OCA\Files_External\Service\GlobalStoragesService;
31
use OCP\Files\Notify\IChange;
32
use OCP\Files\Notify\INotifyHandler;
33
use OCP\Files\Notify\IRenameChange;
34
use OCP\Files\Storage\INotifyStorage;
35
use OCP\Files\Storage\IStorage;
36
use OCP\Files\StorageNotAvailableException;
37
use OCP\IDBConnection;
38
use Symfony\Component\Console\Input\InputArgument;
39
use Symfony\Component\Console\Input\InputInterface;
40
use Symfony\Component\Console\Input\InputOption;
41
use Symfony\Component\Console\Output\OutputInterface;
42
43
class Notify extends Base {
44
	/** @var GlobalStoragesService */
45
	private $globalService;
46
	/** @var IDBConnection */
47
	private $connection;
48
	/** @var \OCP\DB\QueryBuilder\IQueryBuilder */
49
	private $updateQuery;
50
51
	function __construct(GlobalStoragesService $globalService, IDBConnection $connection) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
52
		parent::__construct();
53
		$this->globalService = $globalService;
54
		$this->connection = $connection;
55
		// the query builder doesn't really like subqueries with parameters
56
		$this->updateQuery = $this->connection->prepare(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->connection->prepa...s WHERE mount_id = ?)') of type object<Doctrine\DBAL\Driver\Statement> is incompatible with the declared type object<OCP\DB\QueryBuilder\IQueryBuilder> of property $updateQuery.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
57
			'UPDATE *PREFIX*filecache SET size = -1
58
			WHERE `path` = ?
59
			AND `storage` IN (SELECT storage_id FROM *PREFIX*mounts WHERE mount_id = ?)'
60
		);
61
	}
62
63
	protected function configure() {
64
		$this
65
			->setName('files_external:notify')
66
			->setDescription('Listen for active update notifications for a configured external mount')
67
			->addArgument(
68
				'mount_id',
69
				InputArgument::REQUIRED,
70
				'the mount id of the mount to listen to'
71
			)->addOption(
72
				'user',
73
				'u',
74
				InputOption::VALUE_REQUIRED,
75
				'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
76
			)->addOption(
77
				'password',
78
				'p',
79
				InputOption::VALUE_REQUIRED,
80
				'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
81
			)->addOption(
82
				'path',
83
				'',
84
				InputOption::VALUE_REQUIRED,
85
				'The directory in the storage to listen for updates in',
86
				'/'
87
			);
88
		parent::configure();
89
	}
90
91
	protected function execute(InputInterface $input, OutputInterface $output) {
0 ignored issues
show
Coding Style introduced by
execute uses the super-global variable $_ENV which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
execute uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
92
		$mount = $this->globalService->getStorage($input->getArgument('mount_id'));
93
		if (is_null($mount)) {
94
			$output->writeln('<error>Mount not found</error>');
95
			return 1;
96
		}
97
		$noAuth = false;
98
		try {
99
			$authBackend = $mount->getAuthMechanism();
100
			$authBackend->manipulateStorageConfig($mount);
101
		} catch (InsufficientDataForMeaningfulAnswerException $e) {
102
			$noAuth = true;
103
		} catch (StorageNotAvailableException $e) {
104
			$noAuth = true;
105
		}
106
107 View Code Duplication
		if ($input->getOption('user')) {
108
			$mount->setBackendOption('user', $input->getOption('user'));
109
		} else if (isset($_ENV['NOTIFY_USER'])) {
110
			$mount->setBackendOption('user', $_ENV['NOTIFY_USER']);
111
		} else if (isset($_SERVER['NOTIFY_USER'])) {
112
			$mount->setBackendOption('user', $_SERVER['NOTIFY_USER']);
113
		}
114 View Code Duplication
		if ($input->getOption('password')) {
115
			$mount->setBackendOption('password', $input->getOption('password'));
116
		} else if (isset($_ENV['NOTIFY_PASSWORD'])) {
117
			$mount->setBackendOption('password', $_ENV['NOTIFY_PASSWORD']);
118
		} else if (isset($_SERVER['NOTIFY_PASSWORD'])) {
119
			$mount->setBackendOption('password', $_SERVER['NOTIFY_PASSWORD']);
120
		}
121
122
		try {
123
			$storage = $this->createStorage($mount);
124
		} catch (\Exception $e) {
125
			$output->writeln('<error>Error while trying to create storage</error>');
126
			if ($noAuth) {
127
				$output->writeln('<error>Username and/or password required</error>');
128
			}
129
			return 1;
130
		}
131
		if (!$storage instanceof INotifyStorage) {
132
			$output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
133
			return 1;
134
		}
135
136
		$verbose = $input->getOption('verbose');
137
138
		$path = trim($input->getOption('path'), '/');
139
		$notifyHandler = $storage->notify($path);
140
		$this->selfTest($storage, $notifyHandler, $verbose, $output);
141
		$notifyHandler->listen(function (IChange $change) use ($mount, $verbose, $output) {
142
			if ($verbose) {
143
				$this->logUpdate($change, $output);
144
			}
145
			if ($change instanceof IRenameChange) {
146
				$this->markParentAsOutdated($mount->getId(), $change->getTargetPath());
147
			}
148
			$this->markParentAsOutdated($mount->getId(), $change->getPath());
149
		});
150
	}
151
152
	private function createStorage(StorageConfig $mount) {
153
		$class = $mount->getBackend()->getStorageClass();
154
		return new $class($mount->getBackendOptions());
155
	}
156
157
	private function markParentAsOutdated($mountId, $path) {
158
		$parent = dirname($path);
159
		if ($parent === '.') {
160
			$parent = '';
161
		}
162
		$this->updateQuery->execute([$parent, $mountId]);
163
	}
164
165
	private function logUpdate(IChange $change, OutputInterface $output) {
166
		switch ($change->getType()) {
167
			case INotifyStorage::NOTIFY_ADDED:
168
				$text = 'added';
169
				break;
170
			case INotifyStorage::NOTIFY_MODIFIED:
171
				$text = 'modified';
172
				break;
173
			case INotifyStorage::NOTIFY_REMOVED:
174
				$text = 'removed';
175
				break;
176
			case INotifyStorage::NOTIFY_RENAMED:
177
				$text = 'renamed';
178
				break;
179
			default:
180
				return;
181
		}
182
183
		$text .= ' ' . $change->getPath();
184
		if ($change instanceof IRenameChange) {
185
			$text .= ' to ' . $change->getTargetPath();
186
		}
187
188
		$output->writeln($text);
189
	}
190
191
	private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, $verbose, OutputInterface $output) {
192
		usleep(100 * 1000); //give time for the notify to start
193
		$storage->file_put_contents('/.nc_test_file.txt', 'test content');
194
		$storage->mkdir('/.nc_test_folder');
195
		$storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content');
196
197
		usleep(100 * 1000); //time for all changes to be processed
198
		$changes = $notifyHandler->getChanges();
199
200
		$storage->unlink('/.nc_test_file.txt');
201
		$storage->unlink('/.nc_test_folder/subfile.txt');
202
		$storage->rmdir('/.nc_test_folder');
203
204
		usleep(100 * 1000); //time for all changes to be processed
205
		$notifyHandler->getChanges(); // flush
206
207
		$foundRootChange = false;
208
		$foundSubfolderChange = false;
209
210
		foreach ($changes as $change) {
211
			if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') {
212
				$foundRootChange = true;
213
			} else if ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') {
214
				$foundSubfolderChange = true;
215
			}
216
		}
217
218
		if ($foundRootChange && $foundSubfolderChange && $verbose) {
219
			$output->writeln('<info>Self-test successful</info>');
220
		} else if ($foundRootChange && !$foundSubfolderChange) {
221
			$output->writeln('<error>Error while running self-test, change is subfolder not detected</error>');
222
		} else if (!$foundRootChange) {
223
			$output->writeln('<error>Error while running self-test, no changes detected</error>');
224
		}
225
	}
226
}
227