Passed
Push — master ( 83ed1f...db0268 )
by John
16:12 queued 12s
created
apps/files_sharing/lib/Scanner.php 1 patch
Indentation   +44 added lines, -44 removed lines patch added patch discarded remove patch
@@ -31,52 +31,52 @@
 block discarded – undo
31 31
  * Scanner for SharedStorage
32 32
  */
33 33
 class Scanner extends \OC\Files\Cache\Scanner {
34
-	/**
35
-	 * @var \OCA\Files_Sharing\SharedStorage $storage
36
-	 */
37
-	protected $storage;
34
+    /**
35
+     * @var \OCA\Files_Sharing\SharedStorage $storage
36
+     */
37
+    protected $storage;
38 38
 
39
-	private $sourceScanner;
39
+    private $sourceScanner;
40 40
 
41
-	/**
42
-	 * Returns metadata from the shared storage, but
43
-	 * with permissions from the source storage.
44
-	 *
45
-	 * @param string $path path of the file for which to retrieve metadata
46
-	 *
47
-	 * @return array|null an array of metadata of the file
48
-	 */
49
-	public function getData($path) {
50
-		$data = parent::getData($path);
51
-		if ($data === null) {
52
-			return null;
53
-		}
54
-		$internalPath = $this->storage->getUnjailedPath($path);
55
-		$data['permissions'] = $this->storage->getSourceStorage()->getPermissions($internalPath);
56
-		return $data;
57
-	}
41
+    /**
42
+     * Returns metadata from the shared storage, but
43
+     * with permissions from the source storage.
44
+     *
45
+     * @param string $path path of the file for which to retrieve metadata
46
+     *
47
+     * @return array|null an array of metadata of the file
48
+     */
49
+    public function getData($path) {
50
+        $data = parent::getData($path);
51
+        if ($data === null) {
52
+            return null;
53
+        }
54
+        $internalPath = $this->storage->getUnjailedPath($path);
55
+        $data['permissions'] = $this->storage->getSourceStorage()->getPermissions($internalPath);
56
+        return $data;
57
+    }
58 58
 
59
-	private function getSourceScanner() {
60
-		if ($this->sourceScanner) {
61
-			return $this->sourceScanner;
62
-		}
63
-		if ($this->storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
64
-			/** @var \OC\Files\Storage\Storage $storage */
65
-			[$storage] = $this->storage->resolvePath('');
66
-			$this->sourceScanner = $storage->getScanner();
67
-			return $this->sourceScanner;
68
-		} else {
69
-			return null;
70
-		}
71
-	}
59
+    private function getSourceScanner() {
60
+        if ($this->sourceScanner) {
61
+            return $this->sourceScanner;
62
+        }
63
+        if ($this->storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) {
64
+            /** @var \OC\Files\Storage\Storage $storage */
65
+            [$storage] = $this->storage->resolvePath('');
66
+            $this->sourceScanner = $storage->getScanner();
67
+            return $this->sourceScanner;
68
+        } else {
69
+            return null;
70
+        }
71
+    }
72 72
 
73
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
74
-		$sourceScanner = $this->getSourceScanner();
75
-		if ($sourceScanner instanceof ObjectStoreScanner) {
76
-			// ObjectStoreScanner doesn't scan
77
-			return [];
78
-		} else {
79
-			return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock);
80
-		}
81
-	}
73
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
74
+        $sourceScanner = $this->getSourceScanner();
75
+        if ($sourceScanner instanceof ObjectStoreScanner) {
76
+            // ObjectStoreScanner doesn't scan
77
+            return [];
78
+        } else {
79
+            return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock);
80
+        }
81
+    }
82 82
 }
Please login to merge, or discard this patch.
lib/private/Files/ObjectStore/ObjectStoreScanner.php 2 patches
Indentation   +57 added lines, -57 removed lines patch added patch discarded remove patch
@@ -31,68 +31,68 @@
 block discarded – undo
31 31
 use OCP\Files\FileInfo;
32 32
 
33 33
 class ObjectStoreScanner extends Scanner {
34
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
35
-		return [];
36
-	}
34
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
35
+        return [];
36
+    }
37 37
 
38
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
39
-		return [];
40
-	}
38
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
39
+        return [];
40
+    }
41 41
 
42
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
43
-		return 0;
44
-	}
42
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
43
+        return 0;
44
+    }
45 45
 
46
-	public function backgroundScan() {
47
-		$lastPath = null;
48
-		// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
49
-		// we sort by path DESC to ensure that contents of a folder are handled before the parent folder
50
-		while (($path = $this->getIncomplete()) !== false && $path !== $lastPath) {
51
-			$this->runBackgroundScanJob(function () use ($path) {
52
-				$item = $this->cache->get($path);
53
-				if ($item && $item->getMimeType() !== FileInfo::MIMETYPE_FOLDER) {
54
-					$fh = $this->storage->fopen($path, 'r');
55
-					if ($fh) {
56
-						$stat = fstat($fh);
57
-						if ($stat['size']) {
58
-							$this->cache->update($item->getId(), ['size' => $stat['size']]);
59
-						}
60
-					}
61
-				}
62
-			}, $path);
63
-			// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
64
-			// to make this possible
65
-			$lastPath = $path;
66
-		}
67
-	}
46
+    public function backgroundScan() {
47
+        $lastPath = null;
48
+        // find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
49
+        // we sort by path DESC to ensure that contents of a folder are handled before the parent folder
50
+        while (($path = $this->getIncomplete()) !== false && $path !== $lastPath) {
51
+            $this->runBackgroundScanJob(function () use ($path) {
52
+                $item = $this->cache->get($path);
53
+                if ($item && $item->getMimeType() !== FileInfo::MIMETYPE_FOLDER) {
54
+                    $fh = $this->storage->fopen($path, 'r');
55
+                    if ($fh) {
56
+                        $stat = fstat($fh);
57
+                        if ($stat['size']) {
58
+                            $this->cache->update($item->getId(), ['size' => $stat['size']]);
59
+                        }
60
+                    }
61
+                }
62
+            }, $path);
63
+            // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
64
+            // to make this possible
65
+            $lastPath = $path;
66
+        }
67
+    }
68 68
 
69
-	/**
70
-	 * Unlike the default Cache::getIncomplete this one sorts by path.
71
-	 *
72
-	 * This is needed since self::backgroundScan doesn't fix child entries when running on a parent folder.
73
-	 * By sorting by path we ensure that we encounter the child entries first.
74
-	 *
75
-	 * @return false|string
76
-	 * @throws \OCP\DB\Exception
77
-	 */
78
-	private function getIncomplete() {
79
-		$query = $this->connection->getQueryBuilder();
80
-		$query->select('path')
81
-			->from('filecache')
82
-			->where($query->expr()->eq('storage', $query->createNamedParameter($this->cache->getNumericStorageId(), IQueryBuilder::PARAM_INT)))
83
-			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
84
-			->orderBy('path', 'DESC')
85
-			->setMaxResults(1);
69
+    /**
70
+     * Unlike the default Cache::getIncomplete this one sorts by path.
71
+     *
72
+     * This is needed since self::backgroundScan doesn't fix child entries when running on a parent folder.
73
+     * By sorting by path we ensure that we encounter the child entries first.
74
+     *
75
+     * @return false|string
76
+     * @throws \OCP\DB\Exception
77
+     */
78
+    private function getIncomplete() {
79
+        $query = $this->connection->getQueryBuilder();
80
+        $query->select('path')
81
+            ->from('filecache')
82
+            ->where($query->expr()->eq('storage', $query->createNamedParameter($this->cache->getNumericStorageId(), IQueryBuilder::PARAM_INT)))
83
+            ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
84
+            ->orderBy('path', 'DESC')
85
+            ->setMaxResults(1);
86 86
 
87
-		$result = $query->executeQuery();
88
-		$path = $result->fetchOne();
89
-		$result->closeCursor();
87
+        $result = $query->executeQuery();
88
+        $path = $result->fetchOne();
89
+        $result->closeCursor();
90 90
 
91
-		if ($path === false) {
92
-			return false;
93
-		}
91
+        if ($path === false) {
92
+            return false;
93
+        }
94 94
 
95
-		// Make sure Oracle does not continue with null for empty strings
96
-		return (string)$path;
97
-	}
95
+        // Make sure Oracle does not continue with null for empty strings
96
+        return (string)$path;
97
+    }
98 98
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -48,7 +48,7 @@  discard block
 block discarded – undo
48 48
 		// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
49 49
 		// we sort by path DESC to ensure that contents of a folder are handled before the parent folder
