Passed
Push — master ( 784b2b...ec1acb )
by Blizzz
25:02 queued 08:58
created
lib/private/Files/Storage/Common.php 1 patch
Indentation   +823 added lines, -823 removed lines patch added patch discarded remove patch
@@ -77,831 +77,831 @@
 block discarded – undo
77 77
  * in classes which extend it, e.g. $this->stat() .
78 78
  */
79 79
 abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage {
80
-	use LocalTempFileTrait;
81
-
82
-	protected $cache;
83
-	protected $scanner;
84
-	protected $watcher;
85
-	protected $propagator;
86
-	protected $storageCache;
87
-	protected $updater;
88
-
89
-	protected $mountOptions = [];
90
-	protected $owner = null;
91
-
92
-	/** @var ?bool */
93
-	private $shouldLogLocks = null;
94
-	/** @var ?LoggerInterface */
95
-	private $logger;
96
-
97
-	public function __construct($parameters) {
98
-	}
99
-
100
-	/**
101
-	 * Remove a file or folder
102
-	 *
103
-	 * @param string $path
104
-	 * @return bool
105
-	 */
106
-	protected function remove($path) {
107
-		if ($this->is_dir($path)) {
108
-			return $this->rmdir($path);
109
-		} elseif ($this->is_file($path)) {
110
-			return $this->unlink($path);
111
-		} else {
112
-			return false;
113
-		}
114
-	}
115
-
116
-	public function is_dir($path) {
117
-		return $this->filetype($path) === 'dir';
118
-	}
119
-
120
-	public function is_file($path) {
121
-		return $this->filetype($path) === 'file';
122
-	}
123
-
124
-	public function filesize($path): false|int|float {
125
-		if ($this->is_dir($path)) {
126
-			return 0; //by definition
127
-		} else {
128
-			$stat = $this->stat($path);
129
-			if (isset($stat['size'])) {
130
-				return $stat['size'];
131
-			} else {
132
-				return 0;
133
-			}
134
-		}
135
-	}
136
-
137
-	public function isReadable($path) {
138
-		// at least check whether it exists
139
-		// subclasses might want to implement this more thoroughly
140
-		return $this->file_exists($path);
141
-	}
142
-
143
-	public function isUpdatable($path) {
144
-		// at least check whether it exists
145
-		// subclasses might want to implement this more thoroughly
146
-		// a non-existing file/folder isn't updatable
147
-		return $this->file_exists($path);
148
-	}
149
-
150
-	public function isCreatable($path) {
151
-		if ($this->is_dir($path) && $this->isUpdatable($path)) {
152
-			return true;
153
-		}
154
-		return false;
155
-	}
156
-
157
-	public function isDeletable($path) {
158
-		if ($path === '' || $path === '/') {
159
-			return $this->isUpdatable($path);
160
-		}
161
-		$parent = dirname($path);
162
-		return $this->isUpdatable($parent) && $this->isUpdatable($path);
163
-	}
164
-
165
-	public function isSharable($path) {
166
-		return $this->isReadable($path);
167
-	}
168
-
169
-	public function getPermissions($path) {
170
-		$permissions = 0;
171
-		if ($this->isCreatable($path)) {
172
-			$permissions |= \OCP\Constants::PERMISSION_CREATE;
173
-		}
174
-		if ($this->isReadable($path)) {
175
-			$permissions |= \OCP\Constants::PERMISSION_READ;
176
-		}
177
-		if ($this->isUpdatable($path)) {
178
-			$permissions |= \OCP\Constants::PERMISSION_UPDATE;
179
-		}
180
-		if ($this->isDeletable($path)) {
181
-			$permissions |= \OCP\Constants::PERMISSION_DELETE;
182
-		}
183
-		if ($this->isSharable($path)) {
184
-			$permissions |= \OCP\Constants::PERMISSION_SHARE;
185
-		}
186
-		return $permissions;
187
-	}
188
-
189
-	public function filemtime($path) {
190
-		$stat = $this->stat($path);
191
-		if (isset($stat['mtime']) && $stat['mtime'] > 0) {
192
-			return $stat['mtime'];
193
-		} else {
194
-			return 0;
195
-		}
196
-	}
197
-
198
-	public function file_get_contents($path) {
199
-		$handle = $this->fopen($path, "r");
200
-		if (!$handle) {
201
-			return false;
202
-		}
203
-		$data = stream_get_contents($handle);
204
-		fclose($handle);
205
-		return $data;
206
-	}
207
-
208
-	public function file_put_contents($path, $data) {
209
-		$handle = $this->fopen($path, "w");
210
-		if (!$handle) {
211
-			return false;
212
-		}
213
-		$this->removeCachedFile($path);
214
-		$count = fwrite($handle, $data);
215
-		fclose($handle);
216
-		return $count;
217
-	}
218
-
219
-	public function rename($source, $target) {
220
-		$this->remove($target);
221
-
222
-		$this->removeCachedFile($source);
223
-		return $this->copy($source, $target) and $this->remove($source);
224
-	}
225
-
226
-	public function copy($source, $target) {
227
-		if ($this->is_dir($source)) {
228
-			$this->remove($target);
229
-			$dir = $this->opendir($source);
230
-			$this->mkdir($target);
231
-			while ($file = readdir($dir)) {
232
-				if (!Filesystem::isIgnoredDir($file)) {
233
-					if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
234
-						closedir($dir);
235
-						return false;
236
-					}
237
-				}
238
-			}
239
-			closedir($dir);
240
-			return true;
241
-		} else {
242
-			$sourceStream = $this->fopen($source, 'r');
243
-			$targetStream = $this->fopen($target, 'w');
244
-			[, $result] = \OC_Helper::streamCopy($sourceStream, $targetStream);
245
-			if (!$result) {
246
-				\OCP\Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
247
-			}
248
-			$this->removeCachedFile($target);
249
-			return $result;
250
-		}
251
-	}
252
-
253
-	public function getMimeType($path) {
254
-		if ($this->is_dir($path)) {
255
-			return 'httpd/unix-directory';
256
-		} elseif ($this->file_exists($path)) {
257
-			return \OC::$server->getMimeTypeDetector()->detectPath($path);
258
-		} else {
259
-			return false;
260
-		}
261
-	}
262
-
263
-	public function hash($type, $path, $raw = false) {
264
-		$fh = $this->fopen($path, 'rb');
265
-		$ctx = hash_init($type);
266
-		hash_update_stream($ctx, $fh);
267
-		fclose($fh);
268
-		return hash_final($ctx, $raw);
269
-	}
270
-
271
-	public function search($query) {
272
-		return $this->searchInDir($query);
273
-	}
274
-
275
-	public function getLocalFile($path) {
276
-		return $this->getCachedFile($path);
277
-	}
278
-
279
-	/**
280
-	 * @param string $path
281
-	 * @param string $target
282
-	 */
283
-	private function addLocalFolder($path, $target) {
284
-		$dh = $this->opendir($path);
285
-		if (is_resource($dh)) {
286
-			while (($file = readdir($dh)) !== false) {
287
-				if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
288
-					if ($this->is_dir($path . '/' . $file)) {
289
-						mkdir($target . '/' . $file);
290
-						$this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
291
-					} else {
292
-						$tmp = $this->toTmpFile($path . '/' . $file);
293
-						rename($tmp, $target . '/' . $file);
294
-					}
295
-				}
296
-			}
297
-		}
298
-	}
299
-
300
-	/**
301
-	 * @param string $query
302
-	 * @param string $dir
303
-	 * @return array
304
-	 */
305
-	protected function searchInDir($query, $dir = '') {
306
-		$files = [];
307
-		$dh = $this->opendir($dir);
308
-		if (is_resource($dh)) {
309
-			while (($item = readdir($dh)) !== false) {
310
-				if (\OC\Files\Filesystem::isIgnoredDir($item)) {
311
-					continue;
312
-				}
313
-				if (strstr(strtolower($item), strtolower($query)) !== false) {
314
-					$files[] = $dir . '/' . $item;
315
-				}
316
-				if ($this->is_dir($dir . '/' . $item)) {
317
-					$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
318
-				}
319
-			}
320
-		}
321
-		closedir($dh);
322
-		return $files;
323
-	}
324
-
325
-	/**
326
-	 * Check if a file or folder has been updated since $time
327
-	 *
328
-	 * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
329
-	 * the mtime should always return false here. As a result storage implementations that always return false expect
330
-	 * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
331
-	 * Nextcloud filesystem.
332
-	 *
333
-	 * @param string $path
334
-	 * @param int $time
335
-	 * @return bool
336
-	 */
337
-	public function hasUpdated($path, $time) {
338
-		return $this->filemtime($path) > $time;
339
-	}
340
-
341
-	public function getCache($path = '', $storage = null) {
342
-		if (!$storage) {
343
-			$storage = $this;
344
-		}
345
-		if (!isset($storage->cache)) {
346
-			$storage->cache = new Cache($storage);
347
-		}
348
-		return $storage->cache;
349
-	}
350
-
351
-	public function getScanner($path = '', $storage = null) {
352
-		if (!$storage) {
353
-			$storage = $this;
354
-		}
355
-		if (!isset($storage->scanner)) {
356
-			$storage->scanner = new Scanner($storage);
357
-		}
358
-		return $storage->scanner;
359
-	}
360
-
361
-	public function getWatcher($path = '', $storage = null) {
362
-		if (!$storage) {
363
-			$storage = $this;
364
-		}
365
-		if (!isset($this->watcher)) {
366
-			$this->watcher = new Watcher($storage);
367
-			$globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
368
-			$this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
369
-		}
370
-		return $this->watcher;
371
-	}
372
-
373
-	/**
374
-	 * get a propagator instance for the cache
375
-	 *
376
-	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
377
-	 * @return \OC\Files\Cache\Propagator
378
-	 */
379
-	public function getPropagator($storage = null) {
380
-		if (!$storage) {
381
-			$storage = $this;
382
-		}
383
-		if (!isset($storage->propagator)) {
384
-			$config = \OC::$server->getSystemConfig();
385
-			$storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getValue('instanceid')]);
386
-		}
387
-		return $storage->propagator;
388
-	}
389
-
390
-	public function getUpdater($storage = null) {
391
-		if (!$storage) {
392
-			$storage = $this;
393
-		}
394
-		if (!isset($storage->updater)) {
395
-			$storage->updater = new Updater($storage);
396
-		}
397
-		return $storage->updater;
398
-	}
399
-
400
-	public function getStorageCache($storage = null) {
401
-		if (!$storage) {
402
-			$storage = $this;
403
-		}
404
-		if (!isset($this->storageCache)) {
405
-			$this->storageCache = new \OC\Files\Cache\Storage($storage);
406
-		}
407
-		return $this->storageCache;
408
-	}
409
-
410
-	/**
411
-	 * get the owner of a path
412
-	 *
413
-	 * @param string $path The path to get the owner
414
-	 * @return string|false uid or false
415
-	 */
416
-	public function getOwner($path) {
417
-		if ($this->owner === null) {
418
-			$this->owner = \OC_User::getUser();
419
-		}
420
-
421
-		return $this->owner;
422
-	}
423
-
424
-	/**
425
-	 * get the ETag for a file or folder
426
-	 *
427
-	 * @param string $path
428
-	 * @return string
429
-	 */
430
-	public function getETag($path) {
431
-		return uniqid();
432
-	}
433
-
434
-	/**
435
-	 * clean a path, i.e. remove all redundant '.' and '..'
436
-	 * making sure that it can't point to higher than '/'
437
-	 *
438
-	 * @param string $path The path to clean
439
-	 * @return string cleaned path
440
-	 */
441
-	public function cleanPath($path) {
442
-		if (strlen($path) == 0 or $path[0] != '/') {
443
-			$path = '/' . $path;
444
-		}
445
-
446
-		$output = [];
447
-		foreach (explode('/', $path) as $chunk) {
448
-			if ($chunk == '..') {
449
-				array_pop($output);
450
-			} elseif ($chunk == '.') {
451
-			} else {
452
-				$output[] = $chunk;
453
-			}
454
-		}
455
-		return implode('/', $output);
456
-	}
457
-
458
-	/**
459
-	 * Test a storage for availability
460
-	 *
461
-	 * @return bool
462
-	 */
463
-	public function test() {
464
-		try {
465
-			if ($this->stat('')) {
466
-				return true;
467
-			}
468
-			\OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed");
469
-			return false;
470
-		} catch (\Exception $e) {
471
-			\OC::$server->get(LoggerInterface::class)->warning(
472
-				"External storage not available: " . $e->getMessage(),
473
-				['exception' => $e]
474
-			);
475
-			return false;
476
-		}
477
-	}
478
-
479
-	/**
480
-	 * get the free space in the storage
481
-	 *
482
-	 * @param string $path
483
-	 * @return int|float|false
484
-	 */
485
-	public function free_space($path) {
486
-		return \OCP\Files\FileInfo::SPACE_UNKNOWN;
487
-	}
488
-
489
-	/**
490
-	 * {@inheritdoc}
491
-	 */
492
-	public function isLocal() {
493
-		// the common implementation returns a temporary file by
494
-		// default, which is not local
495
-		return false;
496
-	}
497
-
498
-	/**
499
-	 * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
500
-	 *
501
-	 * @param string $class
502
-	 * @return bool
503
-	 */
504
-	public function instanceOfStorage($class) {
505
-		if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
506
-			// FIXME Temporary fix to keep existing checks working
507
-			$class = '\OCA\Files_Sharing\SharedStorage';
508
-		}
509
-		return is_a($this, $class);
510
-	}
511
-
512
-	/**
513
-	 * A custom storage implementation can return an url for direct download of a give file.
514
-	 *
515
-	 * For now the returned array can hold the parameter url - in future more attributes might follow.
516
-	 *
517
-	 * @param string $path
518
-	 * @return array|false
519
-	 */
520
-	public function getDirectDownload($path) {
521
-		return [];
522
-	}
523
-
524
-	/**
525
-	 * @inheritdoc
526
-	 * @throws InvalidPathException
527
-	 */
528
-	public function verifyPath($path, $fileName) {
529
-		// verify empty and dot files
530
-		$trimmed = trim($fileName);
531
-		if ($trimmed === '') {
532
-			throw new EmptyFileNameException();
533
-		}
534
-
535
-		if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
536
-			throw new InvalidDirectoryException();
537
-		}
538
-
539
-		if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
540
-			// verify database - e.g. mysql only 3-byte chars
541
-			if (preg_match('%(?:
80
+    use LocalTempFileTrait;
81
+
82
+    protected $cache;
83
+    protected $scanner;
84
+    protected $watcher;
85
+    protected $propagator;
86
+    protected $storageCache;
87
+    protected $updater;
88
+
89
+    protected $mountOptions = [];
90
+    protected $owner = null;
91
+
92
+    /** @var ?bool */
93
+    private $shouldLogLocks = null;
94
+    /** @var ?LoggerInterface */
95
+    private $logger;
96
+
97
+    public function __construct($parameters) {
98
+    }
99
+
100
+    /**
101
+     * Remove a file or folder
102
+     *
103
+     * @param string $path
104
+     * @return bool
105
+     */
106
+    protected function remove($path) {
107
+        if ($this->is_dir($path)) {
108
+            return $this->rmdir($path);
109
+        } elseif ($this->is_file($path)) {
110
+            return $this->unlink($path);
111
+        } else {
112
+            return false;
113
+        }
114
+    }
115
+
116
+    public function is_dir($path) {
117
+        return $this->filetype($path) === 'dir';
118
+    }
119
+
120
+    public function is_file($path) {
121
+        return $this->filetype($path) === 'file';
122
+    }
123
+
124
+    public function filesize($path): false|int|float {
125
+        if ($this->is_dir($path)) {
126
+            return 0; //by definition
127
+        } else {
128
+            $stat = $this->stat($path);
129
+            if (isset($stat['size'])) {
130
+                return $stat['size'];
131
+            } else {
132
+                return 0;
133
+            }
134
+        }
135
+    }
136
+
137
+    public function isReadable($path) {
138
+        // at least check whether it exists
139
+        // subclasses might want to implement this more thoroughly
140
+        return $this->file_exists($path);
141
+    }
142
+
143
+    public function isUpdatable($path) {
144
+        // at least check whether it exists
145
+        // subclasses might want to implement this more thoroughly
146
+        // a non-existing file/folder isn't updatable
147
+        return $this->file_exists($path);
148
+    }
149
+
150
+    public function isCreatable($path) {
151
+        if ($this->is_dir($path) && $this->isUpdatable($path)) {
152
+            return true;
153
+        }
154
+        return false;
155
+    }
156
+
157
+    public function isDeletable($path) {
158
+        if ($path === '' || $path === '/') {
159
+            return $this->isUpdatable($path);
160
+        }
161
+        $parent = dirname($path);
162
+        return $this->isUpdatable($parent) && $this->isUpdatable($path);
163
+    }
164
+
165
+    public function isSharable($path) {
166
+        return $this->isReadable($path);
167
+    }
168
+
169
+    public function getPermissions($path) {
170
+        $permissions = 0;
171
+        if ($this->isCreatable($path)) {
172
+            $permissions |= \OCP\Constants::PERMISSION_CREATE;
173
+        }
174
+        if ($this->isReadable($path)) {
175
+            $permissions |= \OCP\Constants::PERMISSION_READ;
176
+        }
177
+        if ($this->isUpdatable($path)) {
178
+            $permissions |= \OCP\Constants::PERMISSION_UPDATE;
179
+        }
180
+        if ($this->isDeletable($path)) {
181
+            $permissions |= \OCP\Constants::PERMISSION_DELETE;
182
+        }
183
+        if ($this->isSharable($path)) {
184
+            $permissions |= \OCP\Constants::PERMISSION_SHARE;
185
+        }
186
+        return $permissions;
187
+    }
188
+
189
+    public function filemtime($path) {
190
+        $stat = $this->stat($path);
191
+        if (isset($stat['mtime']) && $stat['mtime'] > 0) {
192
+            return $stat['mtime'];
193
+        } else {
194
+            return 0;
195
+        }
196
+    }
197
+
198
+    public function file_get_contents($path) {
199
+        $handle = $this->fopen($path, "r");
200
+        if (!$handle) {
201
+            return false;
202
+        }
203
+        $data = stream_get_contents($handle);
204
+        fclose($handle);
205
+        return $data;
206
+    }
207
+
208
+    public function file_put_contents($path, $data) {
209
+        $handle = $this->fopen($path, "w");
210
+        if (!$handle) {
211
+            return false;
212
+        }
213
+        $this->removeCachedFile($path);
214
+        $count = fwrite($handle, $data);
215
+        fclose($handle);
216
+        return $count;
217
+    }
218
+
219
+    public function rename($source, $target) {
220
+        $this->remove($target);
221
+
222
+        $this->removeCachedFile($source);
223
+        return $this->copy($source, $target) and $this->remove($source);
224
+    }
225
+
226
+    public function copy($source, $target) {
227
+        if ($this->is_dir($source)) {
228
+            $this->remove($target);
229
+            $dir = $this->opendir($source);
230
+            $this->mkdir($target);
231
+            while ($file = readdir($dir)) {
232
+                if (!Filesystem::isIgnoredDir($file)) {
233
+                    if (!$this->copy($source . '/' . $file, $target . '/' . $file)) {
234
+                        closedir($dir);
235
+                        return false;
236
+                    }
237
+                }
238
+            }
239
+            closedir($dir);
240
+            return true;
241
+        } else {
242
+            $sourceStream = $this->fopen($source, 'r');
243
+            $targetStream = $this->fopen($target, 'w');
244
+            [, $result] = \OC_Helper::streamCopy($sourceStream, $targetStream);
245
+            if (!$result) {
246
+                \OCP\Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target");
247
+            }
248
+            $this->removeCachedFile($target);
249
+            return $result;
250
+        }
251
+    }
252
+
253
+    public function getMimeType($path) {
254
+        if ($this->is_dir($path)) {
255
+            return 'httpd/unix-directory';
256
+        } elseif ($this->file_exists($path)) {
257
+            return \OC::$server->getMimeTypeDetector()->detectPath($path);
258
+        } else {
259
+            return false;
260
+        }
261
+    }
262
+
263
+    public function hash($type, $path, $raw = false) {
264
+        $fh = $this->fopen($path, 'rb');
265
+        $ctx = hash_init($type);
266
+        hash_update_stream($ctx, $fh);
267
+        fclose($fh);
268
+        return hash_final($ctx, $raw);
269
+    }
270
+
271
+    public function search($query) {
272
+        return $this->searchInDir($query);
273
+    }
274
+
275
+    public function getLocalFile($path) {
276
+        return $this->getCachedFile($path);
277
+    }
278
+
279
+    /**
280
+     * @param string $path
281
+     * @param string $target
282
+     */
283
+    private function addLocalFolder($path, $target) {
284
+        $dh = $this->opendir($path);
285
+        if (is_resource($dh)) {
286
+            while (($file = readdir($dh)) !== false) {
287
+                if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
288
+                    if ($this->is_dir($path . '/' . $file)) {
289
+                        mkdir($target . '/' . $file);
290
+                        $this->addLocalFolder($path . '/' . $file, $target . '/' . $file);
291
+                    } else {
292
+                        $tmp = $this->toTmpFile($path . '/' . $file);
293
+                        rename($tmp, $target . '/' . $file);
294
+                    }
295
+                }
296
+            }
297
+        }
298
+    }
299
+
300
+    /**
301
+     * @param string $query
302
+     * @param string $dir
303
+     * @return array
304
+     */
305
+    protected function searchInDir($query, $dir = '') {
306
+        $files = [];
307
+        $dh = $this->opendir($dir);
308
+        if (is_resource($dh)) {
309
+            while (($item = readdir($dh)) !== false) {
310
+                if (\OC\Files\Filesystem::isIgnoredDir($item)) {
311
+                    continue;
312
+                }
313
+                if (strstr(strtolower($item), strtolower($query)) !== false) {
314
+                    $files[] = $dir . '/' . $item;
315
+                }
316
+                if ($this->is_dir($dir . '/' . $item)) {
317
+                    $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
318
+                }
319
+            }
320
+        }
321
+        closedir($dh);
322
+        return $files;
323
+    }
324
+
325
+    /**
326
+     * Check if a file or folder has been updated since $time
327
+     *
328
+     * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
329
+     * the mtime should always return false here. As a result storage implementations that always return false expect
330
+     * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
331
+     * Nextcloud filesystem.
332
+     *
333
+     * @param string $path
334
+     * @param int $time
335
+     * @return bool
336
+     */
337
+    public function hasUpdated($path, $time) {
338
+        return $this->filemtime($path) > $time;
339
+    }
340
+
341
+    public function getCache($path = '', $storage = null) {
342
+        if (!$storage) {
343
+            $storage = $this;
344
+        }
345
+        if (!isset($storage->cache)) {
346
+            $storage->cache = new Cache($storage);
347
+        }
348
+        return $storage->cache;
349
+    }
350
+
351
+    public function getScanner($path = '', $storage = null) {
352
+        if (!$storage) {
353
+            $storage = $this;
354
+        }
355
+        if (!isset($storage->scanner)) {
356
+            $storage->scanner = new Scanner($storage);
357
+        }
358
+        return $storage->scanner;
359
+    }
360
+
361
+    public function getWatcher($path = '', $storage = null) {
362
+        if (!$storage) {
363
+            $storage = $this;
364
+        }
365
+        if (!isset($this->watcher)) {
366
+            $this->watcher = new Watcher($storage);
367
+            $globalPolicy = \OC::$server->getConfig()->getSystemValue('filesystem_check_changes', Watcher::CHECK_NEVER);
368
+            $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy));
369
+        }
370
+        return $this->watcher;
371
+    }
372
+
373
+    /**
374
+     * get a propagator instance for the cache
375
+     *
376
+     * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
377
+     * @return \OC\Files\Cache\Propagator
378
+     */
379
+    public function getPropagator($storage = null) {
380
+        if (!$storage) {
381
+            $storage = $this;
382
+        }
383
+        if (!isset($storage->propagator)) {
384
+            $config = \OC::$server->getSystemConfig();
385
+            $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getValue('instanceid')]);
386
+        }
387
+        return $storage->propagator;
388
+    }
389
+
390
+    public function getUpdater($storage = null) {
391
+        if (!$storage) {
392
+            $storage = $this;
393
+        }
394
+        if (!isset($storage->updater)) {
395
+            $storage->updater = new Updater($storage);
396
+        }
397
+        return $storage->updater;
398
+    }
399
+
400
+    public function getStorageCache($storage = null) {
401
+        if (!$storage) {
402
+            $storage = $this;
403
+        }
404
+        if (!isset($this->storageCache)) {
405
+            $this->storageCache = new \OC\Files\Cache\Storage($storage);
406
+        }
407
+        return $this->storageCache;
408
+    }
409
+
410
+    /**
411
+     * get the owner of a path
412
+     *
413
+     * @param string $path The path to get the owner
414
+     * @return string|false uid or false
415
+     */
416
+    public function getOwner($path) {
417
+        if ($this->owner === null) {
418
+            $this->owner = \OC_User::getUser();
419
+        }
420
+
421
+        return $this->owner;
422
+    }
423
+
424
+    /**
425
+     * get the ETag for a file or folder
426
+     *
427
+     * @param string $path
428
+     * @return string
429
+     */
430
+    public function getETag($path) {
431
+        return uniqid();
432
+    }
433
+
434
+    /**
435
+     * clean a path, i.e. remove all redundant '.' and '..'
436
+     * making sure that it can't point to higher than '/'
437
+     *
438
+     * @param string $path The path to clean
439
+     * @return string cleaned path
440
+     */
441
+    public function cleanPath($path) {
442
+        if (strlen($path) == 0 or $path[0] != '/') {
443
+            $path = '/' . $path;
444
+        }
445
+
446
+        $output = [];
447
+        foreach (explode('/', $path) as $chunk) {
448
+            if ($chunk == '..') {
449
+                array_pop($output);
450
+            } elseif ($chunk == '.') {
451
+            } else {
452
+                $output[] = $chunk;
453
+            }
454
+        }
455
+        return implode('/', $output);
456
+    }
457
+
458
+    /**
459
+     * Test a storage for availability
460
+     *
461
+     * @return bool
462
+     */
463
+    public function test() {
464
+        try {
465
+            if ($this->stat('')) {
466
+                return true;
467
+            }
468
+            \OC::$server->get(LoggerInterface::class)->info("External storage not available: stat() failed");
469
+            return false;
470
+        } catch (\Exception $e) {
471
+            \OC::$server->get(LoggerInterface::class)->warning(
472
+                "External storage not available: " . $e->getMessage(),
473
+                ['exception' => $e]
474
+            );
475
+            return false;
476
+        }
477
+    }
478
+
479
+    /**
480
+     * get the free space in the storage
481
+     *
482
+     * @param string $path
483
+     * @return int|float|false
484
+     */
485
+    public function free_space($path) {
486
+        return \OCP\Files\FileInfo::SPACE_UNKNOWN;
487
+    }
488
+
489
+    /**
490
+     * {@inheritdoc}
491
+     */
492
+    public function isLocal() {
493
+        // the common implementation returns a temporary file by
494
+        // default, which is not local
495
+        return false;
496
+    }
497
+
498
+    /**
499
+     * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
500
+     *
501
+     * @param string $class
502
+     * @return bool
503
+     */
504
+    public function instanceOfStorage($class) {
505
+        if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') {
506
+            // FIXME Temporary fix to keep existing checks working
507
+            $class = '\OCA\Files_Sharing\SharedStorage';
508
+        }
509
+        return is_a($this, $class);
510
+    }
511
+
512
+    /**
513
+     * A custom storage implementation can return an url for direct download of a give file.
514
+     *
515
+     * For now the returned array can hold the parameter url - in future more attributes might follow.
516
+     *
517
+     * @param string $path
518
+     * @return array|false
519
+     */
520
+    public function getDirectDownload($path) {
521
+        return [];
522
+    }
523
+
524
+    /**
525
+     * @inheritdoc
526
+     * @throws InvalidPathException
527
+     */
528
+    public function verifyPath($path, $fileName) {
529
+        // verify empty and dot files
530
+        $trimmed = trim($fileName);
531
+        if ($trimmed === '') {
532
+            throw new EmptyFileNameException();
533
+        }
534
+
535
+        if (\OC\Files\Filesystem::isIgnoredDir($trimmed)) {
536
+            throw new InvalidDirectoryException();
537
+        }
538
+
539
+        if (!\OC::$server->getDatabaseConnection()->supports4ByteText()) {
540
+            // verify database - e.g. mysql only 3-byte chars
541
+            if (preg_match('%(?:
542 542
       \xF0[\x90-\xBF][\x80-\xBF]{2}      # planes 1-3
543 543
     | [\xF1-\xF3][\x80-\xBF]{3}          # planes 4-15
544 544
     | \xF4[\x80-\x8F][\x80-\xBF]{2}      # plane 16
545 545
 )%xs', $fileName)) {
546
-				throw new InvalidCharacterInPathException();
547
-			}
548
-		}
549
-
550
-		// 255 characters is the limit on common file systems (ext/xfs)
551
-		// oc_filecache has a 250 char length limit for the filename
552
-		if (isset($fileName[250])) {
553
-			throw new FileNameTooLongException();
554
-		}
555
-
556
-		// NOTE: $path will remain unverified for now
557
-		$this->verifyPosixPath($fileName);
558
-	}
559
-
560
-	/**
561
-	 * @param string $fileName
562
-	 * @throws InvalidPathException
563
-	 */
564
-	protected function verifyPosixPath($fileName) {
565
-		$this->scanForInvalidCharacters($fileName, "\\/");
566
-		$fileName = trim($fileName);
567
-		$reservedNames = ['*'];
568
-		if (in_array($fileName, $reservedNames)) {
569
-			throw new ReservedWordException();
570
-		}
571
-	}
572
-
573
-	/**
574
-	 * @param string $fileName
575
-	 * @param string $invalidChars
576
-	 * @throws InvalidPathException
577
-	 */
578
-	private function scanForInvalidCharacters($fileName, $invalidChars) {
579
-		foreach (str_split($invalidChars) as $char) {
580
-			if (strpos($fileName, $char) !== false) {
581
-				throw new InvalidCharacterInPathException();
582
-			}
583
-		}
584
-
585
-		$sanitizedFileName = filter_var($fileName, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
586
-		if ($sanitizedFileName !== $fileName) {
587
-			throw new InvalidCharacterInPathException();
588
-		}
589
-	}
590
-
591
-	/**
592
-	 * @param array $options
593
-	 */
594
-	public function setMountOptions(array $options) {
595
-		$this->mountOptions = $options;
596
-	}
597
-
598
-	/**
599
-	 * @param string $name
600
-	 * @param mixed $default
601
-	 * @return mixed
602
-	 */
603
-	public function getMountOption($name, $default = null) {
604
-		return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default;
605
-	}
606
-
607
-	/**
608
-	 * @param IStorage $sourceStorage
609
-	 * @param string $sourceInternalPath
610
-	 * @param string $targetInternalPath
611
-	 * @param bool $preserveMtime
612
-	 * @return bool
613
-	 */
614
-	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
615
-		if ($sourceStorage === $this) {
616
-			return $this->copy($sourceInternalPath, $targetInternalPath);
617
-		}
618
-
619
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
620
-			$dh = $sourceStorage->opendir($sourceInternalPath);
621
-			$result = $this->mkdir($targetInternalPath);
622
-			if (is_resource($dh)) {
623
-				$result = true;
624
-				while ($result and ($file = readdir($dh)) !== false) {
625
-					if (!Filesystem::isIgnoredDir($file)) {
626
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
627
-					}
628
-				}
629
-			}
630
-		} else {
631
-			$source = $sourceStorage->fopen($sourceInternalPath, 'r');
632
-			$result = false;
633
-			if ($source) {
634
-				try {
635
-					$this->writeStream($targetInternalPath, $source);
636
-					$result = true;
637
-				} catch (\Exception $e) {
638
-					\OC::$server->get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
639
-				}
640
-			}
641
-
642
-			if ($result && $preserveMtime) {
643
-				$mtime = $sourceStorage->filemtime($sourceInternalPath);
644
-				$this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
645
-			}
646
-
647
-			if (!$result) {
648
-				// delete partially written target file
649
-				$this->unlink($targetInternalPath);
650
-				// delete cache entry that was created by fopen
651
-				$this->getCache()->remove($targetInternalPath);
652
-			}
653
-		}
654
-		return (bool)$result;
655
-	}
656
-
657
-	/**
658
-	 * Check if a storage is the same as the current one, including wrapped storages
659
-	 *
660
-	 * @param IStorage $storage
661
-	 * @return bool
662
-	 */
663
-	private function isSameStorage(IStorage $storage): bool {
664
-		while ($storage->instanceOfStorage(Wrapper::class)) {
665
-			/**
666
-			 * @var Wrapper $sourceStorage
667
-			 */
668
-			$storage = $storage->getWrapperStorage();
669
-		}
670
-
671
-		return $storage === $this;
672
-	}
673
-
674
-	/**
675
-	 * @param IStorage $sourceStorage
676
-	 * @param string $sourceInternalPath
677
-	 * @param string $targetInternalPath
678
-	 * @return bool
679
-	 */
680
-	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
681
-		if ($this->isSameStorage($sourceStorage)) {
682
-			// resolve any jailed paths
683
-			while ($sourceStorage->instanceOfStorage(Jail::class)) {
684
-				/**
685
-				 * @var Jail $sourceStorage
686
-				 */
687
-				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
688
-				$sourceStorage = $sourceStorage->getUnjailedStorage();
689
-			}
690
-
691
-			return $this->rename($sourceInternalPath, $targetInternalPath);
692
-		}
693
-
694
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
695
-			return false;
696
-		}
697
-
698
-		$result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
699
-		if ($result) {
700
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
701
-				$result = $sourceStorage->rmdir($sourceInternalPath);
702
-			} else {
703
-				$result = $sourceStorage->unlink($sourceInternalPath);
704
-			}
705
-		}
706
-		return $result;
707
-	}
708
-
709
-	/**
710
-	 * @inheritdoc
711
-	 */
712
-	public function getMetaData($path) {
713
-		if (Filesystem::isFileBlacklisted($path)) {
714
-			throw new ForbiddenException('Invalid path: ' . $path, false);
715
-		}
716
-
717
-		$permissions = $this->getPermissions($path);
718
-		if (!$permissions & \OCP\Constants::PERMISSION_READ) {
719
-			//can't read, nothing we can do
720
-			return null;
721
-		}
722
-
723
-		$data = [];
724
-		$data['mimetype'] = $this->getMimeType($path);
725
-		$data['mtime'] = $this->filemtime($path);
726
-		if ($data['mtime'] === false) {
727
-			$data['mtime'] = time();
728
-		}
729
-		if ($data['mimetype'] == 'httpd/unix-directory') {
730
-			$data['size'] = -1; //unknown
731
-		} else {
732
-			$data['size'] = $this->filesize($path);
733
-		}
734
-		$data['etag'] = $this->getETag($path);
735
-		$data['storage_mtime'] = $data['mtime'];
736
-		$data['permissions'] = $permissions;
737
-		$data['name'] = basename($path);
738
-
739
-		return $data;
740
-	}
741
-
742
-	/**
743
-	 * @param string $path
744
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
745
-	 * @param \OCP\Lock\ILockingProvider $provider
746
-	 * @throws \OCP\Lock\LockedException
747
-	 */
748
-	public function acquireLock($path, $type, ILockingProvider $provider) {
749
-		$logger = $this->getLockLogger();
750
-		if ($logger) {
751
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
752
-			$logger->info(
753
-				sprintf(
754
-					'acquire %s lock on "%s" on storage "%s"',
755
-					$typeString,
756
-					$path,
757
-					$this->getId()
758
-				),
759
-				[
760
-					'app' => 'locking',
761
-				]
762
-			);
763
-		}
764
-		try {
765
-			$provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
766
-		} catch (LockedException $e) {
767
-			if ($logger) {
768
-				$logger->info($e->getMessage(), ['exception' => $e]);
769
-			}
770
-			throw $e;
771
-		}
772
-	}
773
-
774
-	/**
775
-	 * @param string $path
776
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
777
-	 * @param \OCP\Lock\ILockingProvider $provider
778
-	 * @throws \OCP\Lock\LockedException
779
-	 */
780
-	public function releaseLock($path, $type, ILockingProvider $provider) {
781
-		$logger = $this->getLockLogger();
782
-		if ($logger) {
783
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
784
-			$logger->info(
785
-				sprintf(
786
-					'release %s lock on "%s" on storage "%s"',
787
-					$typeString,
788
-					$path,
789
-					$this->getId()
790
-				),
791
-				[
792
-					'app' => 'locking',
793
-				]
794
-			);
795
-		}
796
-		try {
797
-			$provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
798
-		} catch (LockedException $e) {
799
-			if ($logger) {
800
-				$logger->info($e->getMessage(), ['exception' => $e]);
801
-			}
802
-			throw $e;
803
-		}
804
-	}
805
-
806
-	/**
807
-	 * @param string $path
808
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
809
-	 * @param \OCP\Lock\ILockingProvider $provider
810
-	 * @throws \OCP\Lock\LockedException
811
-	 */
812
-	public function changeLock($path, $type, ILockingProvider $provider) {
813
-		$logger = $this->getLockLogger();
814
-		if ($logger) {
815
-			$typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
816
-			$logger->info(
817
-				sprintf(
818
-					'change lock on "%s" to %s on storage "%s"',
819
-					$path,
820
-					$typeString,
821
-					$this->getId()
822
-				),
823
-				[
824
-					'app' => 'locking',
825
-				]
826
-			);
827
-		}
828
-		try {
829
-			$provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
830
-		} catch (LockedException $e) {
831
-			if ($logger) {
832
-				$logger->info($e->getMessage(), ['exception' => $e]);
833
-			}
834
-			throw $e;
835
-		}
836
-	}
837
-
838
-	private function getLockLogger(): ?LoggerInterface {
839
-		if (is_null($this->shouldLogLocks)) {
840
-			$this->shouldLogLocks = \OC::$server->getConfig()->getSystemValue('filelocking.debug', false);
841
-			$this->logger = $this->shouldLogLocks ? \OC::$server->get(LoggerInterface::class) : null;
842
-		}
843
-		return $this->logger;
844
-	}
845
-
846
-	/**
847
-	 * @return array [ available, last_checked ]
848
-	 */
849
-	public function getAvailability() {
850
-		return $this->getStorageCache()->getAvailability();
851
-	}
852
-
853
-	/**
854
-	 * @param bool $isAvailable
855
-	 */
856
-	public function setAvailability($isAvailable) {
857
-		$this->getStorageCache()->setAvailability($isAvailable);
858
-	}
859
-
860
-	/**
861
-	 * @return bool
862
-	 */
863
-	public function needsPartFile() {
864
-		return true;
865
-	}
866
-
867
-	/**
868
-	 * fallback implementation
869
-	 *
870
-	 * @param string $path
871
-	 * @param resource $stream
872
-	 * @param int $size
873
-	 * @return int
874
-	 */
875
-	public function writeStream(string $path, $stream, int $size = null): int {
876
-		$target = $this->fopen($path, 'w');
877
-		if (!$target) {
878
-			throw new GenericFileException("Failed to open $path for writing");
879
-		}
880
-		try {
881
-			[$count, $result] = \OC_Helper::streamCopy($stream, $target);
882
-			if (!$result) {
883
-				throw new GenericFileException("Failed to copy stream");
884
-			}
885
-		} finally {
886
-			fclose($target);
887
-			fclose($stream);
888
-		}
889
-		return $count;
890
-	}
891
-
892
-	public function getDirectoryContent($directory): \Traversable {
893
-		$dh = $this->opendir($directory);
894
-		if (is_resource($dh)) {
895
-			$basePath = rtrim($directory, '/');
896
-			while (($file = readdir($dh)) !== false) {
897
-				if (!Filesystem::isIgnoredDir($file)) {
898
-					$childPath = $basePath . '/' . trim($file, '/');
899
-					$metadata = $this->getMetaData($childPath);
900
-					if ($metadata !== null) {
901
-						yield $metadata;
902
-					}
903
-				}
904
-			}
905
-		}
906
-	}
546
+                throw new InvalidCharacterInPathException();
547
+            }
548
+        }
549
+
550
+        // 255 characters is the limit on common file systems (ext/xfs)
551
+        // oc_filecache has a 250 char length limit for the filename
552
+        if (isset($fileName[250])) {
553
+            throw new FileNameTooLongException();
554
+        }
555
+
556
+        // NOTE: $path will remain unverified for now
557
+        $this->verifyPosixPath($fileName);
558
+    }
559
+
560
+    /**
561
+     * @param string $fileName
562
+     * @throws InvalidPathException
563
+     */
564
+    protected function verifyPosixPath($fileName) {
565
+        $this->scanForInvalidCharacters($fileName, "\\/");
566
+        $fileName = trim($fileName);
567
+        $reservedNames = ['*'];
568
+        if (in_array($fileName, $reservedNames)) {
569
+            throw new ReservedWordException();
570
+        }
571
+    }
572
+
573
+    /**
574
+     * @param string $fileName
575
+     * @param string $invalidChars
576
+     * @throws InvalidPathException
577
+     */
578
+    private function scanForInvalidCharacters($fileName, $invalidChars) {
579
+        foreach (str_split($invalidChars) as $char) {
580
+            if (strpos($fileName, $char) !== false) {
581
+                throw new InvalidCharacterInPathException();
582
+            }
583
+        }
584
+
585
+        $sanitizedFileName = filter_var($fileName, FILTER_UNSAFE_RAW, FILTER_FLAG_STRIP_LOW);
586
+        if ($sanitizedFileName !== $fileName) {
587
+            throw new InvalidCharacterInPathException();
588
+        }
589
+    }
590
+
591
+    /**
592
+     * @param array $options
593
+     */
594
+    public function setMountOptions(array $options) {
595
+        $this->mountOptions = $options;
596
+    }
597
+
598
+    /**
599
+     * @param string $name
600
+     * @param mixed $default
601
+     * @return mixed
602
+     */
603
+    public function getMountOption($name, $default = null) {
604
+        return isset($this->mountOptions[$name]) ? $this->mountOptions[$name] : $default;
605
+    }
606
+
607
+    /**
608
+     * @param IStorage $sourceStorage
609
+     * @param string $sourceInternalPath
610
+     * @param string $targetInternalPath
611
+     * @param bool $preserveMtime
612
+     * @return bool
613
+     */
614
+    public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
615
+        if ($sourceStorage === $this) {
616
+            return $this->copy($sourceInternalPath, $targetInternalPath);
617
+        }
618
+
619
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
620
+            $dh = $sourceStorage->opendir($sourceInternalPath);
621
+            $result = $this->mkdir($targetInternalPath);
622
+            if (is_resource($dh)) {
623
+                $result = true;
624
+                while ($result and ($file = readdir($dh)) !== false) {
625
+                    if (!Filesystem::isIgnoredDir($file)) {
626
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file);
627
+                    }
628
+                }
629
+            }
630
+        } else {
631
+            $source = $sourceStorage->fopen($sourceInternalPath, 'r');
632
+            $result = false;
633
+            if ($source) {
634
+                try {
635
+                    $this->writeStream($targetInternalPath, $source);
636
+                    $result = true;
637
+                } catch (\Exception $e) {
638
+                    \OC::$server->get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]);
639
+                }
640
+            }
641
+
642
+            if ($result && $preserveMtime) {
643
+                $mtime = $sourceStorage->filemtime($sourceInternalPath);
644
+                $this->touch($targetInternalPath, is_int($mtime) ? $mtime : null);
645
+            }
646
+
647
+            if (!$result) {
648
+                // delete partially written target file
649
+                $this->unlink($targetInternalPath);
650
+                // delete cache entry that was created by fopen
651
+                $this->getCache()->remove($targetInternalPath);
652
+            }
653
+        }
654
+        return (bool)$result;
655
+    }
656
+
657
+    /**
658
+     * Check if a storage is the same as the current one, including wrapped storages
659
+     *
660
+     * @param IStorage $storage
661
+     * @return bool
662
+     */
663
+    private function isSameStorage(IStorage $storage): bool {
664
+        while ($storage->instanceOfStorage(Wrapper::class)) {
665
+            /**
666
+             * @var Wrapper $sourceStorage
667
+             */
668
+            $storage = $storage->getWrapperStorage();
669
+        }
670
+
671
+        return $storage === $this;
672
+    }
673
+
674
+    /**
675
+     * @param IStorage $sourceStorage
676
+     * @param string $sourceInternalPath
677
+     * @param string $targetInternalPath
678
+     * @return bool
679
+     */
680
+    public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
681
+        if ($this->isSameStorage($sourceStorage)) {
682
+            // resolve any jailed paths
683
+            while ($sourceStorage->instanceOfStorage(Jail::class)) {
684
+                /**
685
+                 * @var Jail $sourceStorage
686
+                 */
687
+                $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
688
+                $sourceStorage = $sourceStorage->getUnjailedStorage();
689
+            }
690
+
691
+            return $this->rename($sourceInternalPath, $targetInternalPath);
692
+        }
693
+
694
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
695
+            return false;
696
+        }
697
+
698
+        $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true);
699
+        if ($result) {
700
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
701
+                $result = $sourceStorage->rmdir($sourceInternalPath);
702
+            } else {
703
+                $result = $sourceStorage->unlink($sourceInternalPath);
704
+            }
705
+        }
706
+        return $result;
707
+    }
708
+
709
+    /**
710
+     * @inheritdoc
711
+     */
712
+    public function getMetaData($path) {
713
+        if (Filesystem::isFileBlacklisted($path)) {
714
+            throw new ForbiddenException('Invalid path: ' . $path, false);
715
+        }
716
+
717
+        $permissions = $this->getPermissions($path);
718
+        if (!$permissions & \OCP\Constants::PERMISSION_READ) {
719
+            //can't read, nothing we can do
720
+            return null;
721
+        }
722
+
723
+        $data = [];
724
+        $data['mimetype'] = $this->getMimeType($path);
725
+        $data['mtime'] = $this->filemtime($path);
726
+        if ($data['mtime'] === false) {
727
+            $data['mtime'] = time();
728
+        }
729
+        if ($data['mimetype'] == 'httpd/unix-directory') {
730
+            $data['size'] = -1; //unknown
731
+        } else {
732
+            $data['size'] = $this->filesize($path);
733
+        }
734
+        $data['etag'] = $this->getETag($path);
735
+        $data['storage_mtime'] = $data['mtime'];
736
+        $data['permissions'] = $permissions;
737
+        $data['name'] = basename($path);
738
+
739
+        return $data;
740
+    }
741
+
742
+    /**
743
+     * @param string $path
744
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
745
+     * @param \OCP\Lock\ILockingProvider $provider
746
+     * @throws \OCP\Lock\LockedException
747
+     */
748
+    public function acquireLock($path, $type, ILockingProvider $provider) {
749
+        $logger = $this->getLockLogger();
750
+        if ($logger) {
751
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
752
+            $logger->info(
753
+                sprintf(
754
+                    'acquire %s lock on "%s" on storage "%s"',
755
+                    $typeString,
756
+                    $path,
757
+                    $this->getId()
758
+                ),
759
+                [
760
+                    'app' => 'locking',
761
+                ]
762
+            );
763
+        }
764
+        try {
765
+            $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path);
766
+        } catch (LockedException $e) {
767
+            if ($logger) {
768
+                $logger->info($e->getMessage(), ['exception' => $e]);
769
+            }
770
+            throw $e;
771
+        }
772
+    }
773
+
774
+    /**
775
+     * @param string $path
776
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
777
+     * @param \OCP\Lock\ILockingProvider $provider
778
+     * @throws \OCP\Lock\LockedException
779
+     */
780
+    public function releaseLock($path, $type, ILockingProvider $provider) {
781
+        $logger = $this->getLockLogger();
782
+        if ($logger) {
783
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
784
+            $logger->info(
785
+                sprintf(
786
+                    'release %s lock on "%s" on storage "%s"',
787
+                    $typeString,
788
+                    $path,
789
+                    $this->getId()
790
+                ),
791
+                [
792
+                    'app' => 'locking',
793
+                ]
794
+            );
795
+        }
796
+        try {
797
+            $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
798
+        } catch (LockedException $e) {
799
+            if ($logger) {
800
+                $logger->info($e->getMessage(), ['exception' => $e]);
801
+            }
802
+            throw $e;
803
+        }
804
+    }
805
+
806
+    /**
807
+     * @param string $path
808
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
809
+     * @param \OCP\Lock\ILockingProvider $provider
810
+     * @throws \OCP\Lock\LockedException
811
+     */
812
+    public function changeLock($path, $type, ILockingProvider $provider) {
813
+        $logger = $this->getLockLogger();
814
+        if ($logger) {
815
+            $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive';
816
+            $logger->info(
817
+                sprintf(
818
+                    'change lock on "%s" to %s on storage "%s"',
819
+                    $path,
820
+                    $typeString,
821
+                    $this->getId()
822
+                ),
823
+                [
824
+                    'app' => 'locking',
825
+                ]
826
+            );
827
+        }
828
+        try {
829
+            $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
830
+        } catch (LockedException $e) {
831
+            if ($logger) {
832
+                $logger->info($e->getMessage(), ['exception' => $e]);
833
+            }
834
+            throw $e;
835
+        }
836
+    }
837
+
838
+    private function getLockLogger(): ?LoggerInterface {
839
+        if (is_null($this->shouldLogLocks)) {
840
+            $this->shouldLogLocks = \OC::$server->getConfig()->getSystemValue('filelocking.debug', false);
841
+            $this->logger = $this->shouldLogLocks ? \OC::$server->get(LoggerInterface::class) : null;
842
+        }
843
+        return $this->logger;
844
+    }
845
+
846
+    /**
847
+     * @return array [ available, last_checked ]
848
+     */
849
+    public function getAvailability() {
850
+        return $this->getStorageCache()->getAvailability();
851
+    }
852
+
853
+    /**
854
+     * @param bool $isAvailable
855
+     */
856
+    public function setAvailability($isAvailable) {
857
+        $this->getStorageCache()->setAvailability($isAvailable);
858
+    }
859
+
860
+    /**
861
+     * @return bool
862
+     */
863
+    public function needsPartFile() {
864
+        return true;
865
+    }
866
+
867
+    /**
868
+     * fallback implementation
869
+     *
870
+     * @param string $path
871
+     * @param resource $stream
872
+     * @param int $size
873
+     * @return int
874
+     */
875
+    public function writeStream(string $path, $stream, int $size = null): int {
876
+        $target = $this->fopen($path, 'w');
877
+        if (!$target) {
878
+            throw new GenericFileException("Failed to open $path for writing");
879
+        }
880
+        try {
881
+            [$count, $result] = \OC_Helper::streamCopy($stream, $target);
882
+            if (!$result) {
883
+                throw new GenericFileException("Failed to copy stream");
884
+            }
885
+        } finally {
886
+            fclose($target);
887
+            fclose($stream);
888
+        }
889
+        return $count;
890
+    }
891
+
892
+    public function getDirectoryContent($directory): \Traversable {
893
+        $dh = $this->opendir($directory);
894
+        if (is_resource($dh)) {
895
+            $basePath = rtrim($directory, '/');
896
+            while (($file = readdir($dh)) !== false) {
897
+                if (!Filesystem::isIgnoredDir($file)) {
898
+                    $childPath = $basePath . '/' . trim($file, '/');
899
+                    $metadata = $this->getMetaData($childPath);
900
+                    if ($metadata !== null) {
901
+                        yield $metadata;
902
+                    }
903
+                }
904
+            }
905
+        }
906
+    }
907 907
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Command/Notify.php 1 patch
Indentation   +312 added lines, -312 removed lines patch added patch discarded remove patch
@@ -50,316 +50,316 @@
 block discarded – undo