50 50
 		while (($path = $this->getIncomplete()) !== false && $path !== $lastPath) {
51
-			$this->runBackgroundScanJob(function () use ($path) {
51
+			$this->runBackgroundScanJob(function() use ($path) {
52 52
 				$item = $this->cache->get($path);
53 53
 				if ($item && $item->getMimeType() !== FileInfo::MIMETYPE_FOLDER) {
54 54
 					$fh = $this->storage->fopen($path, 'r');
@@ -93,6 +93,6 @@  discard block
 block discarded – undo
93 93
 		}
94 94
 
95 95
 		// Make sure Oracle does not continue with null for empty strings
96
-		return (string)$path;
96
+		return (string) $path;
97 97
 	}
98 98
 }
Please login to merge, or discard this patch.
lib/private/Files/ObjectStore/ObjectStoreStorage.php 1 patch
Indentation   +665 added lines, -665 removed lines patch added patch discarded remove patch
@@ -48,672 +48,672 @@
 block discarded – undo
48 48
 use OCP\Files\Storage\IStorage;
49 49
 
50 50
 class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
51
-	use CopyDirectory;
52
-
53
-	/**
54
-	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
55
-	 */
56
-	protected $objectStore;
57
-	/**
58
-	 * @var string $id
59
-	 */
60
-	protected $id;
61
-	/**
62
-	 * @var \OC\User\User $user
63
-	 */
64
-	protected $user;
65
-
66
-	private $objectPrefix = 'urn:oid:';
67
-
68
-	private $logger;
69
-
70
-	/** @var bool */
71
-	protected $validateWrites = true;
72
-
73
-	public function __construct($params) {
74
-		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
75
-			$this->objectStore = $params['objectstore'];
76
-		} else {
77
-			throw new \Exception('missing IObjectStore instance');
78
-		}
79
-		if (isset($params['storageid'])) {
80
-			$this->id = 'object::store:' . $params['storageid'];
81
-		} else {
82
-			$this->id = 'object::store:' . $this->objectStore->getStorageId();
83
-		}
84
-		if (isset($params['objectPrefix'])) {
85
-			$this->objectPrefix = $params['objectPrefix'];
86
-		}
87
-		if (isset($params['validateWrites'])) {
88
-			$this->validateWrites = (bool)$params['validateWrites'];
89
-		}
90
-		//initialize cache with root directory in cache
91
-		if (!$this->is_dir('/')) {
92
-			$this->mkdir('/');
93
-		}
94
-
95
-		$this->logger = \OC::$server->getLogger();
96
-	}
97
-
98
-	public function mkdir($path) {
99
-		$path = $this->normalizePath($path);
100
-		if ($this->file_exists($path)) {
101
-			$this->logger->warning("Tried to create an object store folder that already exists: $path");
102
-			return false;
103
-		}
104
-
105
-		$mTime = time();
106
-		$data = [
107
-			'mimetype' => 'httpd/unix-directory',
108
-			'size' => 0,
109
-			'mtime' => $mTime,
110
-			'storage_mtime' => $mTime,
111
-			'permissions' => \OCP\Constants::PERMISSION_ALL,
112
-		];
113
-		if ($path === '') {
114
-			//create root on the fly
115
-			$data['etag'] = $this->getETag('');
116
-			$this->getCache()->put('', $data);
117
-			return true;
118
-		} else {
119
-			// if parent does not exist, create it
120
-			$parent = $this->normalizePath(dirname($path));
121
-			$parentType = $this->filetype($parent);
122
-			if ($parentType === false) {
123
-				if (!$this->mkdir($parent)) {
124
-					// something went wrong
125
-					$this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created");
126
-					return false;
127
-				}
128
-			} elseif ($parentType === 'file') {
129
-				// parent is a file
130
-				$this->logger->warning("Parent ($parent) is a file");
131
-				return false;
132
-			}
133
-			// finally create the new dir
134
-			$mTime = time(); // update mtime
135
-			$data['mtime'] = $mTime;
136
-			$data['storage_mtime'] = $mTime;
137
-			$data['etag'] = $this->getETag($path);
138
-			$this->getCache()->put($path, $data);
139
-			return true;
140
-		}
141
-	}
142
-
143
-	/**
144
-	 * @param string $path
145
-	 * @return string
146
-	 */
147
-	private function normalizePath($path) {
148
-		$path = trim($path, '/');
149
-		//FIXME why do we sometimes get a path like 'files//username'?
150
-		$path = str_replace('//', '/', $path);
151
-
152
-		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
153
-		if (!$path || $path === '.') {
154
-			$path = '';
155
-		}
156
-
157
-		return $path;
158
-	}
159
-
160
-	/**
161
-	 * Object Stores use a NoopScanner because metadata is directly stored in
162
-	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
163
-	 *
164
-	 * @param string $path
165
-	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
166
-	 * @return \OC\Files\ObjectStore\ObjectStoreScanner
167
-	 */
168
-	public function getScanner($path = '', $storage = null) {
169
-		if (!$storage) {
170
-			$storage = $this;
171
-		}
172
-		if (!isset($this->scanner)) {
173
-			$this->scanner = new ObjectStoreScanner($storage);
174
-		}
175
-		return $this->scanner;
176
-	}
177
-
178
-	public function getId() {
179
-		return $this->id;
180
-	}
181
-
182
-	public function rmdir($path) {
183
-		$path = $this->normalizePath($path);
184
-		$entry = $this->getCache()->get($path);
185
-
186
-		if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
187
-			return false;
188
-		}
189
-
190
-		return $this->rmObjects($entry);
191
-	}
192
-
193
-	private function rmObjects(ICacheEntry $entry): bool {
194
-		$children = $this->getCache()->getFolderContentsById($entry->getId());
195
-		foreach ($children as $child) {
196
-			if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
197
-				if (!$this->rmObjects($child)) {
198
-					return false;
199
-				}
200
-			} else {
201
-				if (!$this->rmObject($child)) {
202
-					return false;
203
-				}
204
-			}
205
-		}
206
-
207
-		$this->getCache()->remove($entry->getPath());
208
-
209
-		return true;
210
-	}
211
-
212
-	public function unlink($path) {
213
-		$path = $this->normalizePath($path);
214
-		$entry = $this->getCache()->get($path);
215
-
216
-		if ($entry instanceof ICacheEntry) {
217
-			if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
218
-				return $this->rmObjects($entry);
219
-			} else {
220
-				return $this->rmObject($entry);
221
-			}
222
-		}
223
-		return false;
224
-	}
225
-
226
-	public function rmObject(ICacheEntry $entry): bool {
227
-		try {
228
-			$this->objectStore->deleteObject($this->getURN($entry->getId()));
229
-		} catch (\Exception $ex) {
230
-			if ($ex->getCode() !== 404) {
231
-				$this->logger->logException($ex, [
232
-					'app' => 'objectstore',
233
-					'message' => 'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
234
-				]);
235
-				return false;
236
-			}
237
-			//removing from cache is ok as it does not exist in the objectstore anyway
238
-		}
239
-		$this->getCache()->remove($entry->getPath());
240
-		return true;
241
-	}
242
-
243
-	public function stat($path) {
244
-		$path = $this->normalizePath($path);
245
-		$cacheEntry = $this->getCache()->get($path);
246
-		if ($cacheEntry instanceof CacheEntry) {
247
-			return $cacheEntry->getData();
248
-		} else {
249
-			return false;
250
-		}
251
-	}
252
-
253
-	public function getPermissions($path) {
254
-		$stat = $this->stat($path);
255
-
256
-		if (is_array($stat) && isset($stat['permissions'])) {
257
-			return $stat['permissions'];
258
-		}
259
-
260
-		return parent::getPermissions($path);
261
-	}
262
-
263
-	/**
264
-	 * Override this method if you need a different unique resource identifier for your object storage implementation.
265
-	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
266
-	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
267
-	 *
268
-	 * @param int $fileId the fileid
269
-	 * @return null|string the unified resource name used to identify the object
270
-	 */
271
-	public function getURN($fileId) {
272
-		if (is_numeric($fileId)) {
273
-			return $this->objectPrefix . $fileId;
274
-		}
275
-		return null;
276
-	}
277
-
278
-	public function opendir($path) {
279
-		$path = $this->normalizePath($path);
280
-
281
-		try {
282
-			$files = [];
283
-			$folderContents = $this->getCache()->getFolderContents($path);
284
-			foreach ($folderContents as $file) {
285
-				$files[] = $file['name'];
286
-			}
287
-
288
-			return IteratorDirectory::wrap($files);
289
-		} catch (\Exception $e) {
290
-			$this->logger->logException($e);
291
-			return false;
292
-		}
293
-	}
294
-
295
-	public function filetype($path) {
296
-		$path = $this->normalizePath($path);
297
-		$stat = $this->stat($path);
298
-		if ($stat) {
299
-			if ($stat['mimetype'] === 'httpd/unix-directory') {
300
-				return 'dir';
301
-			}
302
-			return 'file';
303
-		} else {
304
-			return false;
305
-		}
306
-	}
307
-
308
-	public function fopen($path, $mode) {
309
-		$path = $this->normalizePath($path);
310
-
311
-		if (strrpos($path, '.') !== false) {
312
-			$ext = substr($path, strrpos($path, '.'));
313
-		} else {
314
-			$ext = '';
315
-		}
316
-
317
-		switch ($mode) {
318
-			case 'r':
319
-			case 'rb':
320
-				$stat = $this->stat($path);
321
-				if (is_array($stat)) {
322
-					$filesize = $stat['size'] ?? 0;
323
-					// Reading 0 sized files is a waste of time
324
-					if ($filesize === 0) {
325
-						return fopen('php://memory', $mode);
326
-					}
327
-
328
-					try {
329
-						$handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
330
-						if ($handle === false) {
331
-							return false; // keep backward compatibility
332
-						}
333
-						$streamStat = fstat($handle);
334
-						$actualSize = $streamStat['size'] ?? -1;
335
-						if ($actualSize > -1 && $actualSize !== $filesize) {
336
-							$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
337
-						}
338
-						return $handle;
339
-					} catch (NotFoundException $e) {
340
-						$this->logger->logException($e, [
341
-							'app' => 'objectstore',
342
-							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
343
-						]);
344
-						throw $e;
345
-					} catch (\Exception $ex) {
346
-						$this->logger->logException($ex, [
347
-							'app' => 'objectstore',
348
-							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
349
-						]);
350
-						return false;
351
-					}
352
-				} else {
353
-					return false;
354
-				}
355
-				// no break
356
-			case 'w':
357
-			case 'wb':
358
-			case 'w+':
359
-			case 'wb+':
360
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
361
-				$handle = fopen($tmpFile, $mode);
362
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
363
-					$this->writeBack($tmpFile, $path);
364
-					unlink($tmpFile);
365
-				});
366
-			case 'a':
367
-			case 'ab':
368
-			case 'r+':
369
-			case 'a+':
370
-			case 'x':
371
-			case 'x+':
372
-			case 'c':
373
-			case 'c+':
374
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
375
-				if ($this->file_exists($path)) {
376
-					$source = $this->fopen($path, 'r');
377
-					file_put_contents($tmpFile, $source);
378
-				}
379
-				$handle = fopen($tmpFile, $mode);
380
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
381
-					$this->writeBack($tmpFile, $path);
382
-					unlink($tmpFile);
383
-				});
384
-		}
385
-		return false;
386
-	}
387
-
388
-	public function file_exists($path) {
389
-		$path = $this->normalizePath($path);
390
-		return (bool)$this->stat($path);
391
-	}
392
-
393
-	public function rename($source, $target) {
394
-		$source = $this->normalizePath($source);
395
-		$target = $this->normalizePath($target);
396
-		$this->remove($target);
397
-		$this->getCache()->move($source, $target);
398
-		$this->touch(dirname($target));
399
-		return true;
400
-	}
401
-
402
-	public function getMimeType($path) {
403
-		$path = $this->normalizePath($path);
404
-		return parent::getMimeType($path);
405
-	}
406
-
407
-	public function touch($path, $mtime = null) {
408
-		if (is_null($mtime)) {
409
-			$mtime = time();
410
-		}
411
-
412
-		$path = $this->normalizePath($path);
413
-		$dirName = dirname($path);
414
-		$parentExists = $this->is_dir($dirName);
415
-		if (!$parentExists) {
416
-			return false;
417
-		}
418
-
419
-		$stat = $this->stat($path);
420
-		if (is_array($stat)) {
421
-			// update existing mtime in db
422
-			$stat['mtime'] = $mtime;
423
-			$this->getCache()->update($stat['fileid'], $stat);
424
-		} else {
425
-			try {
426
-				//create a empty file, need to have at least on char to make it
427
-				// work with all object storage implementations
428
-				$this->file_put_contents($path, ' ');
429
-				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
430
-				$stat = [
431
-					'etag' => $this->getETag($path),
432
-					'mimetype' => $mimeType,
433
-					'size' => 0,
434
-					'mtime' => $mtime,
435
-					'storage_mtime' => $mtime,
436
-					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
437
-				];
438
-				$this->getCache()->put($path, $stat);
439
-			} catch (\Exception $ex) {
440
-				$this->logger->logException($ex, [
441
-					'app' => 'objectstore',
442
-					'message' => 'Could not create object for ' . $path,
443
-				]);
444
-				throw $ex;
445
-			}
446
-		}
447
-		return true;
448
-	}
449
-
450
-	public function writeBack($tmpFile, $path) {
451
-		$size = filesize($tmpFile);
452
-		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
453
-	}
454
-
455
-	/**
456
-	 * external changes are not supported, exclusive access to the object storage is assumed
457
-	 *
458
-	 * @param string $path
459
-	 * @param int $time
460
-	 * @return false
461
-	 */
462
-	public function hasUpdated($path, $time) {
463
-		return false;
464
-	}
465
-
466
-	public function needsPartFile() {
467
-		return false;
468
-	}
469
-
470
-	public function file_put_contents($path, $data) {
471
-		$handle = $this->fopen($path, 'w+');
472
-		$result = fwrite($handle, $data);
473
-		fclose($handle);
474
-		return $result;
475
-	}
476
-
477
-	public function writeStream(string $path, $stream, int $size = null): int {
478
-		$stat = $this->stat($path);
479
-		if (empty($stat)) {
480
-			// create new file
481
-			$stat = [
482
-				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
483
-			];
484
-		}
485
-		// update stat with new data
486
-		$mTime = time();
487
-		$stat['size'] = (int)$size;
488
-		$stat['mtime'] = $mTime;
489
-		$stat['storage_mtime'] = $mTime;
490
-
491
-		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
492
-		$mimetype = $mimetypeDetector->detectPath($path);
493
-
494
-		$stat['mimetype'] = $mimetype;
495
-		$stat['etag'] = $this->getETag($path);
496
-		$stat['checksum'] = '';
497
-
498
-		$exists = $this->getCache()->inCache($path);
499
-		$uploadPath = $exists ? $path : $path . '.part';
500
-
501
-		if ($exists) {
502
-			$fileId = $stat['fileid'];
503
-		} else {
504
-			$fileId = $this->getCache()->put($uploadPath, $stat);
505
-		}
506
-
507
-		$urn = $this->getURN($fileId);
508
-		try {
509
-			//upload to object storage
510
-			if ($size === null) {
511
-				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
512
-					$this->getCache()->update($fileId, [
513
-						'size' => $writtenSize,
514
-					]);
515
-					$size = $writtenSize;
516
-				});
517
-				$this->objectStore->writeObject($urn, $countStream, $mimetype);
518
-				if (is_resource($countStream)) {
519
-					fclose($countStream);
520
-				}
521
-				$stat['size'] = $size;
522
-			} else {
523
-				$this->objectStore->writeObject($urn, $stream, $mimetype);
524
-				if (is_resource($stream)) {
525
-					fclose($stream);
526
-				}
527
-			}
528
-		} catch (\Exception $ex) {
529
-			if (!$exists) {
530
-				/*
51
+    use CopyDirectory;
52
+
53
+    /**
54
+     * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
55
+     */
56
+    protected $objectStore;
57
+    /**
58
+     * @var string $id
59
+     */
60
+    protected $id;
61
+    /**
62
+     * @var \OC\User\User $user
63
+     */
64
+    protected $user;
65
+
66
+    private $objectPrefix = 'urn:oid:';
67
+
68
+    private $logger;
69
+
70
+    /** @var bool */
71
+    protected $validateWrites = true;
72
+
73
+    public function __construct($params) {
74
+        if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
75
+            $this->objectStore = $params['objectstore'];
76
+        } else {
77
+            throw new \Exception('missing IObjectStore instance');
78
+        }
79
+        if (isset($params['storageid'])) {
80
+            $this->id = 'object::store:' . $params['storageid'];
81
+        } else {
82
+            $this->id = 'object::store:' . $this->objectStore->getStorageId();
83
+        }
84
+        if (isset($params['objectPrefix'])) {
85
+            $this->objectPrefix = $params['objectPrefix'];
86
+        }
87
+        if (isset($params['validateWrites'])) {
88
+            $this->validateWrites = (bool)$params['validateWrites'];
89
+        }
90
+        //initialize cache with root directory in cache
91
+        if (!$this->is_dir('/')) {
92
+            $this->mkdir('/');
93
+        }
94
+
95
+        $this->logger = \OC::$server->getLogger();
96
+    }
97
+
98
+    public function mkdir($path) {
99
+        $path = $this->normalizePath($path);
100
+        if ($this->file_exists($path)) {
101
+            $this->logger->warning("Tried to create an object store folder that already exists: $path");
102
+            return false;
103
+        }
104
+
105
+        $mTime = time();
106
+        $data = [
107
+            'mimetype' => 'httpd/unix-directory',
108
+            'size' => 0,
109
+            'mtime' => $mTime,
110
+            'storage_mtime' => $mTime,
111
+            'permissions' => \OCP\Constants::PERMISSION_ALL,
112
+        ];
113
+        if ($path === '') {
114
+            //create root on the fly
115
+            $data['etag'] = $this->getETag('');
116
+            $this->getCache()->put('', $data);
117
+            return true;
118
+        } else {
119
+            // if parent does not exist, create it
120
+            $parent = $this->normalizePath(dirname($path));
121
+            $parentType = $this->filetype($parent);
122
+            if ($parentType === false) {
123
+                if (!$this->mkdir($parent)) {
124
+                    // something went wrong
125
+                    $this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created");
126
+                    return false;
127
+                }
128
+            } elseif ($parentType === 'file') {
129
+                // parent is a file
130
+                $this->logger->warning("Parent ($parent) is a file");
131
+                return false;
132
+            }
133
+            // finally create the new dir
134
+            $mTime = time(); // update mtime
135
+            $data['mtime'] = $mTime;
136
+            $data['storage_mtime'] = $mTime;
137
+            $data['etag'] = $this->getETag($path);
138
+            $this->getCache()->put($path, $data);
139
+            return true;
140
+        }
141
+    }
142
+
143
+    /**
144
+     * @param string $path
145
+     * @return string
146
+     */
147
+    private function normalizePath($path) {
148
+        $path = trim($path, '/');
149
+        //FIXME why do we sometimes get a path like 'files//username'?
150
+        $path = str_replace('//', '/', $path);
151
+
152
+        // dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
153
+        if (!$path || $path === '.') {
154
+            $path = '';
155
+        }
156
+
157
+        return $path;
158
+    }
159
+
160
+    /**
161
+     * Object Stores use a NoopScanner because metadata is directly stored in
162
+     * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
163
+     *
164
+     * @param string $path
165
+     * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
166
+     * @return \OC\Files\ObjectStore\ObjectStoreScanner
167
+     */
168
+    public function getScanner($path = '', $storage = null) {
169
+        if (!$storage) {
170
+            $storage = $this;
171
+        }
172
+        if (!isset($this->scanner)) {
173
+            $this->scanner = new ObjectStoreScanner($storage);
174
+        }
175
+        return $this->scanner;
176
+    }
177
+
178
+    public function getId() {
179
+        return $this->id;
180
+    }
181
+
182
+    public function rmdir($path) {
183
+        $path = $this->normalizePath($path);
184
+        $entry = $this->getCache()->get($path);
185
+
186
+        if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
187
+            return false;
188
+        }
189
+
190
+        return $this->rmObjects($entry);
191
+    }
192
+
193
+    private function rmObjects(ICacheEntry $entry): bool {
194
+        $children = $this->getCache()->getFolderContentsById($entry->getId());
195
+        foreach ($children as $child) {
196
+            if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
197
+                if (!$this->rmObjects($child)) {
198
+                    return false;
199
+                }
200
+            } else {
201
+                if (!$this->rmObject($child)) {
202
+                    return false;
203
+                }
204
+            }
205
+        }
206
+
207
+        $this->getCache()->remove($entry->getPath());
208
+
209
+        return true;
210
+    }
211
+
212
+    public function unlink($path) {
213
+        $path = $this->normalizePath($path);
214
+        $entry = $this->getCache()->get($path);
215
+
216
+        if ($entry instanceof ICacheEntry) {
217
+            if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
218
+                return $this->rmObjects($entry);
219
+            } else {
220
+                return $this->rmObject($entry);
221
+            }
222
+        }
223
+        return false;
224
+    }
225
+
226
+    public function rmObject(ICacheEntry $entry): bool {
227
+        try {
228
+            $this->objectStore->deleteObject($this->getURN($entry->getId()));
229
+        } catch (\Exception $ex) {
230
+            if ($ex->getCode() !== 404) {
231
+                $this->logger->logException($ex, [
232
+                    'app' => 'objectstore',
233
+                    'message' => 'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
234
+                ]);
235
+                return false;
236
+            }
237
+            //removing from cache is ok as it does not exist in the objectstore anyway
238
+        }
239
+        $this->getCache()->remove($entry->getPath());
240
+        return true;
241
+    }
242
+
243
+    public function stat($path) {
244
+        $path = $this->normalizePath($path);
245
+        $cacheEntry = $this->getCache()->get($path);
246
+        if ($cacheEntry instanceof CacheEntry) {
247
+            return $cacheEntry->getData();
248
+        } else {
249
+            return false;
250
+        }
251
+    }
252
+
253
+    public function getPermissions($path) {
254
+        $stat = $this->stat($path);
255
+
256
+        if (is_array($stat) && isset($stat['permissions'])) {
257
+            return $stat['permissions'];
258
+        }
259
+
260
+        return parent::getPermissions($path);
261
+    }
262
+
263
+    /**
264
+     * Override this method if you need a different unique resource identifier for your object storage implementation.
265
+     * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
266
+     * You may need a mapping table to store your URN if it cannot be generated from the fileid.
267
+     *
268
+     * @param int $fileId the fileid
269
+     * @return null|string the unified resource name used to identify the object
270
+     */
271
+    public function getURN($fileId) {
272
+        if (is_numeric($fileId)) {
273
+            return $this->objectPrefix . $fileId;
274
+        }
275
+        return null;
276
+    }
277
+
278
+    public function opendir($path) {
279
+        $path = $this->normalizePath($path);
280
+
281
+        try {
282
+            $files = [];
283
+            $folderContents = $this->getCache()->getFolderContents($path);
284
+            foreach ($folderContents as $file) {
285
+                $files[] = $file['name'];
286
+            }
287
+
288
+            return IteratorDirectory::wrap($files);
289
+        } catch (\Exception $e) {
290
+            $this->logger->logException($e);
291
+            return false;
292
+        }
293
+    }
294
+
295
+    public function filetype($path) {
296
+        $path = $this->normalizePath($path);
297
+        $stat = $this->stat($path);
298
+        if ($stat) {
299
+            if ($stat['mimetype'] === 'httpd/unix-directory') {
300
+                return 'dir';
301
+            }
302
+            return 'file';
303
+        } else {
304
+            return false;
305
+        }
306
+    }
307
+
308
+    public function fopen($path, $mode) {
309
+        $path = $this->normalizePath($path);
310
+
311
+        if (strrpos($path, '.') !== false) {
312
+            $ext = substr($path, strrpos($path, '.'));
313
+        } else {
314
+            $ext = '';
315
+        }
316
+
317
+        switch ($mode) {
318
+            case 'r':
319
+            case 'rb':
320
+                $stat = $this->stat($path);
321
+                if (is_array($stat)) {
322
+                    $filesize = $stat['size'] ?? 0;
323
+                    // Reading 0 sized files is a waste of time
324
+                    if ($filesize === 0) {
325
+                        return fopen('php://memory', $mode);
326
+                    }
327
+
328
+                    try {
329
+                        $handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
330
+                        if ($handle === false) {
331
+                            return false; // keep backward compatibility
332
+                        }
333
+                        $streamStat = fstat($handle);
334
+                        $actualSize = $streamStat['size'] ?? -1;
335
+                        if ($actualSize > -1 && $actualSize !== $filesize) {
336
+                            $this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
337
+                        }
338
+                        return $handle;
339
+                    } catch (NotFoundException $e) {
340
+                        $this->logger->logException($e, [
341
+                            'app' => 'objectstore',
342
+                            'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
343
+                        ]);
344
+                        throw $e;
345
+                    } catch (\Exception $ex) {
346
+                        $this->logger->logException($ex, [
347
+                            'app' => 'objectstore',
348
+                            'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
349
+                        ]);
350
+                        return false;
351
+                    }
352
+                } else {
353
+                    return false;
354
+                }
355
+                // no break
356
+            case 'w':
357
+            case 'wb':
358
+            case 'w+':
359
+            case 'wb+':
360
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
361
+                $handle = fopen($tmpFile, $mode);
362
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
363
+                    $this->writeBack($tmpFile, $path);
364
+                    unlink($tmpFile);
365
+                });
366
+            case 'a':
367
+            case 'ab':
368
+            case 'r+':
369
+            case 'a+':
370
+            case 'x':
371
+            case 'x+':
372
+            case 'c':
373
+            case 'c+':
374
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
375
+                if ($this->file_exists($path)) {
376
+                    $source = $this->fopen($path, 'r');
377
+                    file_put_contents($tmpFile, $source);
378
+                }
379
+                $handle = fopen($tmpFile, $mode);
380
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
381
+                    $this->writeBack($tmpFile, $path);
382
+                    unlink($tmpFile);
383
+                });
384
+        }
385
+        return false;
386
+    }
387
+
388
+    public function file_exists($path) {
389
+        $path = $this->normalizePath($path);
390
+        return (bool)$this->stat($path);
391
+    }
392
+
393
+    public function rename($source, $target) {
394
+        $source = $this->normalizePath($source);
395
+        $target = $this->normalizePath($target);
396
+        $this->remove($target);
397
+        $this->getCache()->move($source, $target);
398
+        $this->touch(dirname($target));
399
+        return true;
400
+    }
401
+
402
+    public function getMimeType($path) {
403
+        $path = $this->normalizePath($path);
404
+        return parent::getMimeType($path);
405
+    }
406
+
407
+    public function touch($path, $mtime = null) {
408
+        if (is_null($mtime)) {
409
+            $mtime = time();
410
+        }
411
+
412
+        $path = $this->normalizePath($path);
413
+        $dirName = dirname($path);
414
+        $parentExists = $this->is_dir($dirName);
415
+        if (!$parentExists) {
416
+            return false;
417
+        }
418
+
419
+        $stat = $this->stat($path);
420
+        if (is_array($stat)) {
421
+            // update existing mtime in db
422
+            $stat['mtime'] = $mtime;
423
+            $this->getCache()->update($stat['fileid'], $stat);
424
+        } else {
425
+            try {
426
+                //create a empty file, need to have at least on char to make it
427
+                // work with all object storage implementations
428
+                $this->file_put_contents($path, ' ');
429
+                $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
430
+                $stat = [
431
+                    'etag' => $this->getETag($path),
432
+                    'mimetype' => $mimeType,
433
+                    'size' => 0,
434
+                    'mtime' => $mtime,
435
+                    'storage_mtime' => $mtime,
436
+                    'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
437
+                ];
438
+                $this->getCache()->put($path, $stat);
439
+            } catch (\Exception $ex) {
440
+                $this->logger->logException($ex, [
441
+                    'app' => 'objectstore',
442
+                    'message' => 'Could not create object for ' . $path,
443
+                ]);
444
+                throw $ex;
445
+            }
446
+        }
447
+        return true;
448
+    }
449
+
450
+    public function writeBack($tmpFile, $path) {
451
+        $size = filesize($tmpFile);
452
+        $this->writeStream($path, fopen($tmpFile, 'r'), $size);
453
+    }
454
+
455
+    /**
456
+     * external changes are not supported, exclusive access to the object storage is assumed
457
+     *
458
+     * @param string $path
459
+     * @param int $time
460
+     * @return false
461
+     */
462
+    public function hasUpdated($path, $time) {
463
+        return false;
464
+    }
465
+
466
+    public function needsPartFile() {
467
+        return false;
468
+    }
469
+
470
+    public function file_put_contents($path, $data) {
471
+        $handle = $this->fopen($path, 'w+');
472
+        $result = fwrite($handle, $data);
473
+        fclose($handle);
474
+        return $result;
475
+    }
476
+
477
+    public function writeStream(string $path, $stream, int $size = null): int {
478
+        $stat = $this->stat($path);
479
+        if (empty($stat)) {
480
+            // create new file
481
+            $stat = [
482
+                'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
483
+            ];
484
+        }
485
+        // update stat with new data
486
+        $mTime = time();
487
+        $stat['size'] = (int)$size;
488
+        $stat['mtime'] = $mTime;
489
+        $stat['storage_mtime'] = $mTime;
490
+
491
+        $mimetypeDetector = \OC::$server->getMimeTypeDetector();
492
+        $mimetype = $mimetypeDetector->detectPath($path);
493
+
494
+        $stat['mimetype'] = $mimetype;
495
+        $stat['etag'] = $this->getETag($path);
496
+        $stat['checksum'] = '';
497
+
498
+        $exists = $this->getCache()->inCache($path);
499
+        $uploadPath = $exists ? $path : $path . '.part';
500
+
501
+        if ($exists) {
502
+            $fileId = $stat['fileid'];
503
+        } else {
504
+            $fileId = $this->getCache()->put($uploadPath, $stat);
505
+        }
506
+
507
+        $urn = $this->getURN($fileId);
508
+        try {
509
+            //upload to object storage
510
+            if ($size === null) {
511
+                $countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
512
+                    $this->getCache()->update($fileId, [
513
+                        'size' => $writtenSize,
514
+                    ]);
515
+                    $size = $writtenSize;
516
+                });
517
+                $this->objectStore->writeObject($urn, $countStream, $mimetype);
518
+                if (is_resource($countStream)) {
519
+                    fclose($countStream);
520
+                }
521
+                $stat['size'] = $size;
522
+            } else {
523
+                $this->objectStore->writeObject($urn, $stream, $mimetype);
524
+                if (is_resource($stream)) {
525
+                    fclose($stream);
526
+                }
527
+            }
528
+        } catch (\Exception $ex) {
529
+            if (!$exists) {
530
+                /*
531 531
 				 * Only remove the entry if we are dealing with a new file.
532 532
 				 * Else people lose access to existing files
533 533
 				 */
534
-				$this->getCache()->remove($uploadPath);
535
-				$this->logger->logException($ex, [
536
-					'app' => 'objectstore',
537
-					'message' => 'Could not create object ' . $urn . ' for ' . $path,
538
-				]);
539
-			} else {
540
-				$this->logger->logException($ex, [
541
-					'app' => 'objectstore',
542
-					'message' => 'Could not update object ' . $urn . ' for ' . $path,
543
-				]);
544
-			}
545
-			throw $ex; // make this bubble up
546
-		}
547
-
548
-		if ($exists) {
549
-			$this->getCache()->update($fileId, $stat);
550
-		} else {
551
-			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
552
-				$this->getCache()->move($uploadPath, $path);
553
-			} else {
554
-				$this->getCache()->remove($uploadPath);
555
-				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
556
-			}
557
-		}
558
-
559
-		return $size;
560
-	}
561
-
562
-	public function getObjectStore(): IObjectStore {
563
-		return $this->objectStore;
564
-	}
565
-
566
-	public function copyFromStorage(
567
-		IStorage $sourceStorage,
568
-		$sourceInternalPath,
569
-		$targetInternalPath,
570
-		$preserveMtime = false
571
-	) {
572
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
573
-			/** @var ObjectStoreStorage $sourceStorage */
574
-			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
575
-				/** @var CacheEntry $sourceEntry */
576
-				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
577
-				$sourceEntryData = $sourceEntry->getData();
578
-				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
579
-				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
580
-				// unjailed storage.
581
-				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
582
-					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
583
-				}
584
-				$this->copyInner($sourceEntry, $targetInternalPath);
585
-				return true;
586
-			}
587
-		}
588
-
589
-		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
590
-	}
591
-
592
-	public function copy($source, $target) {
593
-		$source = $this->normalizePath($source);
594
-		$target = $this->normalizePath($target);
595
-
596
-		$cache = $this->getCache();
597
-		$sourceEntry = $cache->get($source);
598
-		if (!$sourceEntry) {
599
-			throw new NotFoundException('Source object not found');
600
-		}
601
-
602
-		$this->copyInner($sourceEntry, $target);
603
-
604
-		return true;
605
-	}
606
-
607
-	private function copyInner(ICacheEntry $sourceEntry, string $to) {
608
-		$cache = $this->getCache();
609
-
610
-		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
611
-			if ($cache->inCache($to)) {
612
-				$cache->remove($to);
613
-			}
614
-			$this->mkdir($to);
615
-
616
-			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
617
-				$this->copyInner($child, $to . '/' . $child->getName());
618
-			}
619
-		} else {
620
-			$this->copyFile($sourceEntry, $to);
621
-		}
622
-	}
623
-
624
-	private function copyFile(ICacheEntry $sourceEntry, string $to) {
625
-		$cache = $this->getCache();
626
-
627
-		$sourceUrn = $this->getURN($sourceEntry->getId());
628
-
629
-		if (!$cache instanceof Cache) {
630
-			throw new \Exception("Invalid source cache for object store copy");
631
-		}
632
-
633
-		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
634
-
635
-		$targetUrn = $this->getURN($targetId);
636
-
637
-		try {
638
-			$this->objectStore->copyObject($sourceUrn, $targetUrn);
639
-		} catch (\Exception $e) {
640
-			$cache->remove($to);
641
-
642
-			throw $e;
643
-		}
644
-	}
645
-
646
-	public function startChunkedWrite(string $targetPath): string {
647
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
648
-			throw new GenericFileException('Object store does not support multipart upload');
649
-		}
650
-		$cacheEntry = $this->getCache()->get($targetPath);
651
-		$urn = $this->getURN($cacheEntry->getId());
652
-		return $this->objectStore->initiateMultipartUpload($urn);
653
-	}
654
-
655
-	/**
656
-	 *
657
-	 * @throws GenericFileException
658
-	 */
659
-	public function putChunkedWritePart(
660
-		string $targetPath,
661
-		string $writeToken,
662
-		string $chunkId,
663
-		$data,
664
-		$size = null
665
-	): ?array {
666
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
667
-			throw new GenericFileException('Object store does not support multipart upload');
668
-		}
669
-		$cacheEntry = $this->getCache()->get($targetPath);
670
-		$urn = $this->getURN($cacheEntry->getId());
671
-
672
-		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
673
-
674
-		$parts[$chunkId] = [
675
-			'PartNumber' => $chunkId,
676
-			'ETag' => trim($result->get('ETag'), '"'),
677
-		];
678
-		return $parts[$chunkId];
679
-	}
680
-
681
-	public function completeChunkedWrite(string $targetPath, string $writeToken): int {
682
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
683
-			throw new GenericFileException('Object store does not support multipart upload');
684
-		}
685
-		$cacheEntry = $this->getCache()->get($targetPath);
686
-		$urn = $this->getURN($cacheEntry->getId());
687
-		$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
688
-		$sortedParts = array_values($parts);
689
-		sort($sortedParts);
690
-		try {
691
-			$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
692
-			$stat = $this->stat($targetPath);
693
-			$mtime = time();
694
-			if (is_array($stat)) {
695
-				$stat['size'] = $size;
696
-				$stat['mtime'] = $mtime;
697
-				$stat['mimetype'] = $this->getMimeType($targetPath);
698
-				$this->getCache()->update($stat['fileid'], $stat);
699
-			}
700
-		} catch (S3MultipartUploadException|S3Exception $e) {
701
-			$this->objectStore->abortMultipartUpload($urn, $writeToken);
702
-			$this->logger->logException($e, [
703
-				'app' => 'objectstore',
704
-				'message' => 'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
705
-			]);
706
-			throw new GenericFileException('Could not write chunked file');
707
-		}
708
-		return $size;
709
-	}
710
-
711
-	public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
712
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
713
-			throw new GenericFileException('Object store does not support multipart upload');
714
-		}
715
-		$cacheEntry = $this->getCache()->get($targetPath);
716
-		$urn = $this->getURN($cacheEntry->getId());
717
-		$this->objectStore->abortMultipartUpload($urn, $writeToken);
718
-	}
534
+                $this->getCache()->remove($uploadPath);
535
+                $this->logger->logException($ex, [
536
+                    'app' => 'objectstore',
537
+                    'message' => 'Could not create object ' . $urn . ' for ' . $path,
538
+                ]);
539
+            } else {
540
+                $this->logger->logException($ex, [
541
+                    'app' => 'objectstore',
542
+                    'message' => 'Could not update object ' . $urn . ' for ' . $path,
543
+                ]);
544
+            }
545
+            throw $ex; // make this bubble up
546
+        }
547
+
548
+        if ($exists) {
549
+            $this->getCache()->update($fileId, $stat);
550
+        } else {
551
+            if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
552
+                $this->getCache()->move($uploadPath, $path);
553
+            } else {
554
+                $this->getCache()->remove($uploadPath);
555
+                throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
556
+            }
557
+        }
558
+
559
+        return $size;
560
+    }
561
+
562
+    public function getObjectStore(): IObjectStore {
563
+        return $this->objectStore;
564
+    }
565
+
566
+    public function copyFromStorage(
567
+        IStorage $sourceStorage,
568
+        $sourceInternalPath,
569
+        $targetInternalPath,
570
+        $preserveMtime = false
571
+    ) {
572
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
573
+            /** @var ObjectStoreStorage $sourceStorage */
574
+            if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
575
+                /** @var CacheEntry $sourceEntry */
576
+                $sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
577
+                $sourceEntryData = $sourceEntry->getData();
578
+                // $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
579
+                // user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
580
+                // unjailed storage.
581
+                if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
582
+                    $sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
583
+                }
584
+                $this->copyInner($sourceEntry, $targetInternalPath);
585
+                return true;
586
+            }
587
+        }
588
+
589
+        return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
590
+    }
591
+
592
+    public function copy($source, $target) {
593
+        $source = $this->normalizePath($source);
594
+        $target = $this->normalizePath($target);
595
+
596
+        $cache = $this->getCache();
597
+        $sourceEntry = $cache->get($source);
598
+        if (!$sourceEntry) {
599
+            throw new NotFoundException('Source object not found');
600
+        }
601
+
602
+        $this->copyInner($sourceEntry, $target);
603
+
604
+        return true;
605
+    }
606
+
607
+    private function copyInner(ICacheEntry $sourceEntry, string $to) {
608
+        $cache = $this->getCache();
609
+
610
+        if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
611
+            if ($cache->inCache($to)) {
612
+                $cache->remove($to);
613
+            }
614
+            $this->mkdir($to);
615
+
616
+            foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
617
+                $this->copyInner($child, $to . '/' . $child->getName());
618
+            }
619
+        } else {
620
+            $this->copyFile($sourceEntry, $to);
621
+        }
622
+    }
623
+
624
+    private function copyFile(ICacheEntry $sourceEntry, string $to) {
625
+        $cache = $this->getCache();
626
+
627
+        $sourceUrn = $this->getURN($sourceEntry->getId());
628
+
629
+        if (!$cache instanceof Cache) {
630
+            throw new \Exception("Invalid source cache for object store copy");
631
+        }
632
+
633
+        $targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
634
+
635
+        $targetUrn = $this->getURN($targetId);
636
+
637
+        try {
638
+            $this->objectStore->copyObject($sourceUrn, $targetUrn);
639
+        } catch (\Exception $e) {
640
+            $cache->remove($to);
641
+
642
+            throw $e;
643
+        }
644
+    }
645
+
646
+    public function startChunkedWrite(string $targetPath): string {
647
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
648
+            throw new GenericFileException('Object store does not support multipart upload');
649
+        }
650
+        $cacheEntry = $this->getCache()->get($targetPath);
651
+        $urn = $this->getURN($cacheEntry->getId());
652
+        return $this->objectStore->initiateMultipartUpload($urn);
653
+    }
654
+
655
+    /**
656
+     *
657
+     * @throws GenericFileException
658
+     */
659
+    public function putChunkedWritePart(
660
+        string $targetPath,
661
+        string $writeToken,
662
+        string $chunkId,
663
+        $data,
664
+        $size = null
665
+    ): ?array {
666
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
667
+            throw new GenericFileException('Object store does not support multipart upload');
668
+        }
669
+        $cacheEntry = $this->getCache()->get($targetPath);
670
+        $urn = $this->getURN($cacheEntry->getId());
671
+
672
+        $result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
673
+
674
+        $parts[$chunkId] = [
675
+            'PartNumber' => $chunkId,
676
+            'ETag' => trim($result->get('ETag'), '"'),
677
+        ];
678
+        return $parts[$chunkId];
679
+    }
680
+
681
+    public function completeChunkedWrite(string $targetPath, string $writeToken): int {
682
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
683
+            throw new GenericFileException('Object store does not support multipart upload');
684
+        }
685
+        $cacheEntry = $this->getCache()->get($targetPath);
686
+        $urn = $this->getURN($cacheEntry->getId());
687
+        $parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
688
+        $sortedParts = array_values($parts);
689
+        sort($sortedParts);
690
+        try {
691
+            $size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
692
+            $stat = $this->stat($targetPath);
693
+            $mtime = time();
694
+            if (is_array($stat)) {
695
+                $stat['size'] = $size;
696
+                $stat['mtime'] = $mtime;
697
+                $stat['mimetype'] = $this->getMimeType($targetPath);
698
+                $this->getCache()->update($stat['fileid'], $stat);
699
+            }
700
+        } catch (S3MultipartUploadException|S3Exception $e) {
701
+            $this->objectStore->abortMultipartUpload($urn, $writeToken);
702
+            $this->logger->logException($e, [
703
+                'app' => 'objectstore',
704
+                'message' => 'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
705
+            ]);
706
+            throw new GenericFileException('Could not write chunked file');
707
+        }
708
+        return $size;
709
+    }
710
+
711
+    public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
712
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
713
+            throw new GenericFileException('Object store does not support multipart upload');
714
+        }
715
+        $cacheEntry = $this->getCache()->get($targetPath);
716
+        $urn = $this->getURN($cacheEntry->getId());
717
+        $this->objectStore->abortMultipartUpload($urn, $writeToken);
718
+    }
719 719
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Scanner.php 2 patches
Indentation   +528 added lines, -528 removed lines patch added patch discarded remove patch
@@ -59,532 +59,532 @@
 block discarded – undo
59 59
  * @package OC\Files\Cache
60 60
  */
61 61
 class Scanner extends BasicEmitter implements IScanner {
62
-	/**
63
-	 * @var \OC\Files\Storage\Storage $storage
64
-	 */
65
-	protected $storage;
66
-
67
-	/**
68
-	 * @var string $storageId
69
-	 */
70
-	protected $storageId;
71
-
72
-	/**
73
-	 * @var \OC\Files\Cache\Cache $cache
74
-	 */
75
-	protected $cache;
76
-
77
-	/**
78
-	 * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
79
-	 */
80
-	protected $cacheActive;
81
-
82
-	/**
83
-	 * @var bool $useTransactions whether to use transactions
84
-	 */
85
-	protected $useTransactions = true;
86
-
87
-	/**
88
-	 * @var \OCP\Lock\ILockingProvider
89
-	 */
90
-	protected $lockingProvider;
91
-
92
-	protected IDBConnection $connection;
93
-
94
-	public function __construct(\OC\Files\Storage\Storage $storage) {
95
-		$this->storage = $storage;
96
-		$this->storageId = $this->storage->getId();
97
-		$this->cache = $storage->getCache();
98
-		$this->cacheActive = !\OC::$server->getConfig()->getSystemValueBool('filesystem_cache_readonly', false);
99
-		$this->lockingProvider = \OC::$server->getLockingProvider();
100
-		$this->connection = \OC::$server->get(IDBConnection::class);
101
-	}
102
-
103
-	/**
104
-	 * Whether to wrap the scanning of a folder in a database transaction
105
-	 * On default transactions are used
106
-	 *
107
-	 * @param bool $useTransactions
108
-	 */
109
-	public function setUseTransactions($useTransactions) {
110
-		$this->useTransactions = $useTransactions;
111
-	}
112
-
113
-	/**
114
-	 * get all the metadata of a file or folder
115
-	 * *
116
-	 *
117
-	 * @param string $path
118
-	 * @return array|null an array of metadata of the file
119
-	 */
120
-	protected function getData($path) {
121
-		$data = $this->storage->getMetaData($path);
122
-		if (is_null($data)) {
123
-			\OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
124
-		}
125
-		return $data;
126
-	}
127
-
128
-	/**
129
-	 * scan a single file and store it in the cache
130
-	 *
131
-	 * @param string $file
132
-	 * @param int $reuseExisting
133
-	 * @param int $parentId
134
-	 * @param array|null|false $cacheData existing data in the cache for the file to be scanned
135
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
136
-	 * @param null $data the metadata for the file, as returned by the storage
137
-	 * @return array|null an array of metadata of the scanned file
138
-	 * @throws \OCP\Lock\LockedException
139
-	 */
140
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
141
-		if ($file !== '') {
142
-			try {
143
-				$this->storage->verifyPath(dirname($file), basename($file));
144
-			} catch (\Exception $e) {
145
-				return null;
146
-			}
147
-		}
148
-		// only proceed if $file is not a partial file, blacklist is handled by the storage
149
-		if (!self::isPartialFile($file)) {
150
-			// acquire a lock
151
-			if ($lock) {
152
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
153
-					$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
154
-				}
155
-			}
156
-
157
-			try {
158
-				$data = $data ?? $this->getData($file);
159
-			} catch (ForbiddenException $e) {
160
-				if ($lock) {
161
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
162
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
163
-					}
164
-				}
165
-
166
-				return null;
167
-			}
168
-
169
-			try {
170
-				if ($data) {
171
-					// pre-emit only if it was a file. By that we avoid counting/treating folders as files
172
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
173
-						$this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
174
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
175
-					}
176
-
177
-					$parent = dirname($file);
178
-					if ($parent === '.' || $parent === '/') {
179
-						$parent = '';
180
-					}
181
-					if ($parentId === -1) {
182
-						$parentId = $this->cache->getParentId($file);
183
-					}
184
-
185
-					// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
186
-					if ($file && $parentId === -1) {
187
-						$parentData = $this->scanFile($parent);
188
-						if (!$parentData) {
189
-							return null;
190
-						}
191
-						$parentId = $parentData['fileid'];
192
-					}
193
-					if ($parent) {
194
-						$data['parent'] = $parentId;
195
-					}
196
-					if (is_null($cacheData)) {
197
-						/** @var CacheEntry $cacheData */
198
-						$cacheData = $this->cache->get($file);
199
-					}
200
-					if ($cacheData && $reuseExisting && isset($cacheData['fileid'])) {
201
-						// prevent empty etag
202
-						$etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
203
-						$fileId = $cacheData['fileid'];
204
-						$data['fileid'] = $fileId;
205
-						// only reuse data if the file hasn't explicitly changed
206
-						if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
207
-							$data['mtime'] = $cacheData['mtime'];
208
-							if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
209
-								$data['size'] = $cacheData['size'];
210
-							}
211
-							if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
212
-								$data['etag'] = $etag;
213
-							}
214
-						}
215
-
216
-						// we only updated unencrypted_size if it's already set
217
-						if ($cacheData['unencrypted_size'] === 0) {
218
-							unset($data['unencrypted_size']);
219
-						}
220
-
221
-						// Only update metadata that has changed
222
-						$newData = array_diff_assoc($data, $cacheData->getData());
223
-					} else {
224
-						// we only updated unencrypted_size if it's already set
225
-						unset($data['unencrypted_size']);
226
-						$newData = $data;
227
-						$fileId = -1;
228
-					}
229
-					if (!empty($newData)) {
230
-						// Reset the checksum if the data has changed
231
-						$newData['checksum'] = '';
232
-						$newData['parent'] = $parentId;
233
-						$data['fileid'] = $this->addToCache($file, $newData, $fileId);
234
-					}
235
-
236
-					$data['oldSize'] = ($cacheData && isset($cacheData['size'])) ? $cacheData['size'] : 0;
237
-
238
-					if ($cacheData && isset($cacheData['encrypted'])) {
239
-						$data['encrypted'] = $cacheData['encrypted'];
240
-					}
241
-
242
-					// post-emit only if it was a file. By that we avoid counting/treating folders as files
243
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
244
-						$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
245
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
246
-					}
247
-				} else {
248
-					$this->removeFromCache($file);
249
-				}
250
-			} catch (\Exception $e) {
251
-				if ($lock) {
252
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
253
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
254
-					}
255
-				}
256
-				throw $e;
257
-			}
258
-
259
-			// release the acquired lock
260
-			if ($lock) {
261
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
262
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
263
-				}
264
-			}
265
-
266
-			if ($data && !isset($data['encrypted'])) {
267
-				$data['encrypted'] = false;
268
-			}
269
-			return $data;
270
-		}
271
-
272
-		return null;
273
-	}
274
-
275
-	protected function removeFromCache($path) {
276
-		\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
277
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
278
-		if ($this->cacheActive) {
279
-			$this->cache->remove($path);
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * @param string $path
285
-	 * @param array $data
286
-	 * @param int $fileId
287
-	 * @return int the id of the added file
288
-	 */
289
-	protected function addToCache($path, $data, $fileId = -1) {
290
-		if (isset($data['scan_permissions'])) {
291
-			$data['permissions'] = $data['scan_permissions'];
292
-		}
293
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
294
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
295
-		if ($this->cacheActive) {
296
-			if ($fileId !== -1) {
297
-				$this->cache->update($fileId, $data);
298
-				return $fileId;
299
-			} else {
300
-				return $this->cache->insert($path, $data);
301
-			}
302
-		} else {
303
-			return -1;
304
-		}
305
-	}
306
-
307
-	/**
308
-	 * @param string $path
309
-	 * @param array $data
310
-	 * @param int $fileId
311
-	 */
312
-	protected function updateCache($path, $data, $fileId = -1) {
313
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
314
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
315
-		if ($this->cacheActive) {
316
-			if ($fileId !== -1) {
317
-				$this->cache->update($fileId, $data);
318
-			} else {
319
-				$this->cache->put($path, $data);
320
-			}
321
-		}
322
-	}
323
-
324
-	/**
325
-	 * scan a folder and all it's children
326
-	 *
327
-	 * @param string $path
328
-	 * @param bool $recursive
329
-	 * @param int $reuse
330
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
331
-	 * @return array|null an array of the meta data of the scanned file or folder
332
-	 */
333
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
334
-		if ($reuse === -1) {
335
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
336
-		}
337
-		if ($lock) {
338
-			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
339
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
340
-				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
341
-			}
342
-		}
343
-		try {
344
-			try {
345
-				$data = $this->scanFile($path, $reuse, -1, null, $lock);
346
-				if ($data && $data['mimetype'] === 'httpd/unix-directory') {
347
-					$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data);
348
-					$data['size'] = $size;
349
-				}
350
-			} catch (NotFoundException $e) {
351
-				$this->removeFromCache($path);
352
-				return null;
353
-			}
354
-		} finally {
355
-			if ($lock) {
356
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
357
-					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
358
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
359
-				}
360
-			}
361
-		}
362
-		return $data;
363
-	}
364
-
365
-	/**
366
-	 * Get the children currently in the cache
367
-	 *
368
-	 * @param int $folderId
369
-	 * @return array[]
370
-	 */
371
-	protected function getExistingChildren($folderId) {
372
-		$existingChildren = [];
373
-		$children = $this->cache->getFolderContentsById($folderId);
374
-		foreach ($children as $child) {
375
-			$existingChildren[$child['name']] = $child;
376
-		}
377
-		return $existingChildren;
378
-	}
379
-
380
-	/**
381
-	 * scan all the files and folders in a folder
382
-	 *
383
-	 * @param string $path
384
-	 * @param bool $recursive
385
-	 * @param int $reuse
386
-	 * @param int $folderId id for the folder to be scanned
387
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
388
-	 * @param array $data the data of the folder before (re)scanning the children
389
-	 * @return int the size of the scanned folder or -1 if the size is unknown at this stage
390
-	 */
391
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
392
-		if ($reuse === -1) {
393
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
394
-		}
395
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
396
-		$size = 0;
397
-		if (!is_null($folderId)) {
398
-			$folderId = $this->cache->getId($path);
399
-		}
400
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
401
-
402
-		foreach ($childQueue as $child => $childId) {
403
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
404
-			if ($childSize === -1) {
405
-				$size = -1;
406
-			} elseif ($size !== -1) {
407
-				$size += $childSize;
408
-			}
409
-		}
410
-		$oldSize = $data['size'] ?? null;
411
-
412
-		// for encrypted storages, we trigger a regular folder size calculation instead of using the calculated size
413
-		// to make sure we also updated the unencrypted-size where applicable
414
-		if ($this->storage->instanceOfStorage(Encryption::class)) {
415
-			$this->cache->calculateFolderSize($path);
416
-		} else {
417
-			if ($this->cacheActive && $oldSize !== $size) {
418
-				$this->cache->update($folderId, ['size' => $size]);
419
-			}
420
-		}
421
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
422
-		return $size;
423
-	}
424
-
425
-	private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
426
-		// we put this in it's own function so it cleans up the memory before we start recursing
427
-		$existingChildren = $this->getExistingChildren($folderId);
428
-		$newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
429
-
430
-		if (count($existingChildren) === 0 && count($newChildren) === 0) {
431
-			// no need to do a transaction
432
-			return [];
433
-		}
434
-
435
-		if ($this->useTransactions) {
436
-			$this->connection->beginTransaction();
437
-		}
438
-
439
-		$exceptionOccurred = false;
440
-		$childQueue = [];
441
-		$newChildNames = [];
442
-		foreach ($newChildren as $fileMeta) {
443
-			$permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
444
-			if ($permissions === 0) {
445
-				continue;
446
-			}
447
-			$originalFile = $fileMeta['name'];
448
-			$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
449
-			if (trim($originalFile, '/') !== $file) {
450
-				// encoding mismatch, might require compatibility wrapper
451
-				\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
452
-				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
453
-				// skip this entry
454
-				continue;
455
-			}
456
-
457
-			$newChildNames[] = $file;
458
-			$child = $path ? $path . '/' . $file : $file;
459
-			try {
460
-				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
461
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
462
-				if ($data) {
463
-					if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
464
-						$childQueue[$child] = $data['fileid'];
465
-					} elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
466
-						// only recurse into folders which aren't fully scanned
467
-						$childQueue[$child] = $data['fileid'];
468
-					} elseif ($data['size'] === -1) {
469
-						$size = -1;
470
-					} elseif ($size !== -1) {
471
-						$size += $data['size'];
472
-					}
473
-				}
474
-			} catch (Exception $ex) {
475
-				// might happen if inserting duplicate while a scanning
476
-				// process is running in parallel
477
-				// log and ignore
478
-				if ($this->useTransactions) {
479
-					$this->connection->rollback();
480
-					$this->connection->beginTransaction();
481
-				}
482
-				\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
483
-					'app' => 'core',
484
-					'exception' => $ex,
485
-				]);
486
-				$exceptionOccurred = true;
487
-			} catch (\OCP\Lock\LockedException $e) {
488
-				if ($this->useTransactions) {
489
-					$this->connection->rollback();
490
-				}
491
-				throw $e;
492
-			}
493
-		}
494
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
495
-		foreach ($removedChildren as $childName) {
496
-			$child = $path ? $path . '/' . $childName : $childName;
497
-			$this->removeFromCache($child);
498
-		}
499
-		if ($this->useTransactions) {
500
-			$this->connection->commit();
501
-		}
502
-		if ($exceptionOccurred) {
503
-			// It might happen that the parallel scan process has already
504
-			// inserted mimetypes but those weren't available yet inside the transaction
505
-			// To make sure to have the updated mime types in such cases,
506
-			// we reload them here
507
-			\OC::$server->getMimeTypeLoader()->reset();
508
-		}
509
-		return $childQueue;
510
-	}
511
-
512
-	/**
513
-	 * check if the file should be ignored when scanning
514
-	 * NOTE: files with a '.part' extension are ignored as well!
515
-	 *       prevents unfinished put requests to be scanned
516
-	 *
517
-	 * @param string $file
518
-	 * @return boolean
519
-	 */
520
-	public static function isPartialFile($file) {
521
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
522
-			return true;
523
-		}
524
-		if (strpos($file, '.part/') !== false) {
525
-			return true;
526
-		}
527
-
528
-		return false;
529
-	}
530
-
531
-	/**
532
-	 * walk over any folders that are not fully scanned yet and scan them
533
-	 */
534
-	public function backgroundScan() {
535
-		if ($this->storage->instanceOfStorage(Jail::class)) {
536
-			// for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
537
-			// this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
538
-			//
539
-			// Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
540
-			// have to be scanned at some point anyway.
541
-			$unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
542
-			$unJailedScanner->backgroundScan();
543
-		} else {
544
-			if (!$this->cache->inCache('')) {
545
-				// if the storage isn't in the cache yet, just scan the root completely
546
-				$this->runBackgroundScanJob(function () {
547
-					$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
548
-				}, '');
549
-			} else {
550
-				$lastPath = null;
551
-				// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
552
-				while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
553
-					$this->runBackgroundScanJob(function () use ($path) {
554
-						$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
555
-					}, $path);
556
-					// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
557
-					// to make this possible
558
-					$lastPath = $path;
559
-				}
560
-			}
561
-		}
562
-	}
563
-
564
-	protected function runBackgroundScanJob(callable $callback, $path) {
565
-		try {
566
-			$callback();
567
-			\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
568
-			if ($this->cacheActive && $this->cache instanceof Cache) {
569
-				$this->cache->correctFolderSize($path, null, true);
570
-			}
571
-		} catch (\OCP\Files\StorageInvalidException $e) {
572
-			// skip unavailable storages
573
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
574
-			// skip unavailable storages
575
-		} catch (\OCP\Files\ForbiddenException $e) {
576
-			// skip forbidden storages
577
-		} catch (\OCP\Lock\LockedException $e) {
578
-			// skip unavailable storages
579
-		}
580
-	}
581
-
582
-	/**
583
-	 * Set whether the cache is affected by scan operations
584
-	 *
585
-	 * @param boolean $active The active state of the cache
586
-	 */
587
-	public function setCacheActive($active) {
588
-		$this->cacheActive = $active;
589
-	}
62
+    /**
63
+     * @var \OC\Files\Storage\Storage $storage
64
+     */
65
+    protected $storage;
66
+
67
+    /**
68
+     * @var string $storageId
69
+     */
70
+    protected $storageId;
71
+
72
+    /**
73
+     * @var \OC\Files\Cache\Cache $cache
74
+     */
75
+    protected $cache;
76
+
77
+    /**
78
+     * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
79
+     */
80
+    protected $cacheActive;
81
+
82
+    /**
83
+     * @var bool $useTransactions whether to use transactions
84
+     */
85
+    protected $useTransactions = true;
86
+
87
+    /**
88
+     * @var \OCP\Lock\ILockingProvider
89
+     */
90
+    protected $lockingProvider;
91
+
92
+    protected IDBConnection $connection;
93
+
94
+    public function __construct(\OC\Files\Storage\Storage $storage) {
95
+        $this->storage = $storage;
96
+        $this->storageId = $this->storage->getId();
97
+        $this->cache = $storage->getCache();
98
+        $this->cacheActive = !\OC::$server->getConfig()->getSystemValueBool('filesystem_cache_readonly', false);
99
+        $this->lockingProvider = \OC::$server->getLockingProvider();
100
+        $this->connection = \OC::$server->get(IDBConnection::class);
101
+    }
102
+
103
+    /**
104
+     * Whether to wrap the scanning of a folder in a database transaction
105
+     * On default transactions are used
106
+     *
107
+     * @param bool $useTransactions
108
+     */
109
+    public function setUseTransactions($useTransactions) {
110
+        $this->useTransactions = $useTransactions;
111
+    }
112
+
113
+    /**
114
+     * get all the metadata of a file or folder
115
+     * *
116
+     *
117
+     * @param string $path
118
+     * @return array|null an array of metadata of the file
119
+     */
120
+    protected function getData($path) {
121
+        $data = $this->storage->getMetaData($path);
122
+        if (is_null($data)) {
123
+            \OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
124
+        }
125
+        return $data;
126
+    }
127
+
128
+    /**
129
+     * scan a single file and store it in the cache
130
+     *
131
+     * @param string $file
132
+     * @param int $reuseExisting
133
+     * @param int $parentId
134
+     * @param array|null|false $cacheData existing data in the cache for the file to be scanned
135
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
136
+     * @param null $data the metadata for the file, as returned by the storage
137
+     * @return array|null an array of metadata of the scanned file
138
+     * @throws \OCP\Lock\LockedException
139
+     */
140
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
141
+        if ($file !== '') {
142
+            try {
143
+                $this->storage->verifyPath(dirname($file), basename($file));
144
+            } catch (\Exception $e) {
145
+                return null;
146
+            }
147
+        }
148
+        // only proceed if $file is not a partial file, blacklist is handled by the storage
149
+        if (!self::isPartialFile($file)) {
150
+            // acquire a lock
151
+            if ($lock) {
152
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
153
+                    $this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
154
+                }
155
+            }
156
+
157
+            try {
158
+                $data = $data ?? $this->getData($file);
159
+            } catch (ForbiddenException $e) {
160
+                if ($lock) {
161
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
162
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
163
+                    }
164
+                }
165
+
166
+                return null;
167
+            }
168
+
169
+            try {
170
+                if ($data) {
171
+                    // pre-emit only if it was a file. By that we avoid counting/treating folders as files
172
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
173
+                        $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
174
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
175
+                    }
176
+
177
+                    $parent = dirname($file);
178
+                    if ($parent === '.' || $parent === '/') {
179
+                        $parent = '';
180
+                    }
181
+                    if ($parentId === -1) {
182
+                        $parentId = $this->cache->getParentId($file);
183
+                    }
184
+
185
+                    // scan the parent if it's not in the cache (id -1) and the current file is not the root folder
186
+                    if ($file && $parentId === -1) {
187
+                        $parentData = $this->scanFile($parent);
188
+                        if (!$parentData) {
189
+                            return null;
190
+                        }
191
+                        $parentId = $parentData['fileid'];
192
+                    }
193
+                    if ($parent) {
194
+                        $data['parent'] = $parentId;
195
+                    }
196
+                    if (is_null($cacheData)) {
197
+                        /** @var CacheEntry $cacheData */
198
+                        $cacheData = $this->cache->get($file);
199
+                    }
200
+                    if ($cacheData && $reuseExisting && isset($cacheData['fileid'])) {
201
+                        // prevent empty etag
202
+                        $etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
203
+                        $fileId = $cacheData['fileid'];
204
+                        $data['fileid'] = $fileId;
205
+                        // only reuse data if the file hasn't explicitly changed
206
+                        if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
207
+                            $data['mtime'] = $cacheData['mtime'];
208
+                            if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
209
+                                $data['size'] = $cacheData['size'];
210
+                            }
211
+                            if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
212
+                                $data['etag'] = $etag;
213
+                            }
214
+                        }
215
+
216
+                        // we only updated unencrypted_size if it's already set
217
+                        if ($cacheData['unencrypted_size'] === 0) {
218
+                            unset($data['unencrypted_size']);
219
+                        }
220
+
221
+                        // Only update metadata that has changed
222
+                        $newData = array_diff_assoc($data, $cacheData->getData());
223
+                    } else {
224
+                        // we only updated unencrypted_size if it's already set
225
+                        unset($data['unencrypted_size']);
226
+                        $newData = $data;
227
+                        $fileId = -1;
228
+                    }
229
+                    if (!empty($newData)) {
230
+                        // Reset the checksum if the data has changed
231
+                        $newData['checksum'] = '';
232
+                        $newData['parent'] = $parentId;
233
+                        $data['fileid'] = $this->addToCache($file, $newData, $fileId);
234
+                    }
235
+
236
+                    $data['oldSize'] = ($cacheData && isset($cacheData['size'])) ? $cacheData['size'] : 0;
237
+
238
+                    if ($cacheData && isset($cacheData['encrypted'])) {
239
+                        $data['encrypted'] = $cacheData['encrypted'];
240
+                    }
241
+
242
+                    // post-emit only if it was a file. By that we avoid counting/treating folders as files
243
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
244
+                        $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
245
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
246
+                    }
247
+                } else {
248
+                    $this->removeFromCache($file);
249
+                }
250
+            } catch (\Exception $e) {
251
+                if ($lock) {
252
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
253
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
254
+                    }
255
+                }
256
+                throw $e;
257
+            }
258
+
259
+            // release the acquired lock
260
+            if ($lock) {
261
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
262
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
263
+                }
264
+            }
265
+
266
+            if ($data && !isset($data['encrypted'])) {
267
+                $data['encrypted'] = false;
268
+            }
269
+            return $data;
270
+        }
271
+
272
+        return null;
273
+    }
274
+
275
+    protected function removeFromCache($path) {
276
+        \OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
277
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
278
+        if ($this->cacheActive) {
279
+            $this->cache->remove($path);
280
+        }
281
+    }
282
+
283
+    /**
284
+     * @param string $path
285
+     * @param array $data
286
+     * @param int $fileId
287
+     * @return int the id of the added file
288
+     */
289
+    protected function addToCache($path, $data, $fileId = -1) {
290
+        if (isset($data['scan_permissions'])) {
291
+            $data['permissions'] = $data['scan_permissions'];
292
+        }
293
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
294
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
295
+        if ($this->cacheActive) {
296
+            if ($fileId !== -1) {
297
+                $this->cache->update($fileId, $data);
298
+                return $fileId;
299
+            } else {
300
+                return $this->cache->insert($path, $data);
301
+            }
302
+        } else {
303
+            return -1;
304
+        }
305
+    }
306
+
307
+    /**
308
+     * @param string $path
309
+     * @param array $data
310
+     * @param int $fileId
311
+     */
312
+    protected function updateCache($path, $data, $fileId = -1) {
313
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
314
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
315
+        if ($this->cacheActive) {
316
+            if ($fileId !== -1) {
317
+                $this->cache->update($fileId, $data);
318
+            } else {
319
+                $this->cache->put($path, $data);
320
+            }
321
+        }
322
+    }
323
+
324
+    /**
325
+     * scan a folder and all it's children
326
+     *
327
+     * @param string $path
328
+     * @param bool $recursive
329
+     * @param int $reuse
330
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
331
+     * @return array|null an array of the meta data of the scanned file or folder
332
+     */
333
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
334
+        if ($reuse === -1) {
335
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
336
+        }
337
+        if ($lock) {
338
+            if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
339
+                $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
340
+                $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
341
+            }
342
+        }
343
+        try {
344
+            try {
345
+                $data = $this->scanFile($path, $reuse, -1, null, $lock);
346
+                if ($data && $data['mimetype'] === 'httpd/unix-directory') {
347
+                    $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data);
348
+                    $data['size'] = $size;
349
+                }
350
+            } catch (NotFoundException $e) {
351
+                $this->removeFromCache($path);
352
+                return null;
353
+            }
354
+        } finally {
355
+            if ($lock) {
356
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
357
+                    $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
358
+                    $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
359
+                }
360
+            }
361
+        }
362
+        return $data;
363
+    }
364
+
365
+    /**
366
+     * Get the children currently in the cache
367
+     *
368
+     * @param int $folderId
369
+     * @return array[]
370
+     */
371
+    protected function getExistingChildren($folderId) {
372
+        $existingChildren = [];
373
+        $children = $this->cache->getFolderContentsById($folderId);
374
+        foreach ($children as $child) {
375
+            $existingChildren[$child['name']] = $child;
376
+        }
377
+        return $existingChildren;
378
+    }
379
+
380
+    /**
381
+     * scan all the files and folders in a folder
382
+     *
383
+     * @param string $path
384
+     * @param bool $recursive
385
+     * @param int $reuse
386
+     * @param int $folderId id for the folder to be scanned
387
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
388
+     * @param array $data the data of the folder before (re)scanning the children
389
+     * @return int the size of the scanned folder or -1 if the size is unknown at this stage
390
+     */
391
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
392
+        if ($reuse === -1) {
393
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
394
+        }
395
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
396
+        $size = 0;
397
+        if (!is_null($folderId)) {
398
+            $folderId = $this->cache->getId($path);
399
+        }
400
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
401
+
402
+        foreach ($childQueue as $child => $childId) {
403
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
404
+            if ($childSize === -1) {
405
+                $size = -1;
406
+            } elseif ($size !== -1) {
407
+                $size += $childSize;
408
+            }
409
+        }
410
+        $oldSize = $data['size'] ?? null;
411
+
412
+        // for encrypted storages, we trigger a regular folder size calculation instead of using the calculated size
413
+        // to make sure we also updated the unencrypted-size where applicable
414
+        if ($this->storage->instanceOfStorage(Encryption::class)) {
415
+            $this->cache->calculateFolderSize($path);
416
+        } else {
417
+            if ($this->cacheActive && $oldSize !== $size) {
418
+                $this->cache->update($folderId, ['size' => $size]);
419
+            }
420
+        }
421
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
422
+        return $size;
423
+    }
424
+
425
+    private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
426
+        // we put this in it's own function so it cleans up the memory before we start recursing
427
+        $existingChildren = $this->getExistingChildren($folderId);
428
+        $newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
429
+
430
+        if (count($existingChildren) === 0 && count($newChildren) === 0) {
431
+            // no need to do a transaction
432
+            return [];
433
+        }
434
+
435
+        if ($this->useTransactions) {
436
+            $this->connection->beginTransaction();
437
+        }
438
+
439
+        $exceptionOccurred = false;
440
+        $childQueue = [];
441
+        $newChildNames = [];
442
+        foreach ($newChildren as $fileMeta) {
443
+            $permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
444
+            if ($permissions === 0) {
445
+                continue;
446
+            }
447
+            $originalFile = $fileMeta['name'];
448
+            $file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
449
+            if (trim($originalFile, '/') !== $file) {
450
+                // encoding mismatch, might require compatibility wrapper
451
+                \OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
452
+                $this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
453
+                // skip this entry
454
+                continue;
455
+            }
456
+
457
+            $newChildNames[] = $file;
458
+            $child = $path ? $path . '/' . $file : $file;
459
+            try {
460
+                $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
461
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
462
+                if ($data) {
463
+                    if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
464
+                        $childQueue[$child] = $data['fileid'];
465
+                    } elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
466
+                        // only recurse into folders which aren't fully scanned
467
+                        $childQueue[$child] = $data['fileid'];
468
+                    } elseif ($data['size'] === -1) {
469
+                        $size = -1;
470
+                    } elseif ($size !== -1) {
471
+                        $size += $data['size'];
472
+                    }
473
+                }
474
+            } catch (Exception $ex) {
475
+                // might happen if inserting duplicate while a scanning
476
+                // process is running in parallel
477
+                // log and ignore
478
+                if ($this->useTransactions) {
479
+                    $this->connection->rollback();
480
+                    $this->connection->beginTransaction();
481
+                }
482
+                \OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
483
+                    'app' => 'core',
484
+                    'exception' => $ex,
485
+                ]);
486
+                $exceptionOccurred = true;
487
+            } catch (\OCP\Lock\LockedException $e) {
488
+                if ($this->useTransactions) {
489
+                    $this->connection->rollback();
490
+                }
491
+                throw $e;
492
+            }
493
+        }
494
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
495
+        foreach ($removedChildren as $childName) {
496
+            $child = $path ? $path . '/' . $childName : $childName;
497
+            $this->removeFromCache($child);
498
+        }
499
+        if ($this->useTransactions) {
500
+            $this->connection->commit();
501
+        }
502
+        if ($exceptionOccurred) {
503
+            // It might happen that the parallel scan process has already
504
+            // inserted mimetypes but those weren't available yet inside the transaction
505
+            // To make sure to have the updated mime types in such cases,
506
+            // we reload them here
507
+            \OC::$server->getMimeTypeLoader()->reset();
508
+        }
509
+        return $childQueue;
510
+    }
511
+
512
+    /**
513
+     * check if the file should be ignored when scanning
514
+     * NOTE: files with a '.part' extension are ignored as well!
515
+     *       prevents unfinished put requests to be scanned
516
+     *
517
+     * @param string $file
518
+     * @return boolean
519
+     */
520
+    public static function isPartialFile($file) {
521
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
522
+            return true;
523
+        }
524
+        if (strpos($file, '.part/') !== false) {
525
+            return true;
526
+        }
527
+
528
+        return false;
529
+    }
530
+
531
+    /**
532
+     * walk over any folders that are not fully scanned yet and scan them
533
+     */
534
+    public function backgroundScan() {
535
+        if ($this->storage->instanceOfStorage(Jail::class)) {
536
+            // for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
537
+            // this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
538
+            //
539
+            // Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
540
+            // have to be scanned at some point anyway.
541
+            $unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
542
+            $unJailedScanner->backgroundScan();
543
+        } else {
544
+            if (!$this->cache->inCache('')) {
545
+                // if the storage isn't in the cache yet, just scan the root completely
546
+                $this->runBackgroundScanJob(function () {
547
+                    $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
548
+                }, '');
549
+            } else {
550
+                $lastPath = null;
551
+                // find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
552
+                while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
553
+                    $this->runBackgroundScanJob(function () use ($path) {
554
+                        $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
555
+                    }, $path);
556
+                    // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
557
+                    // to make this possible
558
+                    $lastPath = $path;
559
+                }
560
+            }
561
+        }
562
+    }
563
+
564
+    protected function runBackgroundScanJob(callable $callback, $path) {
565
+        try {
566
+            $callback();
567
+            \OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
568
+            if ($this->cacheActive && $this->cache instanceof Cache) {
569
+                $this->cache->correctFolderSize($path, null, true);
570
+            }
571
+        } catch (\OCP\Files\StorageInvalidException $e) {
572
+            // skip unavailable storages
573
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
574
+            // skip unavailable storages
575
+        } catch (\OCP\Files\ForbiddenException $e) {
576
+            // skip forbidden storages
577
+        } catch (\OCP\Lock\LockedException $e) {
578
+            // skip unavailable storages
579
+        }
580
+    }
581
+
582
+    /**
583
+     * Set whether the cache is affected by scan operations
584
+     *
585
+     * @param boolean $active The active state of the cache
586
+     */
587
+    public function setCacheActive($active) {
588
+        $this->cacheActive = $active;
589
+    }
590 590
 }