50 50
 use Symfony\Component\Console\Output\OutputInterface;
51 51
 
52 52
 class Notify extends Base {
53
-	private GlobalStoragesService $globalService;
54
-	private IDBConnection $connection;
55
-	private LoggerInterface $logger;
56
-	/** @var IUserManager */
57
-	private $userManager;
58
-
59
-	public function __construct(
60
-		GlobalStoragesService $globalService,
61
-		IDBConnection $connection,
62
-		LoggerInterface $logger,
63
-		IUserManager $userManager
64
-	) {
65
-		parent::__construct();
66
-		$this->globalService = $globalService;
67
-		$this->connection = $connection;
68
-		$this->logger = $logger;
69
-		$this->userManager = $userManager;
70
-	}
71
-
72
-	protected function configure(): void {
73
-		$this
74
-			->setName('files_external:notify')
75
-			->setDescription('Listen for active update notifications for a configured external mount')
76
-			->addArgument(
77
-				'mount_id',
78
-				InputArgument::REQUIRED,
79
-				'the mount id of the mount to listen to'
80
-			)->addOption(
81
-				'user',
82
-				'u',
83
-				InputOption::VALUE_REQUIRED,
84
-				'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
85
-			)->addOption(
86
-				'password',
87
-				'p',
88
-				InputOption::VALUE_REQUIRED,
89
-				'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
90
-			)->addOption(
91
-				'path',
92
-				'',
93
-				InputOption::VALUE_REQUIRED,
94
-				'The directory in the storage to listen for updates in',
95
-				'/'
96
-			)->addOption(
97
-				'no-self-check',
98
-				'',
99
-				InputOption::VALUE_NONE,
100
-				'Disable self check on startup'
101
-			)->addOption(
102
-				'dry-run',
103
-				'',
104
-				InputOption::VALUE_NONE,
105
-				'Don\'t make any changes, only log detected changes'
106
-			);
107
-		parent::configure();
108
-	}
109
-
110
-	private function getUserOption(InputInterface $input): ?string {
111
-		if ($input->getOption('user')) {
112
-			return (string)$input->getOption('user');
113
-		} elseif (isset($_ENV['NOTIFY_USER'])) {
114
-			return $_ENV['NOTIFY_USER'];
115
-		} elseif (isset($_SERVER['NOTIFY_USER'])) {
116
-			return $_SERVER['NOTIFY_USER'];
117
-		} else {
118
-			return null;
119
-		}
120
-	}
121
-
122
-	private function getPasswordOption(InputInterface $input): ?string {
123
-		if ($input->getOption('password')) {
124
-			return (string)$input->getOption('password');
125
-		} elseif (isset($_ENV['NOTIFY_PASSWORD'])) {
126
-			return $_ENV['NOTIFY_PASSWORD'];
127
-		} elseif (isset($_SERVER['NOTIFY_PASSWORD'])) {
128
-			return $_SERVER['NOTIFY_PASSWORD'];
129
-		} else {
130
-			return null;
131
-		}
132
-	}
133
-
134
-	protected function execute(InputInterface $input, OutputInterface $output): int {
135
-		$mount = $this->globalService->getStorage($input->getArgument('mount_id'));
136
-		if (is_null($mount)) {
137
-			$output->writeln('<error>Mount not found</error>');
138
-			return 1;
139
-		}
140
-		$noAuth = false;
141
-
142
-		$userOption = $this->getUserOption($input);
143
-		$passwordOption = $this->getPasswordOption($input);
144
-
145
-		// if only the user is provided, we get the user object to pass along to the auth backend
146
-		// this allows using saved user credentials
147
-		$user = ($userOption && !$passwordOption) ? $this->userManager->get($userOption) : null;
148
-
149
-		try {
150
-			$authBackend = $mount->getAuthMechanism();
151
-			$authBackend->manipulateStorageConfig($mount, $user);
152
-		} catch (InsufficientDataForMeaningfulAnswerException $e) {
153
-			$noAuth = true;
154
-		} catch (StorageNotAvailableException $e) {
155
-			$noAuth = true;
156
-		}
157
-
158
-		if ($userOption) {
159
-			$mount->setBackendOption('user', $userOption);
160
-		}
161
-		if ($passwordOption) {
162
-			$mount->setBackendOption('password', $passwordOption);
163
-		}
164
-
165
-		try {
166
-			$backend = $mount->getBackend();
167
-			$backend->manipulateStorageConfig($mount, $user);
168
-		} catch (InsufficientDataForMeaningfulAnswerException $e) {
169
-			$noAuth = true;
170
-		} catch (StorageNotAvailableException $e) {
171
-			$noAuth = true;
172
-		}
173
-
174
-		try {
175
-			$storage = $this->createStorage($mount);
176
-		} catch (\Exception $e) {
177
-			$output->writeln('<error>Error while trying to create storage</error>');
178
-			if ($noAuth) {
179
-				$output->writeln('<error>Username and/or password required</error>');
180
-			}
181
-			return 1;
182
-		}
183
-		if (!$storage instanceof INotifyStorage) {
184
-			$output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
185
-			return 1;
186
-		}
187
-
188
-		$dryRun = $input->getOption('dry-run');
189
-		if ($dryRun && $output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
190
-			$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
191
-		}
192
-
193
-		$path = trim($input->getOption('path'), '/');
194
-		$notifyHandler = $storage->notify($path);
195
-
196
-		if (!$input->getOption('no-self-check')) {
197
-			$this->selfTest($storage, $notifyHandler, $output);
198
-		}
199
-
200
-		$notifyHandler->listen(function (IChange $change) use ($mount, $output, $dryRun) {
201
-			$this->logUpdate($change, $output);
202
-			if ($change instanceof IRenameChange) {
203
-				$this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output, $dryRun);
204
-			}
205
-			$this->markParentAsOutdated($mount->getId(), $change->getPath(), $output, $dryRun);
206
-		});
207
-		return 0;
208
-	}
209
-
210
-	private function createStorage(StorageConfig $mount): IStorage {
211
-		$class = $mount->getBackend()->getStorageClass();
212
-		return new $class($mount->getBackendOptions());
213
-	}
214
-
215
-	private function markParentAsOutdated($mountId, $path, OutputInterface $output, bool $dryRun) {
216
-		$parent = ltrim(dirname($path), '/');
217
-		if ($parent === '.') {
218
-			$parent = '';
219
-		}
220
-
221
-		try {
222
-			$storages = $this->getStorageIds($mountId, $parent);
223
-		} catch (DriverException $ex) {
224
-			$this->logger->warning('Error while trying to find correct storage ids.', ['exception' => $ex]);
225
-			$this->connection = $this->reconnectToDatabase($this->connection, $output);
226
-			$output->writeln('<info>Needed to reconnect to the database</info>');
227
-			$storages = $this->getStorageIds($mountId, $path);
228
-		}
229
-		if (count($storages) === 0) {
230
-			$output->writeln("  no users found with access to '$parent', skipping", OutputInterface::VERBOSITY_VERBOSE);
231
-			return;
232
-		}
233
-
234
-		$users = array_map(function (array $storage) {
235
-			return $storage['user_id'];
236
-		}, $storages);
237
-
238
-		$output->writeln("  marking '$parent' as outdated for " . implode(', ', $users), OutputInterface::VERBOSITY_VERBOSE);
239
-
240
-		$storageIds = array_map(function (array $storage) {
241
-			return intval($storage['storage_id']);
242
-		}, $storages);
243
-		$storageIds = array_values(array_unique($storageIds));
244
-
245
-		if ($dryRun) {
246
-			$output->writeln("  dry-run: skipping database write");
247
-		} else {
248
-			$result = $this->updateParent($storageIds, $parent);
249
-			if ($result === 0) {
250
-				//TODO: Find existing parent further up the tree in the database and register that folder instead.
251
-				$this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.');
252
-			}
253
-		}
254
-	}
255
-
256
-	private function logUpdate(IChange $change, OutputInterface $output) {
257
-		switch ($change->getType()) {
258
-			case INotifyStorage::NOTIFY_ADDED:
259
-				$text = 'added';
260
-				break;
261
-			case INotifyStorage::NOTIFY_MODIFIED:
262
-				$text = 'modified';
263
-				break;
264
-			case INotifyStorage::NOTIFY_REMOVED:
265
-				$text = 'removed';
266
-				break;
267
-			case INotifyStorage::NOTIFY_RENAMED:
268
-				$text = 'renamed';
269
-				break;
270
-			default:
271
-				return;
272
-		}
273
-
274
-		$text .= ' ' . $change->getPath();
275
-		if ($change instanceof IRenameChange) {
276
-			$text .= ' to ' . $change->getTargetPath();
277
-		}
278
-
279
-		$output->writeln($text, OutputInterface::VERBOSITY_VERBOSE);
280
-	}
281
-
282
-	private function getStorageIds(int $mountId, string $path): array {
283
-		$pathHash = md5(trim((string)\OC_Util::normalizeUnicode($path), '/'));
284
-		$qb = $this->connection->getQueryBuilder();
285
-		return $qb
286
-			->select('storage_id', 'user_id')
287
-			->from('mounts', 'm')
288
-			->innerJoin('m', 'filecache', 'f', $qb->expr()->eq('m.storage_id', 'f.storage'))
289
-			->where($qb->expr()->eq('mount_id', $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
290
-			->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
291
-			->execute()
292
-			->fetchAll();
293
-	}
294
-
295
-	private function updateParent(array $storageIds, string $parent): int {
296
-		$pathHash = md5(trim((string)\OC_Util::normalizeUnicode($parent), '/'));
297
-		$qb = $this->connection->getQueryBuilder();
298
-		return $qb
299
-			->update('filecache')
300
-			->set('size', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
301
-			->where($qb->expr()->in('storage', $qb->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY, ':storage_ids')))
302
-			->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
303
-			->executeStatement();
304
-	}
305
-
306
-	private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output): IDBConnection {
307
-		try {
308
-			$connection->close();
309
-		} catch (\Exception $ex) {
310
-			$this->logger->warning('Error while disconnecting from DB', ['exception' => $ex]);
311
-			$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
312
-		}
313
-		$connected = false;
314
-		while (!$connected) {
315
-			try {
316
-				$connected = $connection->connect();
317
-			} catch (\Exception $ex) {
318
-				$this->logger->warning('Error while re-connecting to database', ['exception' => $ex]);
319
-				$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
320
-				sleep(60);
321
-			}
322
-		}
323
-		return $connection;
324
-	}
325
-
326
-
327
-	private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, OutputInterface $output) {
328
-		usleep(100 * 1000); //give time for the notify to start
329
-		if (!$storage->file_put_contents('/.nc_test_file.txt', 'test content')) {
330
-			$output->writeln("Failed to create test file for self-test");
331
-			return;
332
-		}
333
-		$storage->mkdir('/.nc_test_folder');
334
-		$storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content');
335
-
336
-		usleep(100 * 1000); //time for all changes to be processed
337
-		$changes = $notifyHandler->getChanges();
338
-
339
-		$storage->unlink('/.nc_test_file.txt');
340
-		$storage->unlink('/.nc_test_folder/subfile.txt');
341
-		$storage->rmdir('/.nc_test_folder');
342
-
343
-		usleep(100 * 1000); //time for all changes to be processed
344
-		$notifyHandler->getChanges(); // flush
345
-
346
-		$foundRootChange = false;
347
-		$foundSubfolderChange = false;
348
-
349
-		foreach ($changes as $change) {
350
-			if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') {
351
-				$foundRootChange = true;
352
-			} elseif ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') {
353
-				$foundSubfolderChange = true;
354
-			}
355
-		}
356
-
357
-		if ($foundRootChange && $foundSubfolderChange) {
358
-			$output->writeln('<info>Self-test successful</info>', OutputInterface::VERBOSITY_VERBOSE);
359
-		} elseif ($foundRootChange) {
360
-			$output->writeln('<error>Error while running self-test, change is subfolder not detected</error>');
361
-		} else {
362
-			$output->writeln('<error>Error while running self-test, no changes detected</error>');
363
-		}
364
-	}
53
+    private GlobalStoragesService $globalService;
54
+    private IDBConnection $connection;
55
+    private LoggerInterface $logger;
56
+    /** @var IUserManager */
57
+    private $userManager;
58
+
59
+    public function __construct(
60
+        GlobalStoragesService $globalService,
61
+        IDBConnection $connection,
62
+        LoggerInterface $logger,
63
+        IUserManager $userManager
64
+    ) {
65
+        parent::__construct();
66
+        $this->globalService = $globalService;
67
+        $this->connection = $connection;
68
+        $this->logger = $logger;
69
+        $this->userManager = $userManager;
70
+    }
71
+
72
+    protected function configure(): void {
73
+        $this
74
+            ->setName('files_external:notify')
75
+            ->setDescription('Listen for active update notifications for a configured external mount')
76
+            ->addArgument(
77
+                'mount_id',
78
+                InputArgument::REQUIRED,
79
+                'the mount id of the mount to listen to'
80
+            )->addOption(
81
+                'user',
82
+                'u',
83
+                InputOption::VALUE_REQUIRED,
84
+                'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
85
+            )->addOption(
86
+                'password',
87
+                'p',
88
+                InputOption::VALUE_REQUIRED,
89
+                'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
90
+            )->addOption(
91
+                'path',
92
+                '',
93
+                InputOption::VALUE_REQUIRED,
94
+                'The directory in the storage to listen for updates in',
95
+                '/'
96
+            )->addOption(
97
+                'no-self-check',
98
+                '',
99
+                InputOption::VALUE_NONE,
100
+                'Disable self check on startup'
101
+            )->addOption(
102
+                'dry-run',
103
+                '',
104
+                InputOption::VALUE_NONE,
105
+                'Don\'t make any changes, only log detected changes'
106
+            );
107
+        parent::configure();
108
+    }
109
+
110
+    private function getUserOption(InputInterface $input): ?string {
111
+        if ($input->getOption('user')) {
112
+            return (string)$input->getOption('user');
113
+        } elseif (isset($_ENV['NOTIFY_USER'])) {
114
+            return $_ENV['NOTIFY_USER'];
115
+        } elseif (isset($_SERVER['NOTIFY_USER'])) {
116
+            return $_SERVER['NOTIFY_USER'];
117
+        } else {
118
+            return null;
119
+        }
120
+    }
121
+
122
+    private function getPasswordOption(InputInterface $input): ?string {
123
+        if ($input->getOption('password')) {
124
+            return (string)$input->getOption('password');
125
+        } elseif (isset($_ENV['NOTIFY_PASSWORD'])) {
126
+            return $_ENV['NOTIFY_PASSWORD'];
127
+        } elseif (isset($_SERVER['NOTIFY_PASSWORD'])) {
128
+            return $_SERVER['NOTIFY_PASSWORD'];
129
+        } else {
130
+            return null;
131
+        }
132
+    }
133
+
134
+    protected function execute(InputInterface $input, OutputInterface $output): int {
135
+        $mount = $this->globalService->getStorage($input->getArgument('mount_id'));
136
+        if (is_null($mount)) {
137
+            $output->writeln('<error>Mount not found</error>');
138
+            return 1;
139
+        }
140
+        $noAuth = false;
141
+
142
+        $userOption = $this->getUserOption($input);
143
+        $passwordOption = $this->getPasswordOption($input);
144
+
145
+        // if only the user is provided, we get the user object to pass along to the auth backend
146
+        // this allows using saved user credentials
147
+        $user = ($userOption && !$passwordOption) ? $this->userManager->get($userOption) : null;
148
+
149
+        try {
150
+            $authBackend = $mount->getAuthMechanism();
151
+            $authBackend->manipulateStorageConfig($mount, $user);
152
+        } catch (InsufficientDataForMeaningfulAnswerException $e) {
153
+            $noAuth = true;
154
+        } catch (StorageNotAvailableException $e) {
155
+            $noAuth = true;
156
+        }
157
+
158
+        if ($userOption) {
159
+            $mount->setBackendOption('user', $userOption);
160
+        }
161
+        if ($passwordOption) {
162
+            $mount->setBackendOption('password', $passwordOption);
163
+        }
164
+
165
+        try {
166
+            $backend = $mount->getBackend();
167
+            $backend->manipulateStorageConfig($mount, $user);
168
+        } catch (InsufficientDataForMeaningfulAnswerException $e) {
169
+            $noAuth = true;
170
+        } catch (StorageNotAvailableException $e) {
171
+            $noAuth = true;
172
+        }
173
+
174
+        try {
175
+            $storage = $this->createStorage($mount);
176
+        } catch (\Exception $e) {
177
+            $output->writeln('<error>Error while trying to create storage</error>');
178
+            if ($noAuth) {
179
+                $output->writeln('<error>Username and/or password required</error>');
180
+            }
181
+            return 1;
182
+        }
183
+        if (!$storage instanceof INotifyStorage) {
184
+            $output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
185
+            return 1;
186
+        }
187
+
188
+        $dryRun = $input->getOption('dry-run');
189
+        if ($dryRun && $output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
190
+            $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
191
+        }
192
+
193
+        $path = trim($input->getOption('path'), '/');
194
+        $notifyHandler = $storage->notify($path);
195
+
196
+        if (!$input->getOption('no-self-check')) {
197
+            $this->selfTest($storage, $notifyHandler, $output);
198
+        }
199
+
200
+        $notifyHandler->listen(function (IChange $change) use ($mount, $output, $dryRun) {
201
+            $this->logUpdate($change, $output);
202
+            if ($change instanceof IRenameChange) {
203
+                $this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output, $dryRun);
204
+            }
205
+            $this->markParentAsOutdated($mount->getId(), $change->getPath(), $output, $dryRun);
206
+        });
207
+        return 0;
208
+    }
209
+
210
+    private function createStorage(StorageConfig $mount): IStorage {
211
+        $class = $mount->getBackend()->getStorageClass();
212
+        return new $class($mount->getBackendOptions());
213
+    }
214
+
215
+    private function markParentAsOutdated($mountId, $path, OutputInterface $output, bool $dryRun) {
216
+        $parent = ltrim(dirname($path), '/');
217
+        if ($parent === '.') {
218
+            $parent = '';
219
+        }
220
+
221
+        try {
222
+            $storages = $this->getStorageIds($mountId, $parent);
223
+        } catch (DriverException $ex) {
224
+            $this->logger->warning('Error while trying to find correct storage ids.', ['exception' => $ex]);
225
+            $this->connection = $this->reconnectToDatabase($this->connection, $output);
226
+            $output->writeln('<info>Needed to reconnect to the database</info>');
227
+            $storages = $this->getStorageIds($mountId, $path);
228
+        }
229
+        if (count($storages) === 0) {
230
+            $output->writeln("  no users found with access to '$parent', skipping", OutputInterface::VERBOSITY_VERBOSE);
231
+            return;
232
+        }
233
+
234
+        $users = array_map(function (array $storage) {
235
+            return $storage['user_id'];
236
+        }, $storages);
237
+
238
+        $output->writeln("  marking '$parent' as outdated for " . implode(', ', $users), OutputInterface::VERBOSITY_VERBOSE);
239
+
240
+        $storageIds = array_map(function (array $storage) {
241
+            return intval($storage['storage_id']);
242
+        }, $storages);
243
+        $storageIds = array_values(array_unique($storageIds));
244
+
245
+        if ($dryRun) {
246
+            $output->writeln("  dry-run: skipping database write");
247
+        } else {
248
+            $result = $this->updateParent($storageIds, $parent);
249
+            if ($result === 0) {
250
+                //TODO: Find existing parent further up the tree in the database and register that folder instead.
251
+                $this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.');
252
+            }
253
+        }
254
+    }
255
+
256
+    private function logUpdate(IChange $change, OutputInterface $output) {
257
+        switch ($change->getType()) {
258
+            case INotifyStorage::NOTIFY_ADDED:
259
+                $text = 'added';
260
+                break;
261
+            case INotifyStorage::NOTIFY_MODIFIED:
262
+                $text = 'modified';
263
+                break;
264
+            case INotifyStorage::NOTIFY_REMOVED:
265
+                $text = 'removed';
266
+                break;
267
+            case INotifyStorage::NOTIFY_RENAMED:
268
+                $text = 'renamed';
269
+                break;
270
+            default:
271
+                return;
272
+        }
273
+
274
+        $text .= ' ' . $change->getPath();
275
+        if ($change instanceof IRenameChange) {
276
+            $text .= ' to ' . $change->getTargetPath();
277
+        }
278
+
279
+        $output->writeln($text, OutputInterface::VERBOSITY_VERBOSE);
280
+    }
281
+
282
+    private function getStorageIds(int $mountId, string $path): array {
283
+        $pathHash = md5(trim((string)\OC_Util::normalizeUnicode($path), '/'));
284
+        $qb = $this->connection->getQueryBuilder();
285
+        return $qb
286
+            ->select('storage_id', 'user_id')
287
+            ->from('mounts', 'm')
288
+            ->innerJoin('m', 'filecache', 'f', $qb->expr()->eq('m.storage_id', 'f.storage'))
289
+            ->where($qb->expr()->eq('mount_id', $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
290
+            ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
291
+            ->execute()
292
+            ->fetchAll();
293
+    }
294
+
295
+    private function updateParent(array $storageIds, string $parent): int {
296
+        $pathHash = md5(trim((string)\OC_Util::normalizeUnicode($parent), '/'));
297
+        $qb = $this->connection->getQueryBuilder();
298
+        return $qb
299
+            ->update('filecache')
300
+            ->set('size', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
301
+            ->where($qb->expr()->in('storage', $qb->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY, ':storage_ids')))
302
+            ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
303
+            ->executeStatement();
304
+    }
305
+
306
+    private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output): IDBConnection {
307
+        try {
308
+            $connection->close();
309
+        } catch (\Exception $ex) {
310
+            $this->logger->warning('Error while disconnecting from DB', ['exception' => $ex]);
311
+            $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
312
+        }
313
+        $connected = false;
314
+        while (!$connected) {
315
+            try {
316
+                $connected = $connection->connect();
317
+            } catch (\Exception $ex) {
318
+                $this->logger->warning('Error while re-connecting to database', ['exception' => $ex]);
319
+                $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
320
+                sleep(60);
321
+            }
322
+        }
323
+        return $connection;
324
+    }
325
+
326
+
327
+    private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, OutputInterface $output) {
328
+        usleep(100 * 1000); //give time for the notify to start
329
+        if (!$storage->file_put_contents('/.nc_test_file.txt', 'test content')) {
330
+            $output->writeln("Failed to create test file for self-test");
331
+            return;
332
+        }
333
+        $storage->mkdir('/.nc_test_folder');
334
+        $storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content');
335
+
336
+        usleep(100 * 1000); //time for all changes to be processed
337
+        $changes = $notifyHandler->getChanges();
338
+
339
+        $storage->unlink('/.nc_test_file.txt');
340
+        $storage->unlink('/.nc_test_folder/subfile.txt');
341
+        $storage->rmdir('/.nc_test_folder');
342
+
343
+        usleep(100 * 1000); //time for all changes to be processed
344
+        $notifyHandler->getChanges(); // flush
345
+
346
+        $foundRootChange = false;
347
+        $foundSubfolderChange = false;
348
+
349
+        foreach ($changes as $change) {
350
+            if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') {
351
+                $foundRootChange = true;
352
+            } elseif ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') {
353
+                $foundSubfolderChange = true;
354
+            }
355
+        }
356
+
357
+        if ($foundRootChange && $foundSubfolderChange) {
358
+            $output->writeln('<info>Self-test successful</info>', OutputInterface::VERBOSITY_VERBOSE);
359
+        } elseif ($foundRootChange) {
360
+            $output->writeln('<error>Error while running self-test, change is subfolder not detected</error>');
361
+        } else {
362
+            $output->writeln('<error>Error while running self-test, no changes detected</error>');
363
+        }
364
+    }
365 365
 }
Please login to merge, or discard this patch.