Please login to merge, or discard this patch.
Spacing   +9 added lines, -9 removed lines patch added patch discarded remove patch
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 		}
337 337
 		if ($lock) {
338 338
 			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
339
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
339
+				$this->storage->acquireLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
340 340
 				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
341 341
 			}
342 342
 		}
@@ -355,7 +355,7 @@  discard block
 block discarded – undo
355 355
 			if ($lock) {
356 356
 				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
357 357
 					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
358
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
358
+					$this->storage->releaseLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
359 359
 				}
360 360
 			}
361 361
 		}
@@ -448,14 +448,14 @@  discard block
 block discarded – undo
448 448
 			$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
449 449
 			if (trim($originalFile, '/') !== $file) {
450 450
 				// encoding mismatch, might require compatibility wrapper
451
-				\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
452
-				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
451
+				\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'.$originalFile.'" in path "'.$path.'".', ['app' => 'core']);
452
+				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path.'/'.$originalFile : $originalFile]);
453 453
 				// skip this entry
454 454
 				continue;
455 455
 			}
456 456
 
457 457
 			$newChildNames[] = $file;
458
-			$child = $path ? $path . '/' . $file : $file;
458
+			$child = $path ? $path.'/'.$file : $file;
459 459
 			try {
460 460
 				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
461 461
 				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
@@ -479,7 +479,7 @@  discard block
 block discarded – undo
479 479
 					$this->connection->rollback();
480 480
 					$this->connection->beginTransaction();
481 481
 				}
482
-				\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
482
+				\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "'.$child.'"', [
483 483
 					'app' => 'core',
484 484
 					'exception' => $ex,
485 485
 				]);
@@ -493,7 +493,7 @@  discard block
 block discarded – undo
493 493
 		}
494 494
 		$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
495 495
 		foreach ($removedChildren as $childName) {
496
-			$child = $path ? $path . '/' . $childName : $childName;
496
+			$child = $path ? $path.'/'.$childName : $childName;
497 497
 			$this->removeFromCache($child);
498 498
 		}
499 499
 		if ($this->useTransactions) {
@@ -543,14 +543,14 @@  discard block
 block discarded – undo
543 543
 		} else {
544 544
 			if (!$this->cache->inCache('')) {
545 545
 				// if the storage isn't in the cache yet, just scan the root completely
546
-				$this->runBackgroundScanJob(function () {
546
+				$this->runBackgroundScanJob(function() {
547 547
 					$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
548 548
 				}, '');
549 549
 			} else {
550 550
 				$lastPath = null;
551 551
 				// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
552 552
 				while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
553
-					$this->runBackgroundScanJob(function () use ($path) {
553
+					$this->runBackgroundScanJob(function() use ($path) {
554 554
 						$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
555 555
 					}, $path);
556 556
 					// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
Please login to merge, or discard this patch.