Passed
Push — master ( e81fdf...9f1d49 )
by Robin
16:12 queued 13s
created
lib/private/Files/Cache/Wrapper/CacheJail.php 1 patch
Indentation   +272 added lines, -272 removed lines patch added patch discarded remove patch
@@ -39,305 +39,305 @@
 block discarded – undo
39 39
  * Jail to a subdirectory of the wrapped cache
40 40
  */
41 41
 class CacheJail extends CacheWrapper {
42
-	/**
43
-	 * @var string
44
-	 */
45
-	protected $root;
46
-	protected $unjailedRoot;
42
+    /**
43
+     * @var string
44
+     */
45
+    protected $root;
46
+    protected $unjailedRoot;
47 47
 
48
-	/**
49
-	 * @param ?\OCP\Files\Cache\ICache $cache
50
-	 * @param string $root
51
-	 */
52
-	public function __construct($cache, $root) {
53
-		parent::__construct($cache);
54
-		$this->root = $root;
55
-		$this->connection = \OC::$server->getDatabaseConnection();
56
-		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
48
+    /**
49
+     * @param ?\OCP\Files\Cache\ICache $cache
50
+     * @param string $root
51
+     */
52
+    public function __construct($cache, $root) {
53
+        parent::__construct($cache);
54
+        $this->root = $root;
55
+        $this->connection = \OC::$server->getDatabaseConnection();
56
+        $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
57 57
 
58
-		if ($cache instanceof CacheJail) {
59
-			$this->unjailedRoot = $cache->getSourcePath($root);
60
-		} else {
61
-			$this->unjailedRoot = $root;
62
-		}
63
-	}
58
+        if ($cache instanceof CacheJail) {
59
+            $this->unjailedRoot = $cache->getSourcePath($root);
60
+        } else {
61
+            $this->unjailedRoot = $root;
62
+        }
63
+    }
64 64
 
65
-	protected function getRoot() {
66
-		return $this->root;
67
-	}
65
+    protected function getRoot() {
66
+        return $this->root;
67
+    }
68 68
 
69
-	/**
70
-	 * Get the root path with any nested jails resolved
71
-	 *
72
-	 * @return string
73
-	 */
74
-	protected function getGetUnjailedRoot() {
75
-		return $this->unjailedRoot;
76
-	}
69
+    /**
70
+     * Get the root path with any nested jails resolved
71
+     *
72
+     * @return string
73
+     */
74
+    protected function getGetUnjailedRoot() {
75
+        return $this->unjailedRoot;
76
+    }
77 77
 
78
-	protected function getSourcePath($path) {
79
-		if ($path === '') {
80
-			return $this->getRoot();
81
-		} else {
82
-			return $this->getRoot() . '/' . ltrim($path, '/');
83
-		}
84
-	}
78
+    protected function getSourcePath($path) {
79
+        if ($path === '') {
80
+            return $this->getRoot();
81
+        } else {
82
+            return $this->getRoot() . '/' . ltrim($path, '/');
83
+        }
84
+    }
85 85
 
86
-	/**
87
-	 * @param string $path
88
-	 * @param null|string $root
89
-	 * @return null|string the jailed path or null if the path is outside the jail
90
-	 */
91
-	protected function getJailedPath(string $path, string $root = null) {
92
-		if ($root === null) {
93
-			$root = $this->getRoot();
94
-		}
95
-		if ($root === '') {
96
-			return $path;
97
-		}
98
-		$rootLength = strlen($root) + 1;
99
-		if ($path === $root) {
100
-			return '';
101
-		} elseif (substr($path, 0, $rootLength) === $root . '/') {
102
-			return substr($path, $rootLength);
103
-		} else {
104
-			return null;
105
-		}
106
-	}
86
+    /**
87
+     * @param string $path
88
+     * @param null|string $root
89
+     * @return null|string the jailed path or null if the path is outside the jail
90
+     */
91
+    protected function getJailedPath(string $path, string $root = null) {
92
+        if ($root === null) {
93
+            $root = $this->getRoot();
94
+        }
95
+        if ($root === '') {
96
+            return $path;
97
+        }
98
+        $rootLength = strlen($root) + 1;
99
+        if ($path === $root) {
100
+            return '';
101
+        } elseif (substr($path, 0, $rootLength) === $root . '/') {
102
+            return substr($path, $rootLength);
103
+        } else {
104
+            return null;
105
+        }
106
+    }
107 107
 
108
-	protected function formatCacheEntry($entry) {
109
-		if (isset($entry['path'])) {
110
-			$entry['path'] = $this->getJailedPath($entry['path']);
111
-		}
112
-		return $entry;
113
-	}
108
+    protected function formatCacheEntry($entry) {
109
+        if (isset($entry['path'])) {
110
+            $entry['path'] = $this->getJailedPath($entry['path']);
111
+        }
112
+        return $entry;
113
+    }
114 114
 
115
-	/**
116
-	 * get the stored metadata of a file or folder
117
-	 *
118
-	 * @param string /int $file
119
-	 * @return ICacheEntry|false
120
-	 */
121
-	public function get($file) {
122
-		if (is_string($file) or $file == '') {
123
-			$file = $this->getSourcePath($file);
124
-		}
125
-		return parent::get($file);
126
-	}
115
+    /**
116
+     * get the stored metadata of a file or folder
117
+     *
118
+     * @param string /int $file
119
+     * @return ICacheEntry|false
120
+     */
121
+    public function get($file) {
122
+        if (is_string($file) or $file == '') {
123
+            $file = $this->getSourcePath($file);
124
+        }
125
+        return parent::get($file);
126
+    }
127 127
 
128
-	/**
129
-	 * insert meta data for a new file or folder
130
-	 *
131
-	 * @param string $file
132
-	 * @param array $data
133
-	 *
134
-	 * @return int file id
135
-	 * @throws \RuntimeException
136
-	 */
137
-	public function insert($file, array $data) {
138
-		return $this->getCache()->insert($this->getSourcePath($file), $data);
139
-	}
128
+    /**
129
+     * insert meta data for a new file or folder
130
+     *
131
+     * @param string $file
132
+     * @param array $data
133
+     *
134
+     * @return int file id
135
+     * @throws \RuntimeException
136
+     */
137
+    public function insert($file, array $data) {
138
+        return $this->getCache()->insert($this->getSourcePath($file), $data);
139
+    }
140 140
 
141
-	/**
142
-	 * update the metadata in the cache
143
-	 *
144
-	 * @param int $id
145
-	 * @param array $data
146
-	 */
147
-	public function update($id, array $data) {
148
-		$this->getCache()->update($id, $data);
149
-	}
141
+    /**
142
+     * update the metadata in the cache
143
+     *
144
+     * @param int $id
145
+     * @param array $data
146
+     */
147
+    public function update($id, array $data) {
148
+        $this->getCache()->update($id, $data);
149
+    }
150 150
 
151
-	/**
152
-	 * get the file id for a file
153
-	 *
154
-	 * @param string $file
155
-	 * @return int
156
-	 */
157
-	public function getId($file) {
158
-		return $this->getCache()->getId($this->getSourcePath($file));
159
-	}
151
+    /**
152
+     * get the file id for a file
153
+     *
154
+     * @param string $file
155
+     * @return int
156
+     */
157
+    public function getId($file) {
158
+        return $this->getCache()->getId($this->getSourcePath($file));
159
+    }
160 160
 
161
-	/**
162
-	 * get the id of the parent folder of a file
163
-	 *
164
-	 * @param string $file
165
-	 * @return int
166
-	 */
167
-	public function getParentId($file) {
168
-		return $this->getCache()->getParentId($this->getSourcePath($file));
169
-	}
161
+    /**
162
+     * get the id of the parent folder of a file
163
+     *
164
+     * @param string $file
165
+     * @return int
166
+     */
167
+    public function getParentId($file) {
168
+        return $this->getCache()->getParentId($this->getSourcePath($file));
169
+    }
170 170
 
171
-	/**
172
-	 * check if a file is available in the cache
173
-	 *
174
-	 * @param string $file
175
-	 * @return bool
176
-	 */
177
-	public function inCache($file) {
178
-		return $this->getCache()->inCache($this->getSourcePath($file));
179
-	}
171
+    /**
172
+     * check if a file is available in the cache
173
+     *
174
+     * @param string $file
175
+     * @return bool
176
+     */
177
+    public function inCache($file) {
178
+        return $this->getCache()->inCache($this->getSourcePath($file));
179
+    }
180 180
 
181
-	/**
182
-	 * remove a file or folder from the cache
183
-	 *
184
-	 * @param string $file
185
-	 */
186
-	public function remove($file) {
187
-		$this->getCache()->remove($this->getSourcePath($file));
188
-	}
181
+    /**
182
+     * remove a file or folder from the cache
183
+     *
184
+     * @param string $file
185
+     */
186
+    public function remove($file) {
187
+        $this->getCache()->remove($this->getSourcePath($file));
188
+    }
189 189
 
190
-	/**
191
-	 * Move a file or folder in the cache
192
-	 *
193
-	 * @param string $source
194
-	 * @param string $target
195
-	 */
196
-	public function move($source, $target) {
197
-		$this->getCache()->move($this->getSourcePath($source), $this->getSourcePath($target));
198
-	}
190
+    /**
191
+     * Move a file or folder in the cache
192
+     *
193
+     * @param string $source
194
+     * @param string $target
195
+     */
196
+    public function move($source, $target) {
197
+        $this->getCache()->move($this->getSourcePath($source), $this->getSourcePath($target));
198
+    }
199 199
 
200
-	/**
201
-	 * Get the storage id and path needed for a move
202
-	 *
203
-	 * @param string $path
204
-	 * @return array [$storageId, $internalPath]
205
-	 */
206
-	protected function getMoveInfo($path) {
207
-		return [$this->getNumericStorageId(), $this->getSourcePath($path)];
208
-	}
200
+    /**
201
+     * Get the storage id and path needed for a move
202
+     *
203
+     * @param string $path
204
+     * @return array [$storageId, $internalPath]
205
+     */
206
+    protected function getMoveInfo($path) {
207
+        return [$this->getNumericStorageId(), $this->getSourcePath($path)];
208
+    }
209 209
 
210
-	/**
211
-	 * remove all entries for files that are stored on the storage from the cache
212
-	 */
213
-	public function clear() {
214
-		$this->getCache()->remove($this->getRoot());
215
-	}
210
+    /**
211
+     * remove all entries for files that are stored on the storage from the cache
212
+     */
213
+    public function clear() {
214
+        $this->getCache()->remove($this->getRoot());
215
+    }
216 216
 
217
-	/**
218
-	 * @param string $file
219
-	 *
220
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
221
-	 */
222
-	public function getStatus($file) {
223
-		return $this->getCache()->getStatus($this->getSourcePath($file));
224
-	}
217
+    /**
218
+     * @param string $file
219
+     *
220
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
221
+     */
222
+    public function getStatus($file) {
223
+        return $this->getCache()->getStatus($this->getSourcePath($file));
224
+    }
225 225
 
226
-	/**
227
-	 * update the folder size and the size of all parent folders
228
-	 *
229
-	 * @param string|boolean $path
230
-	 * @param array $data (optional) meta data of the folder
231
-	 */
232
-	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
233
-		if ($this->getCache() instanceof Cache) {
234
-			$this->getCache()->correctFolderSize($this->getSourcePath($path), $data, $isBackgroundScan);
235
-		}
236
-	}
226
+    /**
227
+     * update the folder size and the size of all parent folders
228
+     *
229
+     * @param string|boolean $path
230
+     * @param array $data (optional) meta data of the folder
231
+     */
232
+    public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
233
+        if ($this->getCache() instanceof Cache) {
234
+            $this->getCache()->correctFolderSize($this->getSourcePath($path), $data, $isBackgroundScan);
235
+        }
236
+    }
237 237
 
238
-	/**
239
-	 * get the size of a folder and set it in the cache
240
-	 *
241
-	 * @param string $path
242
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
243
-	 * @return int|float
244
-	 */
245
-	public function calculateFolderSize($path, $entry = null) {
246
-		if ($this->getCache() instanceof Cache) {
247
-			return $this->getCache()->calculateFolderSize($this->getSourcePath($path), $entry);
248
-		} else {
249
-			return 0;
250
-		}
251
-	}
238
+    /**
239
+     * get the size of a folder and set it in the cache
240
+     *
241
+     * @param string $path
242
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
243
+     * @return int|float
244
+     */
245
+    public function calculateFolderSize($path, $entry = null) {
246
+        if ($this->getCache() instanceof Cache) {
247
+            return $this->getCache()->calculateFolderSize($this->getSourcePath($path), $entry);
248
+        } else {
249
+            return 0;
250
+        }
251
+    }
252 252
 
253
-	/**
254
-	 * get all file ids on the files on the storage
255
-	 *
256
-	 * @return int[]
257
-	 */
258
-	public function getAll() {
259
-		// not supported
260
-		return [];
261
-	}
253
+    /**
254
+     * get all file ids on the files on the storage
255
+     *
256
+     * @return int[]
257
+     */
258
+    public function getAll() {
259
+        // not supported
260
+        return [];
261
+    }
262 262
 
263
-	/**
264
-	 * find a folder in the cache which has not been fully scanned
265
-	 *
266
-	 * If multiply incomplete folders are in the cache, the one with the highest id will be returned,
267
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
268
-	 * likely the folder where we stopped scanning previously
269
-	 *
270
-	 * @return string|false the path of the folder or false when no folder matched
271
-	 */
272
-	public function getIncomplete() {
273
-		// not supported
274
-		return false;
275
-	}
263
+    /**
264
+     * find a folder in the cache which has not been fully scanned
265
+     *
266
+     * If multiply incomplete folders are in the cache, the one with the highest id will be returned,
267
+     * use the one with the highest id gives the best result with the background scanner, since that is most
268
+     * likely the folder where we stopped scanning previously
269
+     *
270
+     * @return string|false the path of the folder or false when no folder matched
271
+     */
272
+    public function getIncomplete() {
273
+        // not supported
274
+        return false;
275
+    }
276 276
 
277
-	/**
278
-	 * get the path of a file on this storage by it's id
279
-	 *
280
-	 * @param int $id
281
-	 * @return string|null
282
-	 */
283
-	public function getPathById($id) {
284
-		$path = $this->getCache()->getPathById($id);
285
-		if ($path === null) {
286
-			return null;
287
-		}
277
+    /**
278
+     * get the path of a file on this storage by it's id
279
+     *
280
+     * @param int $id
281
+     * @return string|null
282
+     */
283
+    public function getPathById($id) {
284
+        $path = $this->getCache()->getPathById($id);
285
+        if ($path === null) {
286
+            return null;
287
+        }
288 288
 
289
-		return $this->getJailedPath($path);
290
-	}
289
+        return $this->getJailedPath($path);
290
+    }
291 291
 
292
-	/**
293
-	 * Move a file or folder in the cache
294
-	 *
295
-	 * Note that this should make sure the entries are removed from the source cache
296
-	 *
297
-	 * @param \OCP\Files\Cache\ICache $sourceCache
298
-	 * @param string $sourcePath
299
-	 * @param string $targetPath
300
-	 */
301
-	public function moveFromCache(\OCP\Files\Cache\ICache $sourceCache, $sourcePath, $targetPath) {
302
-		if ($sourceCache === $this) {
303
-			return $this->move($sourcePath, $targetPath);
304
-		}
305
-		return $this->getCache()->moveFromCache($sourceCache, $sourcePath, $this->getSourcePath($targetPath));
306
-	}
292
+    /**
293
+     * Move a file or folder in the cache
294
+     *
295
+     * Note that this should make sure the entries are removed from the source cache
296
+     *
297
+     * @param \OCP\Files\Cache\ICache $sourceCache
298
+     * @param string $sourcePath
299
+     * @param string $targetPath
300
+     */
301
+    public function moveFromCache(\OCP\Files\Cache\ICache $sourceCache, $sourcePath, $targetPath) {
302
+        if ($sourceCache === $this) {
303
+            return $this->move($sourcePath, $targetPath);
304
+        }
305
+        return $this->getCache()->moveFromCache($sourceCache, $sourcePath, $this->getSourcePath($targetPath));
306
+    }
307 307
 
308
-	public function getQueryFilterForStorage(): ISearchOperator {
309
-		return $this->addJailFilterQuery($this->getCache()->getQueryFilterForStorage());
310
-	}
308
+    public function getQueryFilterForStorage(): ISearchOperator {
309
+        return $this->addJailFilterQuery($this->getCache()->getQueryFilterForStorage());
310
+    }
311 311
 
312
-	protected function addJailFilterQuery(ISearchOperator $filter): ISearchOperator {
313
-		if ($this->getGetUnjailedRoot() !== '' && $this->getGetUnjailedRoot() !== '/') {
314
-			return new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND,
315
-				[
316
-					$filter,
317
-					new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR,
318
-						[
319
-							new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', $this->getGetUnjailedRoot()),
320
-							new SearchComparison(ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE, 'path', SearchComparison::escapeLikeParameter($this->getGetUnjailedRoot()) . '/%'),
321
-						],
322
-					)
323
-				]
324
-			);
325
-		} else {
326
-			return $filter;
327
-		}
328
-	}
312
+    protected function addJailFilterQuery(ISearchOperator $filter): ISearchOperator {
313
+        if ($this->getGetUnjailedRoot() !== '' && $this->getGetUnjailedRoot() !== '/') {
314
+            return new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_AND,
315
+                [
316
+                    $filter,
317
+                    new SearchBinaryOperator(ISearchBinaryOperator::OPERATOR_OR,
318
+                        [
319
+                            new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'path', $this->getGetUnjailedRoot()),
320
+                            new SearchComparison(ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE, 'path', SearchComparison::escapeLikeParameter($this->getGetUnjailedRoot()) . '/%'),
321
+                        ],
322
+                    )
323
+                ]
324
+            );
325
+        } else {
326
+            return $filter;
327
+        }
328
+    }
329 329
 
330
-	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
331
-		if ($this->getGetUnjailedRoot() === '' || str_starts_with($rawEntry->getPath(), $this->getGetUnjailedRoot())) {
332
-			$rawEntry = $this->getCache()->getCacheEntryFromSearchResult($rawEntry);
333
-			if ($rawEntry) {
334
-				$jailedPath = $this->getJailedPath($rawEntry->getPath());
335
-				if ($jailedPath !== null) {
336
-					return $this->formatCacheEntry(clone $rawEntry) ?: null;
337
-				}
338
-			}
339
-		}
330
+    public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
331
+        if ($this->getGetUnjailedRoot() === '' || str_starts_with($rawEntry->getPath(), $this->getGetUnjailedRoot())) {
332
+            $rawEntry = $this->getCache()->getCacheEntryFromSearchResult($rawEntry);
333
+            if ($rawEntry) {
334
+                $jailedPath = $this->getJailedPath($rawEntry->getPath());
335
+                if ($jailedPath !== null) {
336
+                    return $this->formatCacheEntry(clone $rawEntry) ?: null;
337
+                }
338
+            }
339
+        }
340 340
 
341
-		return null;
342
-	}
341
+        return null;
342
+    }
343 343
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/SearchBuilder.php 2 patches
Indentation   +215 added lines, -215 removed lines patch added patch discarded remove patch
@@ -37,219 +37,219 @@
 block discarded – undo
37 37
  * Tools for transforming search queries into database queries
38 38
  */
39 39
 class SearchBuilder {
40
-	protected static $searchOperatorMap = [
41
-		ISearchComparison::COMPARE_LIKE => 'iLike',
42
-		ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
43
-		ISearchComparison::COMPARE_EQUAL => 'eq',
44
-		ISearchComparison::COMPARE_GREATER_THAN => 'gt',
45
-		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
46
-		ISearchComparison::COMPARE_LESS_THAN => 'lt',
47
-		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
48
-	];
49
-
50
-	protected static $searchOperatorNegativeMap = [
51
-		ISearchComparison::COMPARE_LIKE => 'notLike',
52
-		ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
53
-		ISearchComparison::COMPARE_EQUAL => 'neq',
54
-		ISearchComparison::COMPARE_GREATER_THAN => 'lte',
55
-		ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
56
-		ISearchComparison::COMPARE_LESS_THAN => 'gte',
57
-		ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'gt',
58
-	];
59
-
60
-	public const TAG_FAVORITE = '_$!<Favorite>!$_';
61
-
62
-	/** @var IMimeTypeLoader */
63
-	private $mimetypeLoader;
64
-
65
-	public function __construct(
66
-		IMimeTypeLoader $mimetypeLoader
67
-	) {
68
-		$this->mimetypeLoader = $mimetypeLoader;
69
-	}
70
-
71
-	/**
72
-	 * Whether or not the tag tables should be joined to complete the search
73
-	 *
74
-	 * @param ISearchOperator $operator
75
-	 * @return boolean
76
-	 */
77
-	public function shouldJoinTags(ISearchOperator $operator) {
78
-		if ($operator instanceof ISearchBinaryOperator) {
79
-			return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
80
-				return $shouldJoin || $this->shouldJoinTags($operator);
81
-			}, false);
82
-		} elseif ($operator instanceof ISearchComparison) {
83
-			return $operator->getField() === 'tagname' || $operator->getField() === 'favorite' || $operator->getField() === 'systemtag';
84
-		}
85
-		return false;
86
-	}
87
-
88
-	/**
89
-	 * @param IQueryBuilder $builder
90
-	 * @param ISearchOperator[] $operators
91
-	 */
92
-	public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
93
-		return array_filter(array_map(function ($operator) use ($builder) {
94
-			return $this->searchOperatorToDBExpr($builder, $operator);
95
-		}, $operators));
96
-	}
97
-
98
-	public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
99
-		$expr = $builder->expr();
100
-
101
-		if ($operator instanceof ISearchBinaryOperator) {
102
-			if (count($operator->getArguments()) === 0) {
103
-				return null;
104
-			}
105
-
106
-			switch ($operator->getType()) {
107
-				case ISearchBinaryOperator::OPERATOR_NOT:
108
-					$negativeOperator = $operator->getArguments()[0];
109
-					if ($negativeOperator instanceof ISearchComparison) {
110
-						return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
111
-					} else {
112
-						throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
113
-					}
114
-					// no break
115
-				case ISearchBinaryOperator::OPERATOR_AND:
116
-					return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
117
-				case ISearchBinaryOperator::OPERATOR_OR:
118
-					return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
119
-				default:
120
-					throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
121
-			}
122
-		} elseif ($operator instanceof ISearchComparison) {
123
-			return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
124
-		} else {
125
-			throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
126
-		}
127
-	}
128
-
129
-	private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
130
-		$this->validateComparison($comparison);
131
-
132
-		[$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
133
-		if (isset($operatorMap[$type])) {
134
-			$queryOperator = $operatorMap[$type];
135
-			return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
136
-		} else {
137
-			throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
138
-		}
139
-	}
140
-
141
-	private function getOperatorFieldAndValue(ISearchComparison $operator) {
142
-		$field = $operator->getField();
143
-		$value = $operator->getValue();
144
-		$type = $operator->getType();
145
-		if ($field === 'mimetype') {
146
-			$value = (string)$value;
147
-			if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
148
-				$value = (int)$this->mimetypeLoader->getId($value);
149
-			} elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
150
-				// transform "mimetype='foo/%'" to "mimepart='foo'"
151
-				if (preg_match('|(.+)/%|', $value, $matches)) {
152
-					$field = 'mimepart';
153
-					$value = (int)$this->mimetypeLoader->getId($matches[1]);
154
-					$type = ISearchComparison::COMPARE_EQUAL;
155
-				} elseif (str_contains($value, '%')) {
156
-					throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
157
-				} else {
158
-					$field = 'mimetype';
159
-					$value = (int)$this->mimetypeLoader->getId($value);
160
-					$type = ISearchComparison::COMPARE_EQUAL;
161
-				}
162
-			}
163
-		} elseif ($field === 'favorite') {
164
-			$field = 'tag.category';
165
-			$value = self::TAG_FAVORITE;
166
-		} elseif ($field === 'name') {
167
-			$field = 'file.name';
168
-		} elseif ($field === 'tagname') {
169
-			$field = 'tag.category';
170
-		} elseif ($field === 'systemtag') {
171
-			$field = 'systemtag.name';
172
-		} elseif ($field === 'fileid') {
173
-			$field = 'file.fileid';
174
-		} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
175
-			$field = 'path_hash';
176
-			$value = md5((string)$value);
177
-		}
178
-		return [$field, $value, $type];
179
-	}
180
-
181
-	private function validateComparison(ISearchComparison $operator) {
182
-		$types = [
183
-			'mimetype' => 'string',
184
-			'mtime' => 'integer',
185
-			'name' => 'string',
186
-			'path' => 'string',
187
-			'size' => 'integer',
188
-			'tagname' => 'string',
189
-			'systemtag' => 'string',
190
-			'favorite' => 'boolean',
191
-			'fileid' => 'integer',
192
-			'storage' => 'integer',
193
-		];
194
-		$comparisons = [
195
-			'mimetype' => ['eq', 'like'],
196
-			'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
197
-			'name' => ['eq', 'like', 'clike'],
198
-			'path' => ['eq', 'like', 'clike'],
199
-			'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
200
-			'tagname' => ['eq', 'like'],
201
-			'systemtag' => ['eq', 'like'],
202
-			'favorite' => ['eq'],
203
-			'fileid' => ['eq'],
204
-			'storage' => ['eq'],
205
-		];
206
-
207
-		if (!isset($types[$operator->getField()])) {
208
-			throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
209
-		}
210
-		$type = $types[$operator->getField()];
211
-		if (gettype($operator->getValue()) !== $type) {
212
-			throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
213
-		}
214
-		if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
215
-			throw new \InvalidArgumentException('Unsupported comparison for field  ' . $operator->getField() . ': ' . $operator->getType());
216
-		}
217
-	}
218
-
219
-	private function getParameterForValue(IQueryBuilder $builder, $value) {
220
-		if ($value instanceof \DateTime) {
221
-			$value = $value->getTimestamp();
222
-		}
223
-		if (is_numeric($value)) {
224
-			$type = IQueryBuilder::PARAM_INT;
225
-		} else {
226
-			$type = IQueryBuilder::PARAM_STR;
227
-		}
228
-		return $builder->createNamedParameter($value, $type);
229
-	}
230
-
231
-	/**
232
-	 * @param IQueryBuilder $query
233
-	 * @param ISearchOrder[] $orders
234
-	 */
235
-	public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
236
-		foreach ($orders as $order) {
237
-			$field = $order->getField();
238
-			if ($field === 'fileid') {
239
-				$field = 'file.fileid';
240
-			}
241
-
242
-			// Mysql really likes to pick an index for sorting if it can't fully satisfy the where
243
-			// filter with an index, since search queries pretty much never are fully filtered by index
244
-			// mysql often picks an index for sorting instead of the much more useful index for filtering.
245
-			//
246
-			// By changing the order by to an expression, mysql isn't smart enough to see that it could still
247
-			// use the index, so it instead picks an index for the filtering
248
-			if ($field === 'mtime') {
249
-				$field = $query->func()->add($field, $query->createNamedParameter(0));
250
-			}
251
-
252
-			$query->addOrderBy($field, $order->getDirection());
253
-		}
254
-	}
40
+    protected static $searchOperatorMap = [
41
+        ISearchComparison::COMPARE_LIKE => 'iLike',
42
+        ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'like',
43
+        ISearchComparison::COMPARE_EQUAL => 'eq',
44
+        ISearchComparison::COMPARE_GREATER_THAN => 'gt',
45
+        ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'gte',
46
+        ISearchComparison::COMPARE_LESS_THAN => 'lt',
47
+        ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'lte',
48
+    ];
49
+
50
+    protected static $searchOperatorNegativeMap = [
51
+        ISearchComparison::COMPARE_LIKE => 'notLike',
52
+        ISearchComparison::COMPARE_LIKE_CASE_SENSITIVE => 'notLike',
53
+        ISearchComparison::COMPARE_EQUAL => 'neq',
54
+        ISearchComparison::COMPARE_GREATER_THAN => 'lte',
55
+        ISearchComparison::COMPARE_GREATER_THAN_EQUAL => 'lt',
56
+        ISearchComparison::COMPARE_LESS_THAN => 'gte',
57
+        ISearchComparison::COMPARE_LESS_THAN_EQUAL => 'gt',
58
+    ];
59
+
60
+    public const TAG_FAVORITE = '_$!<Favorite>!$_';
61
+
62
+    /** @var IMimeTypeLoader */
63
+    private $mimetypeLoader;
64
+
65
+    public function __construct(
66
+        IMimeTypeLoader $mimetypeLoader
67
+    ) {
68
+        $this->mimetypeLoader = $mimetypeLoader;
69
+    }
70
+
71
+    /**
72
+     * Whether or not the tag tables should be joined to complete the search
73
+     *
74
+     * @param ISearchOperator $operator
75
+     * @return boolean
76
+     */
77
+    public function shouldJoinTags(ISearchOperator $operator) {
78
+        if ($operator instanceof ISearchBinaryOperator) {
79
+            return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
80
+                return $shouldJoin || $this->shouldJoinTags($operator);
81
+            }, false);
82
+        } elseif ($operator instanceof ISearchComparison) {
83
+            return $operator->getField() === 'tagname' || $operator->getField() === 'favorite' || $operator->getField() === 'systemtag';
84
+        }
85
+        return false;
86
+    }
87
+
88
+    /**
89
+     * @param IQueryBuilder $builder
90
+     * @param ISearchOperator[] $operators
91
+     */
92
+    public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
93
+        return array_filter(array_map(function ($operator) use ($builder) {
94
+            return $this->searchOperatorToDBExpr($builder, $operator);
95
+        }, $operators));
96
+    }
97
+
98
+    public function searchOperatorToDBExpr(IQueryBuilder $builder, ISearchOperator $operator) {
99
+        $expr = $builder->expr();
100
+
101
+        if ($operator instanceof ISearchBinaryOperator) {
102
+            if (count($operator->getArguments()) === 0) {
103
+                return null;
104
+            }
105
+
106
+            switch ($operator->getType()) {
107
+                case ISearchBinaryOperator::OPERATOR_NOT:
108
+                    $negativeOperator = $operator->getArguments()[0];
109
+                    if ($negativeOperator instanceof ISearchComparison) {
110
+                        return $this->searchComparisonToDBExpr($builder, $negativeOperator, self::$searchOperatorNegativeMap);
111
+                    } else {
112
+                        throw new \InvalidArgumentException('Binary operators inside "not" is not supported');
113
+                    }
114
+                    // no break
115
+                case ISearchBinaryOperator::OPERATOR_AND:
116
+                    return call_user_func_array([$expr, 'andX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
117
+                case ISearchBinaryOperator::OPERATOR_OR:
118
+                    return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
119
+                default:
120
+                    throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
121
+            }
122
+        } elseif ($operator instanceof ISearchComparison) {
123
+            return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
124
+        } else {
125
+            throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
126
+        }
127
+    }
128
+
129
+    private function searchComparisonToDBExpr(IQueryBuilder $builder, ISearchComparison $comparison, array $operatorMap) {
130
+        $this->validateComparison($comparison);
131
+
132
+        [$field, $value, $type] = $this->getOperatorFieldAndValue($comparison);
133
+        if (isset($operatorMap[$type])) {
134
+            $queryOperator = $operatorMap[$type];
135
+            return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
136
+        } else {
137
+            throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
138
+        }
139
+    }
140
+
141
+    private function getOperatorFieldAndValue(ISearchComparison $operator) {
142
+        $field = $operator->getField();
143
+        $value = $operator->getValue();
144
+        $type = $operator->getType();
145
+        if ($field === 'mimetype') {
146
+            $value = (string)$value;
147
+            if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
148
+                $value = (int)$this->mimetypeLoader->getId($value);
149
+            } elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
150
+                // transform "mimetype='foo/%'" to "mimepart='foo'"
151
+                if (preg_match('|(.+)/%|', $value, $matches)) {
152
+                    $field = 'mimepart';
153
+                    $value = (int)$this->mimetypeLoader->getId($matches[1]);
154
+                    $type = ISearchComparison::COMPARE_EQUAL;
155
+                } elseif (str_contains($value, '%')) {
156
+                    throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
157
+                } else {
158
+                    $field = 'mimetype';
159
+                    $value = (int)$this->mimetypeLoader->getId($value);
160
+                    $type = ISearchComparison::COMPARE_EQUAL;
161
+                }
162
+            }
163
+        } elseif ($field === 'favorite') {
164
+            $field = 'tag.category';
165
+            $value = self::TAG_FAVORITE;
166
+        } elseif ($field === 'name') {
167
+            $field = 'file.name';
168
+        } elseif ($field === 'tagname') {
169
+            $field = 'tag.category';
170
+        } elseif ($field === 'systemtag') {
171
+            $field = 'systemtag.name';
172
+        } elseif ($field === 'fileid') {
173
+            $field = 'file.fileid';
174
+        } elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
175
+            $field = 'path_hash';
176
+            $value = md5((string)$value);
177
+        }
178
+        return [$field, $value, $type];
179
+    }
180
+
181
+    private function validateComparison(ISearchComparison $operator) {
182
+        $types = [
183
+            'mimetype' => 'string',
184
+            'mtime' => 'integer',
185
+            'name' => 'string',
186
+            'path' => 'string',
187
+            'size' => 'integer',
188
+            'tagname' => 'string',
189
+            'systemtag' => 'string',
190
+            'favorite' => 'boolean',
191
+            'fileid' => 'integer',
192
+            'storage' => 'integer',
193
+        ];
194
+        $comparisons = [
195
+            'mimetype' => ['eq', 'like'],
196
+            'mtime' => ['eq', 'gt', 'lt', 'gte', 'lte'],
197
+            'name' => ['eq', 'like', 'clike'],
198
+            'path' => ['eq', 'like', 'clike'],
199
+            'size' => ['eq', 'gt', 'lt', 'gte', 'lte'],
200
+            'tagname' => ['eq', 'like'],
201
+            'systemtag' => ['eq', 'like'],
202
+            'favorite' => ['eq'],
203
+            'fileid' => ['eq'],
204
+            'storage' => ['eq'],
205
+        ];
206
+
207
+        if (!isset($types[$operator->getField()])) {
208
+            throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
209
+        }
210
+        $type = $types[$operator->getField()];
211
+        if (gettype($operator->getValue()) !== $type) {
212
+            throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
213
+        }
214
+        if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
215
+            throw new \InvalidArgumentException('Unsupported comparison for field  ' . $operator->getField() . ': ' . $operator->getType());
216
+        }
217
+    }
218
+
219
+    private function getParameterForValue(IQueryBuilder $builder, $value) {
220
+        if ($value instanceof \DateTime) {
221
+            $value = $value->getTimestamp();
222
+        }
223
+        if (is_numeric($value)) {
224
+            $type = IQueryBuilder::PARAM_INT;
225
+        } else {
226
+            $type = IQueryBuilder::PARAM_STR;
227
+        }
228
+        return $builder->createNamedParameter($value, $type);
229
+    }
230
+
231
+    /**
232
+     * @param IQueryBuilder $query
233
+     * @param ISearchOrder[] $orders
234
+     */
235
+    public function addSearchOrdersToQuery(IQueryBuilder $query, array $orders) {
236
+        foreach ($orders as $order) {
237
+            $field = $order->getField();
238
+            if ($field === 'fileid') {
239
+                $field = 'file.fileid';
240
+            }
241
+
242
+            // Mysql really likes to pick an index for sorting if it can't fully satisfy the where
243
+            // filter with an index, since search queries pretty much never are fully filtered by index
244
+            // mysql often picks an index for sorting instead of the much more useful index for filtering.
245
+            //
246
+            // By changing the order by to an expression, mysql isn't smart enough to see that it could still
247
+            // use the index, so it instead picks an index for the filtering
248
+            if ($field === 'mtime') {
249
+                $field = $query->func()->add($field, $query->createNamedParameter(0));
250
+            }
251
+
252
+            $query->addOrderBy($field, $order->getDirection());
253
+        }
254
+    }
255 255
 }
Please login to merge, or discard this patch.
Spacing   +14 added lines, -14 removed lines patch added patch discarded remove patch
@@ -76,7 +76,7 @@  discard block
 block discarded – undo
76 76
 	 */
77 77
 	public function shouldJoinTags(ISearchOperator $operator) {
78 78
 		if ($operator instanceof ISearchBinaryOperator) {
79
-			return array_reduce($operator->getArguments(), function ($shouldJoin, ISearchOperator $operator) {
79
+			return array_reduce($operator->getArguments(), function($shouldJoin, ISearchOperator $operator) {
80 80
 				return $shouldJoin || $this->shouldJoinTags($operator);
81 81
 			}, false);
82 82
 		} elseif ($operator instanceof ISearchComparison) {
@@ -90,7 +90,7 @@  discard block
 block discarded – undo
90 90
 	 * @param ISearchOperator[] $operators
91 91
 	 */
92 92
 	public function searchOperatorArrayToDBExprArray(IQueryBuilder $builder, array $operators) {
93
-		return array_filter(array_map(function ($operator) use ($builder) {
93
+		return array_filter(array_map(function($operator) use ($builder) {
94 94
 			return $this->searchOperatorToDBExpr($builder, $operator);
95 95
 		}, $operators));
96 96
 	}
@@ -117,12 +117,12 @@  discard block
 block discarded – undo
117 117
 				case ISearchBinaryOperator::OPERATOR_OR:
118 118
 					return call_user_func_array([$expr, 'orX'], $this->searchOperatorArrayToDBExprArray($builder, $operator->getArguments()));
119 119
 				default:
120
-					throw new \InvalidArgumentException('Invalid operator type: ' . $operator->getType());
120
+					throw new \InvalidArgumentException('Invalid operator type: '.$operator->getType());
121 121
 			}
122 122
 		} elseif ($operator instanceof ISearchComparison) {
123 123
 			return $this->searchComparisonToDBExpr($builder, $operator, self::$searchOperatorMap);
124 124
 		} else {
125
-			throw new \InvalidArgumentException('Invalid operator type: ' . get_class($operator));
125
+			throw new \InvalidArgumentException('Invalid operator type: '.get_class($operator));
126 126
 		}
127 127
 	}
128 128
 
@@ -134,7 +134,7 @@  discard block
 block discarded – undo
134 134
 			$queryOperator = $operatorMap[$type];
135 135
 			return $builder->expr()->$queryOperator($field, $this->getParameterForValue($builder, $value));
136 136
 		} else {
137
-			throw new \InvalidArgumentException('Invalid operator type: ' . $comparison->getType());
137
+			throw new \InvalidArgumentException('Invalid operator type: '.$comparison->getType());
138 138
 		}
139 139
 	}
140 140
 
@@ -143,20 +143,20 @@  discard block
 block discarded – undo
143 143
 		$value = $operator->getValue();
144 144
 		$type = $operator->getType();
145 145
 		if ($field === 'mimetype') {
146
-			$value = (string)$value;
146
+			$value = (string) $value;
147 147
 			if ($operator->getType() === ISearchComparison::COMPARE_EQUAL) {
148
-				$value = (int)$this->mimetypeLoader->getId($value);
148
+				$value = (int) $this->mimetypeLoader->getId($value);
149 149
 			} elseif ($operator->getType() === ISearchComparison::COMPARE_LIKE) {
150 150
 				// transform "mimetype='foo/%'" to "mimepart='foo'"
151 151
 				if (preg_match('|(.+)/%|', $value, $matches)) {
152 152
 					$field = 'mimepart';
153
-					$value = (int)$this->mimetypeLoader->getId($matches[1]);
153
+					$value = (int) $this->mimetypeLoader->getId($matches[1]);
154 154
 					$type = ISearchComparison::COMPARE_EQUAL;
155 155
 				} elseif (str_contains($value, '%')) {
156
-					throw new \InvalidArgumentException('Unsupported query value for mimetype: ' . $value . ', only values in the format "mime/type" or "mime/%" are supported');
156
+					throw new \InvalidArgumentException('Unsupported query value for mimetype: '.$value.', only values in the format "mime/type" or "mime/%" are supported');
157 157
 				} else {
158 158
 					$field = 'mimetype';
159
-					$value = (int)$this->mimetypeLoader->getId($value);
159
+					$value = (int) $this->mimetypeLoader->getId($value);
160 160
 					$type = ISearchComparison::COMPARE_EQUAL;
161 161
 				}
162 162
 			}
@@ -173,7 +173,7 @@  discard block
 block discarded – undo
173 173
 			$field = 'file.fileid';
174 174
 		} elseif ($field === 'path' && $type === ISearchComparison::COMPARE_EQUAL && $operator->getQueryHint(ISearchComparison::HINT_PATH_EQ_HASH, true)) {
175 175
 			$field = 'path_hash';
176
-			$value = md5((string)$value);
176
+			$value = md5((string) $value);
177 177
 		}
178 178
 		return [$field, $value, $type];
179 179
 	}
@@ -205,14 +205,14 @@  discard block
 block discarded – undo
205 205
 		];
206 206
 
207 207
 		if (!isset($types[$operator->getField()])) {
208
-			throw new \InvalidArgumentException('Unsupported comparison field ' . $operator->getField());
208
+			throw new \InvalidArgumentException('Unsupported comparison field '.$operator->getField());
209 209
 		}
210 210
 		$type = $types[$operator->getField()];
211 211
 		if (gettype($operator->getValue()) !== $type) {
212
-			throw new \InvalidArgumentException('Invalid type for field ' . $operator->getField());
212
+			throw new \InvalidArgumentException('Invalid type for field '.$operator->getField());
213 213
 		}
214 214
 		if (!in_array($operator->getType(), $comparisons[$operator->getField()])) {
215
-			throw new \InvalidArgumentException('Unsupported comparison for field  ' . $operator->getField() . ': ' . $operator->getType());
215
+			throw new \InvalidArgumentException('Unsupported comparison for field  '.$operator->getField().': '.$operator->getType());
216 216
 		}
217 217
 	}
218 218
 
Please login to merge, or discard this patch.
lib/private/Files/Cache/Cache.php 2 patches
Indentation   +1098 added lines, -1098 removed lines patch added patch discarded remove patch
@@ -74,1102 +74,1102 @@
 block discarded – undo
74 74
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
75 75
  */
76 76
 class Cache implements ICache {
77
-	use MoveFromCacheTrait {
78
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
79
-	}
80
-
81
-	/**
82
-	 * @var array partial data for the cache
83
-	 */
84
-	protected $partial = [];
85
-
86
-	/**
87
-	 * @var string
88
-	 */
89
-	protected $storageId;
90
-
91
-	private $storage;
92
-
93
-	/**
94
-	 * @var Storage $storageCache
95
-	 */
96
-	protected $storageCache;
97
-
98
-	/** @var IMimeTypeLoader */
99
-	protected $mimetypeLoader;
100
-
101
-	/**
102
-	 * @var IDBConnection
103
-	 */
104
-	protected $connection;
105
-
106
-	/**
107
-	 * @var IEventDispatcher
108
-	 */
109
-	protected $eventDispatcher;
110
-
111
-	/** @var QuerySearchHelper */
112
-	protected $querySearchHelper;
113
-
114
-	/**
115
-	 * @param IStorage $storage
116
-	 */
117
-	public function __construct(IStorage $storage) {
118
-		$this->storageId = $storage->getId();
119
-		$this->storage = $storage;
120
-		if (strlen($this->storageId) > 64) {
121
-			$this->storageId = md5($this->storageId);
122
-		}
123
-
124
-		$this->storageCache = new Storage($storage);
125
-		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
126
-		$this->connection = \OC::$server->getDatabaseConnection();
127
-		$this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
128
-		$this->querySearchHelper = \OC::$server->query(QuerySearchHelper::class);
129
-	}
130
-
131
-	protected function getQueryBuilder() {
132
-		return new CacheQueryBuilder(
133
-			$this->connection,
134
-			\OC::$server->getSystemConfig(),
135
-			\OC::$server->get(LoggerInterface::class)
136
-		);
137
-	}
138
-
139
-	/**
140
-	 * Get the numeric storage id for this cache's storage
141
-	 *
142
-	 * @return int
143
-	 */
144
-	public function getNumericStorageId() {
145
-		return $this->storageCache->getNumericId();
146
-	}
147
-
148
-	/**
149
-	 * get the stored metadata of a file or folder
150
-	 *
151
-	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
152
-	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
153
-	 */
154
-	public function get($file) {
155
-		$query = $this->getQueryBuilder();
156
-		$query->selectFileCache();
157
-
158
-		if (is_string($file) || $file == '') {
159
-			// normalize file
160
-			$file = $this->normalize($file);
161
-
162
-			$query->whereStorageId($this->getNumericStorageId())
163
-				->wherePath($file);
164
-		} else { //file id
165
-			$query->whereFileId($file);
166
-		}
167
-
168
-		$result = $query->execute();
169
-		$data = $result->fetch();
170
-		$result->closeCursor();
171
-
172
-		//merge partial data
173
-		if (!$data && is_string($file) && isset($this->partial[$file])) {
174
-			return $this->partial[$file];
175
-		} elseif (!$data) {
176
-			return $data;
177
-		} else {
178
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
179
-		}
180
-	}
181
-
182
-	/**
183
-	 * Create a CacheEntry from database row
184
-	 *
185
-	 * @param array $data
186
-	 * @param IMimeTypeLoader $mimetypeLoader
187
-	 * @return CacheEntry
188
-	 */
189
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
190
-		//fix types
191
-		$data['name'] = (string)$data['name'];
192
-		$data['path'] = (string)$data['path'];
193
-		$data['fileid'] = (int)$data['fileid'];
194
-		$data['parent'] = (int)$data['parent'];
195
-		$data['size'] = Util::numericToNumber($data['size']);
196
-		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
197
-		$data['mtime'] = (int)$data['mtime'];
198
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
199
-		$data['encryptedVersion'] = (int)$data['encrypted'];
200
-		$data['encrypted'] = (bool)$data['encrypted'];
201
-		$data['storage_id'] = $data['storage'];
202
-		$data['storage'] = (int)$data['storage'];
203
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
204
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
205
-		if ($data['storage_mtime'] == 0) {
206
-			$data['storage_mtime'] = $data['mtime'];
207
-		}
208
-		$data['permissions'] = (int)$data['permissions'];
209
-		if (isset($data['creation_time'])) {
210
-			$data['creation_time'] = (int)$data['creation_time'];
211
-		}
212
-		if (isset($data['upload_time'])) {
213
-			$data['upload_time'] = (int)$data['upload_time'];
214
-		}
215
-		return new CacheEntry($data);
216
-	}
217
-
218
-	/**
219
-	 * get the metadata of all files stored in $folder
220
-	 *
221
-	 * @param string $folder
222
-	 * @return ICacheEntry[]
223
-	 */
224
-	public function getFolderContents($folder) {
225
-		$fileId = $this->getId($folder);
226
-		return $this->getFolderContentsById($fileId);
227
-	}
228
-
229
-	/**
230
-	 * get the metadata of all files stored in $folder
231
-	 *
232
-	 * @param int $fileId the file id of the folder
233
-	 * @return ICacheEntry[]
234
-	 */
235
-	public function getFolderContentsById($fileId) {
236
-		if ($fileId > -1) {
237
-			$query = $this->getQueryBuilder();
238
-			$query->selectFileCache()
239
-				->whereParent($fileId)
240
-				->orderBy('name', 'ASC');
241
-
242
-			$result = $query->execute();
243
-			$files = $result->fetchAll();
244
-			$result->closeCursor();
245
-
246
-			return array_map(function (array $data) {
247
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
248
-			}, $files);
249
-		}
250
-		return [];
251
-	}
252
-
253
-	/**
254
-	 * insert or update meta data for a file or folder
255
-	 *
256
-	 * @param string $file
257
-	 * @param array $data
258
-	 *
259
-	 * @return int file id
260
-	 * @throws \RuntimeException
261
-	 */
262
-	public function put($file, array $data) {
263
-		if (($id = $this->getId($file)) > -1) {
264
-			$this->update($id, $data);
265
-			return $id;
266
-		} else {
267
-			return $this->insert($file, $data);
268
-		}
269
-	}
270
-
271
-	/**
272
-	 * insert meta data for a new file or folder
273
-	 *
274
-	 * @param string $file
275
-	 * @param array $data
276
-	 *
277
-	 * @return int file id
278
-	 * @throws \RuntimeException
279
-	 */
280
-	public function insert($file, array $data) {
281
-		// normalize file
282
-		$file = $this->normalize($file);
283
-
284
-		if (isset($this->partial[$file])) { //add any saved partial data
285
-			$data = array_merge($this->partial[$file], $data);
286
-			unset($this->partial[$file]);
287
-		}
288
-
289
-		$requiredFields = ['size', 'mtime', 'mimetype'];
290
-		foreach ($requiredFields as $field) {
291
-			if (!isset($data[$field])) { //data not complete save as partial and return
292
-				$this->partial[$file] = $data;
293
-				return -1;
294
-			}
295
-		}
296
-
297
-		$data['path'] = $file;
298
-		if (!isset($data['parent'])) {
299
-			$data['parent'] = $this->getParentId($file);
300
-		}
301
-		$data['name'] = basename($file);
302
-
303
-		[$values, $extensionValues] = $this->normalizeData($data);
304
-		$storageId = $this->getNumericStorageId();
305
-		$values['storage'] = $storageId;
306
-
307
-		try {
308
-			$builder = $this->connection->getQueryBuilder();
309
-			$builder->insert('filecache');
310
-
311
-			foreach ($values as $column => $value) {
312
-				$builder->setValue($column, $builder->createNamedParameter($value));
313
-			}
314
-
315
-			if ($builder->execute()) {
316
-				$fileId = $builder->getLastInsertId();
317
-
318
-				if (count($extensionValues)) {
319
-					$query = $this->getQueryBuilder();
320
-					$query->insert('filecache_extended');
321
-
322
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
323
-					foreach ($extensionValues as $column => $value) {
324
-						$query->setValue($column, $query->createNamedParameter($value));
325
-					}
326
-					$query->execute();
327
-				}
328
-
329
-				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
330
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
331
-				$this->eventDispatcher->dispatchTyped($event);
332
-				return $fileId;
333
-			}
334
-		} catch (UniqueConstraintViolationException $e) {
335
-			// entry exists already
336
-			if ($this->connection->inTransaction()) {
337
-				$this->connection->commit();
338
-				$this->connection->beginTransaction();
339
-			}
340
-		}
341
-
342
-		// The file was created in the mean time
343
-		if (($id = $this->getId($file)) > -1) {
344
-			$this->update($id, $data);
345
-			return $id;
346
-		} else {
347
-			throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
348
-		}
349
-	}
350
-
351
-	/**
352
-	 * update the metadata of an existing file or folder in the cache
353
-	 *
354
-	 * @param int $id the fileid of the existing file or folder
355
-	 * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
356
-	 */
357
-	public function update($id, array $data) {
358
-		if (isset($data['path'])) {
359
-			// normalize path
360
-			$data['path'] = $this->normalize($data['path']);
361
-		}
362
-
363
-		if (isset($data['name'])) {
364
-			// normalize path
365
-			$data['name'] = $this->normalize($data['name']);
366
-		}
367
-
368
-		[$values, $extensionValues] = $this->normalizeData($data);
369
-
370
-		if (count($values)) {
371
-			$query = $this->getQueryBuilder();
372
-
373
-			$query->update('filecache')
374
-				->whereFileId($id)
375
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
376
-					return $query->expr()->orX(
377
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
378
-						$query->expr()->isNull($key)
379
-					);
380
-				}, array_keys($values), array_values($values))));
381
-
382
-			foreach ($values as $key => $value) {
383
-				$query->set($key, $query->createNamedParameter($value));
384
-			}
385
-
386
-			$query->execute();
387
-		}
388
-
389
-		if (count($extensionValues)) {
390
-			try {
391
-				$query = $this->getQueryBuilder();
392
-				$query->insert('filecache_extended');
393
-
394
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
395
-				foreach ($extensionValues as $column => $value) {
396
-					$query->setValue($column, $query->createNamedParameter($value));
397
-				}
398
-
399
-				$query->execute();
400
-			} catch (UniqueConstraintViolationException $e) {
401
-				$query = $this->getQueryBuilder();
402
-				$query->update('filecache_extended')
403
-					->whereFileId($id)
404
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
405
-						return $query->expr()->orX(
406
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
407
-							$query->expr()->isNull($key)
408
-						);
409
-					}, array_keys($extensionValues), array_values($extensionValues))));
410
-
411
-				foreach ($extensionValues as $key => $value) {
412
-					$query->set($key, $query->createNamedParameter($value));
413
-				}
414
-
415
-				$query->execute();
416
-			}
417
-		}
418
-
419
-		$path = $this->getPathById($id);
420
-		// path can still be null if the file doesn't exist
421
-		if ($path !== null) {
422
-			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
423
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
424
-			$this->eventDispatcher->dispatchTyped($event);
425
-		}
426
-	}
427
-
428
-	/**
429
-	 * extract query parts and params array from data array
430
-	 *
431
-	 * @param array $data
432
-	 * @return array
433
-	 */
434
-	protected function normalizeData(array $data): array {
435
-		$fields = [
436
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
437
-			'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
438
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
439
-
440
-		$doNotCopyStorageMTime = false;
441
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
442
-			// this horrific magic tells it to not copy storage_mtime to mtime
443
-			unset($data['mtime']);
444
-			$doNotCopyStorageMTime = true;
445
-		}
446
-
447
-		$params = [];
448
-		$extensionParams = [];
449
-		foreach ($data as $name => $value) {
450
-			if (array_search($name, $fields) !== false) {
451
-				if ($name === 'path') {
452
-					$params['path_hash'] = md5($value);
453
-				} elseif ($name === 'mimetype') {
454
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
455
-					$value = $this->mimetypeLoader->getId($value);
456
-				} elseif ($name === 'storage_mtime') {
457
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
458
-						$params['mtime'] = $value;
459
-					}
460
-				} elseif ($name === 'encrypted') {
461
-					if (isset($data['encryptedVersion'])) {
462
-						$value = $data['encryptedVersion'];
463
-					} else {
464
-						// Boolean to integer conversion
465
-						$value = $value ? 1 : 0;
466
-					}
467
-				}
468
-				$params[$name] = $value;
469
-			}
470
-			if (array_search($name, $extensionFields) !== false) {
471
-				$extensionParams[$name] = $value;
472
-			}
473
-		}
474
-		return [$params, array_filter($extensionParams)];
475
-	}
476
-
477
-	/**
478
-	 * get the file id for a file
479
-	 *
480
-	 * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
481
-	 *
482
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
483
-	 *
484
-	 * @param string $file
485
-	 * @return int
486
-	 */
487
-	public function getId($file) {
488
-		// normalize file
489
-		$file = $this->normalize($file);
490
-
491
-		$query = $this->getQueryBuilder();
492
-		$query->select('fileid')
493
-			->from('filecache')
494
-			->whereStorageId($this->getNumericStorageId())
495
-			->wherePath($file);
496
-
497
-		$result = $query->execute();
498
-		$id = $result->fetchOne();
499
-		$result->closeCursor();
500
-
501
-		return $id === false ? -1 : (int)$id;
502
-	}
503
-
504
-	/**
505
-	 * get the id of the parent folder of a file
506
-	 *
507
-	 * @param string $file
508
-	 * @return int
509
-	 */
510
-	public function getParentId($file) {
511
-		if ($file === '') {
512
-			return -1;
513
-		} else {
514
-			$parent = $this->getParentPath($file);
515
-			return (int)$this->getId($parent);
516
-		}
517
-	}
518
-
519
-	private function getParentPath($path) {
520
-		$parent = dirname($path);
521
-		if ($parent === '.') {
522
-			$parent = '';
523
-		}
524
-		return $parent;
525
-	}
526
-
527
-	/**
528
-	 * check if a file is available in the cache
529
-	 *
530
-	 * @param string $file
531
-	 * @return bool
532
-	 */
533
-	public function inCache($file) {
534
-		return $this->getId($file) != -1;
535
-	}
536
-
537
-	/**
538
-	 * remove a file or folder from the cache
539
-	 *
540
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
541
-	 *
542
-	 * @param string $file
543
-	 */
544
-	public function remove($file) {
545
-		$entry = $this->get($file);
546
-
547
-		if ($entry instanceof ICacheEntry) {
548
-			$query = $this->getQueryBuilder();
549
-			$query->delete('filecache')
550
-				->whereFileId($entry->getId());
551
-			$query->execute();
552
-
553
-			$query = $this->getQueryBuilder();
554
-			$query->delete('filecache_extended')
555
-				->whereFileId($entry->getId());
556
-			$query->execute();
557
-
558
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
559
-				$this->removeChildren($entry);
560
-			}
561
-
562
-			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
563
-		}
564
-	}
565
-
566
-	/**
567
-	 * Get all sub folders of a folder
568
-	 *
569
-	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
570
-	 * @return ICacheEntry[] the cache entries for the subfolders
571
-	 */
572
-	private function getSubFolders(ICacheEntry $entry) {
573
-		$children = $this->getFolderContentsById($entry->getId());
574
-		return array_filter($children, function ($child) {
575
-			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
576
-		});
577
-	}
578
-
579
-	/**
580
-	 * Remove all children of a folder
581
-	 *
582
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
583
-	 * @throws \OC\DatabaseException
584
-	 */
585
-	private function removeChildren(ICacheEntry $entry) {
586
-		$parentIds = [$entry->getId()];
587
-		$queue = [$entry->getId()];
588
-		$deletedIds = [];
589
-		$deletedPaths = [];
590
-
591
-		// we walk depth first through the file tree, removing all filecache_extended attributes while we walk
592
-		// and collecting all folder ids to later use to delete the filecache entries
593
-		while ($entryId = array_pop($queue)) {
594
-			$children = $this->getFolderContentsById($entryId);
595
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
596
-				return $cacheEntry->getId();
597
-			}, $children);
598
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
599
-				return $cacheEntry->getPath();
600
-			}, $children);
601
-
602
-			$deletedIds = array_merge($deletedIds, $childIds);
603
-			$deletedPaths = array_merge($deletedPaths, $childPaths);
604
-
605
-			$query = $this->getQueryBuilder();
606
-			$query->delete('filecache_extended')
607
-				->where($query->expr()->in('fileid', $query->createParameter('childIds')));
608
-
609
-			foreach (array_chunk($childIds, 1000) as $childIdChunk) {
610
-				$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
611
-				$query->execute();
612
-			}
613
-
614
-			/** @var ICacheEntry[] $childFolders */
615
-			$childFolders = array_filter($children, function ($child) {
616
-				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
617
-			});
618
-			foreach ($childFolders as $folder) {
619
-				$parentIds[] = $folder->getId();
620
-				$queue[] = $folder->getId();
621
-			}
622
-		}
623
-
624
-		$query = $this->getQueryBuilder();
625
-		$query->delete('filecache')
626
-			->whereParentInParameter('parentIds');
627
-
628
-		foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
629
-			$query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
630
-			$query->execute();
631
-		}
632
-
633
-		foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
634
-			$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
635
-				$this->storage,
636
-				$filePath,
637
-				$fileId,
638
-				$this->getNumericStorageId()
639
-			);
640
-			$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
641
-		}
642
-	}
643
-
644
-	/**
645
-	 * Move a file or folder in the cache
646
-	 *
647
-	 * @param string $source
648
-	 * @param string $target
649
-	 */
650
-	public function move($source, $target) {
651
-		$this->moveFromCache($this, $source, $target);
652
-	}
653
-
654
-	/**
655
-	 * Get the storage id and path needed for a move
656
-	 *
657
-	 * @param string $path
658
-	 * @return array [$storageId, $internalPath]
659
-	 */
660
-	protected function getMoveInfo($path) {
661
-		return [$this->getNumericStorageId(), $path];
662
-	}
663
-
664
-	protected function hasEncryptionWrapper(): bool {
665
-		return $this->storage->instanceOfStorage(Encryption::class);
666
-	}
667
-
668
-	/**
669
-	 * Move a file or folder in the cache
670
-	 *
671
-	 * @param ICache $sourceCache
672
-	 * @param string $sourcePath
673
-	 * @param string $targetPath
674
-	 * @throws \OC\DatabaseException
675
-	 * @throws \Exception if the given storages have an invalid id
676
-	 */
677
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
678
-		if ($sourceCache instanceof Cache) {
679
-			// normalize source and target
680
-			$sourcePath = $this->normalize($sourcePath);
681
-			$targetPath = $this->normalize($targetPath);
682
-
683
-			$sourceData = $sourceCache->get($sourcePath);
684
-			if ($sourceData === false) {
685
-				throw new \Exception('Invalid source storage path: ' . $sourcePath);
686
-			}
687
-
688
-			$sourceId = $sourceData['fileid'];
689
-			$newParentId = $this->getParentId($targetPath);
690
-
691
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
692
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
693
-
694
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
695
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
696
-			}
697
-			if (is_null($targetStorageId) || $targetStorageId === false) {
698
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
699
-			}
700
-
701
-			$this->connection->beginTransaction();
702
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
703
-				//update all child entries
704
-				$sourceLength = mb_strlen($sourcePath);
705
-				$query = $this->connection->getQueryBuilder();
706
-
707
-				$fun = $query->func();
708
-				$newPathFunction = $fun->concat(
709
-					$query->createNamedParameter($targetPath),
710
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
711
-				);
712
-				$query->update('filecache')
713
-					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
714
-					->set('path_hash', $fun->md5($newPathFunction))
715
-					->set('path', $newPathFunction)
716
-					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
717
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
718
-
719
-				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
720
-				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
721
-					$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
722
-				}
723
-
724
-				try {
725
-					$query->execute();
726
-				} catch (\OC\DatabaseException $e) {
727
-					$this->connection->rollBack();
728
-					throw $e;
729
-				}
730
-			}
731
-
732
-			$query = $this->getQueryBuilder();
733
-			$query->update('filecache')
734
-				->set('storage', $query->createNamedParameter($targetStorageId))
735
-				->set('path', $query->createNamedParameter($targetPath))
736
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
737
-				->set('name', $query->createNamedParameter(basename($targetPath)))
738
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
739
-				->whereFileId($sourceId);
740
-
741
-			// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
742
-			if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
743
-				$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
744
-			}
745
-
746
-			$query->execute();
747
-
748
-			$this->connection->commit();
749
-
750
-			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
751
-				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
752
-				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
753
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
754
-				$this->eventDispatcher->dispatchTyped($event);
755
-			} else {
756
-				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
757
-				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
758
-				$this->eventDispatcher->dispatchTyped($event);
759
-			}
760
-		} else {
761
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
762
-		}
763
-	}
764
-
765
-	/**
766
-	 * remove all entries for files that are stored on the storage from the cache
767
-	 */
768
-	public function clear() {
769
-		$query = $this->getQueryBuilder();
770
-		$query->delete('filecache')
771
-			->whereStorageId($this->getNumericStorageId());
772
-		$query->execute();
773
-
774
-		$query = $this->connection->getQueryBuilder();
775
-		$query->delete('storages')
776
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
777
-		$query->execute();
778
-	}
779
-
780
-	/**
781
-	 * Get the scan status of a file
782
-	 *
783
-	 * - Cache::NOT_FOUND: File is not in the cache
784
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
785
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
786
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
787
-	 *
788
-	 * @param string $file
789
-	 *
790
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
791
-	 */
792
-	public function getStatus($file) {
793
-		// normalize file
794
-		$file = $this->normalize($file);
795
-
796
-		$query = $this->getQueryBuilder();
797
-		$query->select('size')
798
-			->from('filecache')
799
-			->whereStorageId($this->getNumericStorageId())
800
-			->wherePath($file);
801
-
802
-		$result = $query->execute();
803
-		$size = $result->fetchOne();
804
-		$result->closeCursor();
805
-
806
-		if ($size !== false) {
807
-			if ((int)$size === -1) {
808
-				return self::SHALLOW;
809
-			} else {
810
-				return self::COMPLETE;
811
-			}
812
-		} else {
813
-			if (isset($this->partial[$file])) {
814
-				return self::PARTIAL;
815
-			} else {
816
-				return self::NOT_FOUND;
817
-			}
818
-		}
819
-	}
820
-
821
-	/**
822
-	 * search for files matching $pattern
823
-	 *
824
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
825
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
826
-	 */
827
-	public function search($pattern) {
828
-		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
829
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
830
-	}
831
-
832
-	/**
833
-	 * search for files by mimetype
834
-	 *
835
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
836
-	 *        where it will search for all mimetypes in the group ('image/*')
837
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
838
-	 */
839
-	public function searchByMime($mimetype) {
840
-		if (!str_contains($mimetype, '/')) {
841
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
842
-		} else {
843
-			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
844
-		}
845
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
846
-	}
847
-
848
-	public function searchQuery(ISearchQuery $searchQuery) {
849
-		return current($this->querySearchHelper->searchInCaches($searchQuery, [$this]));
850
-	}
851
-
852
-	/**
853
-	 * Re-calculate the folder size and the size of all parent folders
854
-	 *
855
-	 * @param string|boolean $path
856
-	 * @param array $data (optional) meta data of the folder
857
-	 */
858
-	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
859
-		$this->calculateFolderSize($path, $data);
860
-		if ($path !== '') {
861
-			$parent = dirname($path);
862
-			if ($parent === '.' || $parent === '/') {
863
-				$parent = '';
864
-			}
865
-			if ($isBackgroundScan) {
866
-				$parentData = $this->get($parent);
867
-				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
868
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
869
-				}
870
-			} else {
871
-				$this->correctFolderSize($parent);
872
-			}
873
-		}
874
-	}
875
-
876
-	/**
877
-	 * get the incomplete count that shares parent $folder
878
-	 *
879
-	 * @param int $fileId the file id of the folder
880
-	 * @return int
881
-	 */
882
-	public function getIncompleteChildrenCount($fileId) {
883
-		if ($fileId > -1) {
884
-			$query = $this->getQueryBuilder();
885
-			$query->select($query->func()->count())
886
-				->from('filecache')
887
-				->whereParent($fileId)
888
-				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
889
-
890
-			$result = $query->execute();
891
-			$size = (int)$result->fetchOne();
892
-			$result->closeCursor();
893
-
894
-			return $size;
895
-		}
896
-		return -1;
897
-	}
898
-
899
-	/**
900
-	 * calculate the size of a folder and set it in the cache
901
-	 *
902
-	 * @param string $path
903
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
904
-	 * @return int|float
905
-	 */
906
-	public function calculateFolderSize($path, $entry = null) {
907
-		return $this->calculateFolderSizeInner($path, $entry);
908
-	}
909
-
910
-
911
-	/**
912
-	 * inner function because we can't add new params to the public function without breaking any child classes
913
-	 *
914
-	 * @param string $path
915
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
916
-	 * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
917
-	 * @return int|float
918
-	 */
919
-	protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
920
-		$totalSize = 0;
921
-		if (is_null($entry) || !isset($entry['fileid'])) {
922
-			$entry = $this->get($path);
923
-		}
924
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
925
-			$id = $entry['fileid'];
926
-
927
-			$query = $this->getQueryBuilder();
928
-			$query->select('size', 'unencrypted_size')
929
-				->from('filecache')
930
-				->whereParent($id);
931
-			if ($ignoreUnknown) {
932
-				$query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
933
-			}
934
-
935
-			$result = $query->execute();
936
-			$rows = $result->fetchAll();
937
-			$result->closeCursor();
938
-
939
-			if ($rows) {
940
-				$sizes = array_map(function (array $row) {
941
-					return Util::numericToNumber($row['size']);
942
-				}, $rows);
943
-				$unencryptedOnlySizes = array_map(function (array $row) {
944
-					return Util::numericToNumber($row['unencrypted_size']);
945
-				}, $rows);
946
-				$unencryptedSizes = array_map(function (array $row) {
947
-					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
948
-				}, $rows);
949
-
950
-				$sum = array_sum($sizes);
951
-				$min = min($sizes);
952
-
953
-				$unencryptedSum = array_sum($unencryptedSizes);
954
-				$unencryptedMin = min($unencryptedSizes);
955
-				$unencryptedMax = max($unencryptedOnlySizes);
956
-
957
-				$sum = 0 + $sum;
958
-				$min = 0 + $min;
959
-				if ($min === -1) {
960
-					$totalSize = $min;
961
-				} else {
962
-					$totalSize = $sum;
963
-				}
964
-				if ($unencryptedMin === -1 || $min === -1) {
965
-					$unencryptedTotal = $unencryptedMin;
966
-				} else {
967
-					$unencryptedTotal = $unencryptedSum;
968
-				}
969
-			} else {
970
-				$totalSize = 0;
971
-				$unencryptedTotal = 0;
972
-				$unencryptedMax = 0;
973
-			}
974
-
975
-			// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
976
-			$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || $entry['unencrypted_size'] > 0;
977
-			if ($entry['size'] !== $totalSize || ($entry['unencrypted_size'] !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
978
-				if ($shouldWriteUnEncryptedSize) {
979
-					// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
980
-					if ($unencryptedMax === 0) {
981
-						$unencryptedTotal = 0;
982
-					}
983
-
984
-					$this->update($id, [
985
-						'size' => $totalSize,
986
-						'unencrypted_size' => $unencryptedTotal,
987
-					]);
988
-				} else {
989
-					$this->update($id, [
990
-						'size' => $totalSize,
991
-					]);
992
-				}
993
-			}
994
-		}
995
-		return $totalSize;
996
-	}
997
-
998
-	/**
999
-	 * get all file ids on the files on the storage
1000
-	 *
1001
-	 * @return int[]
1002
-	 */
1003
-	public function getAll() {
1004
-		$query = $this->getQueryBuilder();
1005
-		$query->select('fileid')
1006
-			->from('filecache')
1007
-			->whereStorageId($this->getNumericStorageId());
1008
-
1009
-		$result = $query->execute();
1010
-		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1011
-		$result->closeCursor();
1012
-
1013
-		return array_map(function ($id) {
1014
-			return (int)$id;
1015
-		}, $files);
1016
-	}
1017
-
1018
-	/**
1019
-	 * find a folder in the cache which has not been fully scanned
1020
-	 *
1021
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1022
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
1023
-	 * likely the folder where we stopped scanning previously
1024
-	 *
1025
-	 * @return string|false the path of the folder or false when no folder matched
1026
-	 */
1027
-	public function getIncomplete() {
1028
-		$query = $this->getQueryBuilder();
1029
-		$query->select('path')
1030
-			->from('filecache')
1031
-			->whereStorageId($this->getNumericStorageId())
1032
-			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1033
-			->orderBy('fileid', 'DESC')
1034
-			->setMaxResults(1);
1035
-
1036
-		$result = $query->execute();
1037
-		$path = $result->fetchOne();
1038
-		$result->closeCursor();
1039
-
1040
-		if ($path === false) {
1041
-			return false;
1042
-		}
1043
-
1044
-		// Make sure Oracle does not continue with null for empty strings
1045
-		return (string)$path;
1046
-	}
1047
-
1048
-	/**
1049
-	 * get the path of a file on this storage by it's file id
1050
-	 *
1051
-	 * @param int $id the file id of the file or folder to search
1052
-	 * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1053
-	 */
1054
-	public function getPathById($id) {
1055
-		$query = $this->getQueryBuilder();
1056
-		$query->select('path')
1057
-			->from('filecache')
1058
-			->whereStorageId($this->getNumericStorageId())
1059
-			->whereFileId($id);
1060
-
1061
-		$result = $query->execute();
1062
-		$path = $result->fetchOne();
1063
-		$result->closeCursor();
1064
-
1065
-		if ($path === false) {
1066
-			return null;
1067
-		}
1068
-
1069
-		return (string)$path;
1070
-	}
1071
-
1072
-	/**
1073
-	 * get the storage id of the storage for a file and the internal path of the file
1074
-	 * unlike getPathById this does not limit the search to files on this storage and
1075
-	 * instead does a global search in the cache table
1076
-	 *
1077
-	 * @param int $id
1078
-	 * @return array first element holding the storage id, second the path
1079
-	 * @deprecated use getPathById() instead
1080
-	 */
1081
-	public static function getById($id) {
1082
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1083
-		$query->select('path', 'storage')
1084
-			->from('filecache')
1085
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1086
-
1087
-		$result = $query->execute();
1088
-		$row = $result->fetch();
1089
-		$result->closeCursor();
1090
-
1091
-		if ($row) {
1092
-			$numericId = $row['storage'];
1093
-			$path = $row['path'];
1094
-		} else {
1095
-			return null;
1096
-		}
1097
-
1098
-		if ($id = Storage::getStorageId($numericId)) {
1099
-			return [$id, $path];
1100
-		} else {
1101
-			return null;
1102
-		}
1103
-	}
1104
-
1105
-	/**
1106
-	 * normalize the given path
1107
-	 *
1108
-	 * @param string $path
1109
-	 * @return string
1110
-	 */
1111
-	public function normalize($path) {
1112
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1113
-	}
1114
-
1115
-	/**
1116
-	 * Copy a file or folder in the cache
1117
-	 *
1118
-	 * @param ICache $sourceCache
1119
-	 * @param ICacheEntry $sourceEntry
1120
-	 * @param string $targetPath
1121
-	 * @return int fileId of copied entry
1122
-	 */
1123
-	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1124
-		if ($sourceEntry->getId() < 0) {
1125
-			throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1126
-		}
1127
-		$data = $this->cacheEntryToArray($sourceEntry);
1128
-
1129
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1130
-		if ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
1131
-			$data['encrypted'] = 0;
1132
-		}
1133
-
1134
-		$fileId = $this->put($targetPath, $data);
1135
-		if ($fileId <= 0) {
1136
-			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1137
-		}
1138
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1139
-			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1140
-			foreach ($folderContent as $subEntry) {
1141
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1142
-				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1143
-			}
1144
-		}
1145
-		return $fileId;
1146
-	}
1147
-
1148
-	private function cacheEntryToArray(ICacheEntry $entry): array {
1149
-		return [
1150
-			'size' => $entry->getSize(),
1151
-			'mtime' => $entry->getMTime(),
1152
-			'storage_mtime' => $entry->getStorageMTime(),
1153
-			'mimetype' => $entry->getMimeType(),
1154
-			'mimepart' => $entry->getMimePart(),
1155
-			'etag' => $entry->getEtag(),
1156
-			'permissions' => $entry->getPermissions(),
1157
-			'encrypted' => $entry->isEncrypted(),
1158
-			'creation_time' => $entry->getCreationTime(),
1159
-			'upload_time' => $entry->getUploadTime(),
1160
-			'metadata_etag' => $entry->getMetadataEtag(),
1161
-		];
1162
-	}
1163
-
1164
-	public function getQueryFilterForStorage(): ISearchOperator {
1165
-		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1166
-	}
1167
-
1168
-	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1169
-		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1170
-			return $rawEntry;
1171
-		} else {
1172
-			return null;
1173
-		}
1174
-	}
77
+    use MoveFromCacheTrait {
78
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
79
+    }
80
+
81
+    /**
82
+     * @var array partial data for the cache
83
+     */
84
+    protected $partial = [];
85
+
86
+    /**
87
+     * @var string
88
+     */
89
+    protected $storageId;
90
+
91
+    private $storage;
92
+
93
+    /**
94
+     * @var Storage $storageCache
95
+     */
96
+    protected $storageCache;
97
+
98
+    /** @var IMimeTypeLoader */
99
+    protected $mimetypeLoader;
100
+
101
+    /**
102
+     * @var IDBConnection
103
+     */
104
+    protected $connection;
105
+
106
+    /**
107
+     * @var IEventDispatcher
108
+     */
109
+    protected $eventDispatcher;
110
+
111
+    /** @var QuerySearchHelper */
112
+    protected $querySearchHelper;
113
+
114
+    /**
115
+     * @param IStorage $storage
116
+     */
117
+    public function __construct(IStorage $storage) {
118
+        $this->storageId = $storage->getId();
119
+        $this->storage = $storage;
120
+        if (strlen($this->storageId) > 64) {
121
+            $this->storageId = md5($this->storageId);
122
+        }
123
+
124
+        $this->storageCache = new Storage($storage);
125
+        $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
126
+        $this->connection = \OC::$server->getDatabaseConnection();
127
+        $this->eventDispatcher = \OC::$server->get(IEventDispatcher::class);
128
+        $this->querySearchHelper = \OC::$server->query(QuerySearchHelper::class);
129
+    }
130
+
131
+    protected function getQueryBuilder() {
132
+        return new CacheQueryBuilder(
133
+            $this->connection,
134
+            \OC::$server->getSystemConfig(),
135
+            \OC::$server->get(LoggerInterface::class)
136
+        );
137
+    }
138
+
139
+    /**
140
+     * Get the numeric storage id for this cache's storage
141
+     *
142
+     * @return int
143
+     */
144
+    public function getNumericStorageId() {
145
+        return $this->storageCache->getNumericId();
146
+    }
147
+
148
+    /**
149
+     * get the stored metadata of a file or folder
150
+     *
151
+     * @param string | int $file either the path of a file or folder or the file id for a file or folder
152
+     * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
153
+     */
154
+    public function get($file) {
155
+        $query = $this->getQueryBuilder();
156
+        $query->selectFileCache();
157
+
158
+        if (is_string($file) || $file == '') {
159
+            // normalize file
160
+            $file = $this->normalize($file);
161
+
162
+            $query->whereStorageId($this->getNumericStorageId())
163
+                ->wherePath($file);
164
+        } else { //file id
165
+            $query->whereFileId($file);
166
+        }
167
+
168
+        $result = $query->execute();
169
+        $data = $result->fetch();
170
+        $result->closeCursor();
171
+
172
+        //merge partial data
173
+        if (!$data && is_string($file) && isset($this->partial[$file])) {
174
+            return $this->partial[$file];
175
+        } elseif (!$data) {
176
+            return $data;
177
+        } else {
178
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
179
+        }
180
+    }
181
+
182
+    /**
183
+     * Create a CacheEntry from database row
184
+     *
185
+     * @param array $data
186
+     * @param IMimeTypeLoader $mimetypeLoader
187
+     * @return CacheEntry
188
+     */
189
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
190
+        //fix types
191
+        $data['name'] = (string)$data['name'];
192
+        $data['path'] = (string)$data['path'];
193
+        $data['fileid'] = (int)$data['fileid'];
194
+        $data['parent'] = (int)$data['parent'];
195
+        $data['size'] = Util::numericToNumber($data['size']);
196
+        $data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
197
+        $data['mtime'] = (int)$data['mtime'];
198
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
199
+        $data['encryptedVersion'] = (int)$data['encrypted'];
200
+        $data['encrypted'] = (bool)$data['encrypted'];
201
+        $data['storage_id'] = $data['storage'];
202
+        $data['storage'] = (int)$data['storage'];
203
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
204
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
205
+        if ($data['storage_mtime'] == 0) {
206
+            $data['storage_mtime'] = $data['mtime'];
207
+        }
208
+        $data['permissions'] = (int)$data['permissions'];
209
+        if (isset($data['creation_time'])) {
210
+            $data['creation_time'] = (int)$data['creation_time'];
211
+        }
212
+        if (isset($data['upload_time'])) {
213
+            $data['upload_time'] = (int)$data['upload_time'];
214
+        }
215
+        return new CacheEntry($data);
216
+    }
217
+
218
+    /**
219
+     * get the metadata of all files stored in $folder
220
+     *
221
+     * @param string $folder
222
+     * @return ICacheEntry[]
223
+     */
224
+    public function getFolderContents($folder) {
225
+        $fileId = $this->getId($folder);
226
+        return $this->getFolderContentsById($fileId);
227
+    }
228
+
229
+    /**
230
+     * get the metadata of all files stored in $folder
231
+     *
232
+     * @param int $fileId the file id of the folder
233
+     * @return ICacheEntry[]
234
+     */
235
+    public function getFolderContentsById($fileId) {
236
+        if ($fileId > -1) {
237
+            $query = $this->getQueryBuilder();
238
+            $query->selectFileCache()
239
+                ->whereParent($fileId)
240
+                ->orderBy('name', 'ASC');
241
+
242
+            $result = $query->execute();
243
+            $files = $result->fetchAll();
244
+            $result->closeCursor();
245
+
246
+            return array_map(function (array $data) {
247
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
248
+            }, $files);
249
+        }
250
+        return [];
251
+    }
252
+
253
+    /**
254
+     * insert or update meta data for a file or folder
255
+     *
256
+     * @param string $file
257
+     * @param array $data
258
+     *
259
+     * @return int file id
260
+     * @throws \RuntimeException
261
+     */
262
+    public function put($file, array $data) {
263
+        if (($id = $this->getId($file)) > -1) {
264
+            $this->update($id, $data);
265
+            return $id;
266
+        } else {
267
+            return $this->insert($file, $data);
268
+        }
269
+    }
270
+
271
+    /**
272
+     * insert meta data for a new file or folder
273
+     *
274
+     * @param string $file
275
+     * @param array $data
276
+     *
277
+     * @return int file id
278
+     * @throws \RuntimeException
279
+     */
280
+    public function insert($file, array $data) {
281
+        // normalize file
282
+        $file = $this->normalize($file);
283
+
284
+        if (isset($this->partial[$file])) { //add any saved partial data
285
+            $data = array_merge($this->partial[$file], $data);
286
+            unset($this->partial[$file]);
287
+        }
288
+
289
+        $requiredFields = ['size', 'mtime', 'mimetype'];
290
+        foreach ($requiredFields as $field) {
291
+            if (!isset($data[$field])) { //data not complete save as partial and return
292
+                $this->partial[$file] = $data;
293
+                return -1;
294
+            }
295
+        }
296
+
297
+        $data['path'] = $file;
298
+        if (!isset($data['parent'])) {
299
+            $data['parent'] = $this->getParentId($file);
300
+        }
301
+        $data['name'] = basename($file);
302
+
303
+        [$values, $extensionValues] = $this->normalizeData($data);
304
+        $storageId = $this->getNumericStorageId();
305
+        $values['storage'] = $storageId;
306
+
307
+        try {
308
+            $builder = $this->connection->getQueryBuilder();
309
+            $builder->insert('filecache');
310
+
311
+            foreach ($values as $column => $value) {
312
+                $builder->setValue($column, $builder->createNamedParameter($value));
313
+            }
314
+
315
+            if ($builder->execute()) {
316
+                $fileId = $builder->getLastInsertId();
317
+
318
+                if (count($extensionValues)) {
319
+                    $query = $this->getQueryBuilder();
320
+                    $query->insert('filecache_extended');
321
+
322
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
323
+                    foreach ($extensionValues as $column => $value) {
324
+                        $query->setValue($column, $query->createNamedParameter($value));
325
+                    }
326
+                    $query->execute();
327
+                }
328
+
329
+                $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
330
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
331
+                $this->eventDispatcher->dispatchTyped($event);
332
+                return $fileId;
333
+            }
334
+        } catch (UniqueConstraintViolationException $e) {
335
+            // entry exists already
336
+            if ($this->connection->inTransaction()) {
337
+                $this->connection->commit();
338
+                $this->connection->beginTransaction();
339
+            }
340
+        }
341
+
342
+        // The file was created in the mean time
343
+        if (($id = $this->getId($file)) > -1) {
344
+            $this->update($id, $data);
345
+            return $id;
346
+        } else {
347
+            throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
348
+        }
349
+    }
350
+
351
+    /**
352
+     * update the metadata of an existing file or folder in the cache
353
+     *
354
+     * @param int $id the fileid of the existing file or folder
355
+     * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
356
+     */
357
+    public function update($id, array $data) {
358
+        if (isset($data['path'])) {
359
+            // normalize path
360
+            $data['path'] = $this->normalize($data['path']);
361
+        }
362
+
363
+        if (isset($data['name'])) {
364
+            // normalize path
365
+            $data['name'] = $this->normalize($data['name']);
366
+        }
367
+
368
+        [$values, $extensionValues] = $this->normalizeData($data);
369
+
370
+        if (count($values)) {
371
+            $query = $this->getQueryBuilder();
372
+
373
+            $query->update('filecache')
374
+                ->whereFileId($id)
375
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
376
+                    return $query->expr()->orX(
377
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
378
+                        $query->expr()->isNull($key)
379
+                    );
380
+                }, array_keys($values), array_values($values))));
381
+
382
+            foreach ($values as $key => $value) {
383
+                $query->set($key, $query->createNamedParameter($value));
384
+            }
385
+
386
+            $query->execute();
387
+        }
388
+
389
+        if (count($extensionValues)) {
390
+            try {
391
+                $query = $this->getQueryBuilder();
392
+                $query->insert('filecache_extended');
393
+
394
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
395
+                foreach ($extensionValues as $column => $value) {
396
+                    $query->setValue($column, $query->createNamedParameter($value));
397
+                }
398
+
399
+                $query->execute();
400
+            } catch (UniqueConstraintViolationException $e) {
401
+                $query = $this->getQueryBuilder();
402
+                $query->update('filecache_extended')
403
+                    ->whereFileId($id)
404
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
405
+                        return $query->expr()->orX(
406
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
407
+                            $query->expr()->isNull($key)
408
+                        );
409
+                    }, array_keys($extensionValues), array_values($extensionValues))));
410
+
411
+                foreach ($extensionValues as $key => $value) {
412
+                    $query->set($key, $query->createNamedParameter($value));
413
+                }
414
+
415
+                $query->execute();
416
+            }
417
+        }
418
+
419
+        $path = $this->getPathById($id);
420
+        // path can still be null if the file doesn't exist
421
+        if ($path !== null) {
422
+            $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
423
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
424
+            $this->eventDispatcher->dispatchTyped($event);
425
+        }
426
+    }
427
+
428
+    /**
429
+     * extract query parts and params array from data array
430
+     *
431
+     * @param array $data
432
+     * @return array
433
+     */
434
+    protected function normalizeData(array $data): array {
435
+        $fields = [
436
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
437
+            'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
438
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
439
+
440
+        $doNotCopyStorageMTime = false;
441
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
442
+            // this horrific magic tells it to not copy storage_mtime to mtime
443
+            unset($data['mtime']);
444
+            $doNotCopyStorageMTime = true;
445
+        }
446
+
447
+        $params = [];
448
+        $extensionParams = [];
449
+        foreach ($data as $name => $value) {
450
+            if (array_search($name, $fields) !== false) {
451
+                if ($name === 'path') {
452
+                    $params['path_hash'] = md5($value);
453
+                } elseif ($name === 'mimetype') {
454
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
455
+                    $value = $this->mimetypeLoader->getId($value);
456
+                } elseif ($name === 'storage_mtime') {
457
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
458
+                        $params['mtime'] = $value;
459
+                    }
460
+                } elseif ($name === 'encrypted') {
461
+                    if (isset($data['encryptedVersion'])) {
462
+                        $value = $data['encryptedVersion'];
463
+                    } else {
464
+                        // Boolean to integer conversion
465
+                        $value = $value ? 1 : 0;
466
+                    }
467
+                }
468
+                $params[$name] = $value;
469
+            }
470
+            if (array_search($name, $extensionFields) !== false) {
471
+                $extensionParams[$name] = $value;
472
+            }
473
+        }
474
+        return [$params, array_filter($extensionParams)];
475
+    }
476
+
477
+    /**
478
+     * get the file id for a file
479
+     *
480
+     * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
481
+     *
482
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
483
+     *
484
+     * @param string $file
485
+     * @return int
486
+     */
487
+    public function getId($file) {
488
+        // normalize file
489
+        $file = $this->normalize($file);
490
+
491
+        $query = $this->getQueryBuilder();
492
+        $query->select('fileid')
493
+            ->from('filecache')
494
+            ->whereStorageId($this->getNumericStorageId())
495
+            ->wherePath($file);
496
+
497
+        $result = $query->execute();
498
+        $id = $result->fetchOne();
499
+        $result->closeCursor();
500
+
501
+        return $id === false ? -1 : (int)$id;
502
+    }
503
+
504
+    /**
505
+     * get the id of the parent folder of a file
506
+     *
507
+     * @param string $file
508
+     * @return int
509
+     */
510
+    public function getParentId($file) {
511
+        if ($file === '') {
512
+            return -1;
513
+        } else {
514
+            $parent = $this->getParentPath($file);
515
+            return (int)$this->getId($parent);
516
+        }
517
+    }
518
+
519
+    private function getParentPath($path) {
520
+        $parent = dirname($path);
521
+        if ($parent === '.') {
522
+            $parent = '';
523
+        }
524
+        return $parent;
525
+    }
526
+
527
+    /**
528
+     * check if a file is available in the cache
529
+     *
530
+     * @param string $file
531
+     * @return bool
532
+     */
533
+    public function inCache($file) {
534
+        return $this->getId($file) != -1;
535
+    }
536
+
537
+    /**
538
+     * remove a file or folder from the cache
539
+     *
540
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
541
+     *
542
+     * @param string $file
543
+     */
544
+    public function remove($file) {
545
+        $entry = $this->get($file);
546
+
547
+        if ($entry instanceof ICacheEntry) {
548
+            $query = $this->getQueryBuilder();
549
+            $query->delete('filecache')
550
+                ->whereFileId($entry->getId());
551
+            $query->execute();
552
+
553
+            $query = $this->getQueryBuilder();
554
+            $query->delete('filecache_extended')
555
+                ->whereFileId($entry->getId());
556
+            $query->execute();
557
+
558
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
559
+                $this->removeChildren($entry);
560
+            }
561
+
562
+            $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
563
+        }
564
+    }
565
+
566
+    /**
567
+     * Get all sub folders of a folder
568
+     *
569
+     * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
570
+     * @return ICacheEntry[] the cache entries for the subfolders
571
+     */
572
+    private function getSubFolders(ICacheEntry $entry) {
573
+        $children = $this->getFolderContentsById($entry->getId());
574
+        return array_filter($children, function ($child) {
575
+            return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
576
+        });
577
+    }
578
+
579
+    /**
580
+     * Remove all children of a folder
581
+     *
582
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
583
+     * @throws \OC\DatabaseException
584
+     */
585
+    private function removeChildren(ICacheEntry $entry) {
586
+        $parentIds = [$entry->getId()];
587
+        $queue = [$entry->getId()];
588
+        $deletedIds = [];
589
+        $deletedPaths = [];
590
+
591
+        // we walk depth first through the file tree, removing all filecache_extended attributes while we walk
592
+        // and collecting all folder ids to later use to delete the filecache entries
593
+        while ($entryId = array_pop($queue)) {
594
+            $children = $this->getFolderContentsById($entryId);
595
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
596
+                return $cacheEntry->getId();
597
+            }, $children);
598
+            $childPaths = array_map(function (ICacheEntry $cacheEntry) {
599
+                return $cacheEntry->getPath();
600
+            }, $children);
601
+
602
+            $deletedIds = array_merge($deletedIds, $childIds);
603
+            $deletedPaths = array_merge($deletedPaths, $childPaths);
604
+
605
+            $query = $this->getQueryBuilder();
606
+            $query->delete('filecache_extended')
607
+                ->where($query->expr()->in('fileid', $query->createParameter('childIds')));
608
+
609
+            foreach (array_chunk($childIds, 1000) as $childIdChunk) {
610
+                $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
611
+                $query->execute();
612
+            }
613
+
614
+            /** @var ICacheEntry[] $childFolders */
615
+            $childFolders = array_filter($children, function ($child) {
616
+                return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
617
+            });
618
+            foreach ($childFolders as $folder) {
619
+                $parentIds[] = $folder->getId();
620
+                $queue[] = $folder->getId();
621
+            }
622
+        }
623
+
624
+        $query = $this->getQueryBuilder();
625
+        $query->delete('filecache')
626
+            ->whereParentInParameter('parentIds');
627
+
628
+        foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
629
+            $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
630
+            $query->execute();
631
+        }
632
+
633
+        foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
634
+            $cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
635
+                $this->storage,
636
+                $filePath,
637
+                $fileId,
638
+                $this->getNumericStorageId()
639
+            );
640
+            $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
641
+        }
642
+    }
643
+
644
+    /**
645
+     * Move a file or folder in the cache
646
+     *
647
+     * @param string $source
648
+     * @param string $target
649
+     */
650
+    public function move($source, $target) {
651
+        $this->moveFromCache($this, $source, $target);
652
+    }
653
+
654
+    /**
655
+     * Get the storage id and path needed for a move
656
+     *
657
+     * @param string $path
658
+     * @return array [$storageId, $internalPath]
659
+     */
660
+    protected function getMoveInfo($path) {
661
+        return [$this->getNumericStorageId(), $path];
662
+    }
663
+
664
+    protected function hasEncryptionWrapper(): bool {
665
+        return $this->storage->instanceOfStorage(Encryption::class);
666
+    }
667
+
668
+    /**
669
+     * Move a file or folder in the cache
670
+     *
671
+     * @param ICache $sourceCache
672
+     * @param string $sourcePath
673
+     * @param string $targetPath
674
+     * @throws \OC\DatabaseException
675
+     * @throws \Exception if the given storages have an invalid id
676
+     */
677
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
678
+        if ($sourceCache instanceof Cache) {
679
+            // normalize source and target
680
+            $sourcePath = $this->normalize($sourcePath);
681
+            $targetPath = $this->normalize($targetPath);
682
+
683
+            $sourceData = $sourceCache->get($sourcePath);
684
+            if ($sourceData === false) {
685
+                throw new \Exception('Invalid source storage path: ' . $sourcePath);
686
+            }
687
+
688
+            $sourceId = $sourceData['fileid'];
689
+            $newParentId = $this->getParentId($targetPath);
690
+
691
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
692
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
693
+
694
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
695
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
696
+            }
697
+            if (is_null($targetStorageId) || $targetStorageId === false) {
698
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
699
+            }
700
+
701
+            $this->connection->beginTransaction();
702
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
703
+                //update all child entries
704
+                $sourceLength = mb_strlen($sourcePath);
705
+                $query = $this->connection->getQueryBuilder();
706
+
707
+                $fun = $query->func();
708
+                $newPathFunction = $fun->concat(
709
+                    $query->createNamedParameter($targetPath),
710
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
711
+                );
712
+                $query->update('filecache')
713
+                    ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
714
+                    ->set('path_hash', $fun->md5($newPathFunction))
715
+                    ->set('path', $newPathFunction)
716
+                    ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
717
+                    ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
718
+
719
+                // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
720
+                if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
721
+                    $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
722
+                }
723
+
724
+                try {
725
+                    $query->execute();
726
+                } catch (\OC\DatabaseException $e) {
727
+                    $this->connection->rollBack();
728
+                    throw $e;
729
+                }
730
+            }
731
+
732
+            $query = $this->getQueryBuilder();
733
+            $query->update('filecache')
734
+                ->set('storage', $query->createNamedParameter($targetStorageId))
735
+                ->set('path', $query->createNamedParameter($targetPath))
736
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
737
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
738
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
739
+                ->whereFileId($sourceId);
740
+
741
+            // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
742
+            if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
743
+                $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
744
+            }
745
+
746
+            $query->execute();
747
+
748
+            $this->connection->commit();
749
+
750
+            if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
751
+                $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
752
+                $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
753
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
754
+                $this->eventDispatcher->dispatchTyped($event);
755
+            } else {
756
+                $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
757
+                $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
758
+                $this->eventDispatcher->dispatchTyped($event);
759
+            }
760
+        } else {
761
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
762
+        }
763
+    }
764
+
765
+    /**
766
+     * remove all entries for files that are stored on the storage from the cache
767
+     */
768
+    public function clear() {
769
+        $query = $this->getQueryBuilder();
770
+        $query->delete('filecache')
771
+            ->whereStorageId($this->getNumericStorageId());
772
+        $query->execute();
773
+
774
+        $query = $this->connection->getQueryBuilder();
775
+        $query->delete('storages')
776
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
777
+        $query->execute();
778
+    }
779
+
780
+    /**
781
+     * Get the scan status of a file
782
+     *
783
+     * - Cache::NOT_FOUND: File is not in the cache
784
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
785
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
786
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
787
+     *
788
+     * @param string $file
789
+     *
790
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
791
+     */
792
+    public function getStatus($file) {
793
+        // normalize file
794
+        $file = $this->normalize($file);
795
+
796
+        $query = $this->getQueryBuilder();
797
+        $query->select('size')
798
+            ->from('filecache')
799
+            ->whereStorageId($this->getNumericStorageId())
800
+            ->wherePath($file);
801
+
802
+        $result = $query->execute();
803
+        $size = $result->fetchOne();
804
+        $result->closeCursor();
805
+
806
+        if ($size !== false) {
807
+            if ((int)$size === -1) {
808
+                return self::SHALLOW;
809
+            } else {
810
+                return self::COMPLETE;
811
+            }
812
+        } else {
813
+            if (isset($this->partial[$file])) {
814
+                return self::PARTIAL;
815
+            } else {
816
+                return self::NOT_FOUND;
817
+            }
818
+        }
819
+    }
820
+
821
+    /**
822
+     * search for files matching $pattern
823
+     *
824
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
825
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
826
+     */
827
+    public function search($pattern) {
828
+        $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
829
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
830
+    }
831
+
832
+    /**
833
+     * search for files by mimetype
834
+     *
835
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
836
+     *        where it will search for all mimetypes in the group ('image/*')
837
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
838
+     */
839
+    public function searchByMime($mimetype) {
840
+        if (!str_contains($mimetype, '/')) {
841
+            $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
842
+        } else {
843
+            $operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
844
+        }
845
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
846
+    }
847
+
848
+    public function searchQuery(ISearchQuery $searchQuery) {
849
+        return current($this->querySearchHelper->searchInCaches($searchQuery, [$this]));
850
+    }
851
+
852
+    /**
853
+     * Re-calculate the folder size and the size of all parent folders
854
+     *
855
+     * @param string|boolean $path
856
+     * @param array $data (optional) meta data of the folder
857
+     */
858
+    public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
859
+        $this->calculateFolderSize($path, $data);
860
+        if ($path !== '') {
861
+            $parent = dirname($path);
862
+            if ($parent === '.' || $parent === '/') {
863
+                $parent = '';
864
+            }
865
+            if ($isBackgroundScan) {
866
+                $parentData = $this->get($parent);
867
+                if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
868
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
869
+                }
870
+            } else {
871
+                $this->correctFolderSize($parent);
872
+            }
873
+        }
874
+    }
875
+
876
+    /**
877
+     * get the incomplete count that shares parent $folder
878
+     *
879
+     * @param int $fileId the file id of the folder
880
+     * @return int
881
+     */
882
+    public function getIncompleteChildrenCount($fileId) {
883
+        if ($fileId > -1) {
884
+            $query = $this->getQueryBuilder();
885
+            $query->select($query->func()->count())
886
+                ->from('filecache')
887
+                ->whereParent($fileId)
888
+                ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
889
+
890
+            $result = $query->execute();
891
+            $size = (int)$result->fetchOne();
892
+            $result->closeCursor();
893
+
894
+            return $size;
895
+        }
896
+        return -1;
897
+    }
898
+
899
+    /**
900
+     * calculate the size of a folder and set it in the cache
901
+     *
902
+     * @param string $path
903
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
904
+     * @return int|float
905
+     */
906
+    public function calculateFolderSize($path, $entry = null) {
907
+        return $this->calculateFolderSizeInner($path, $entry);
908
+    }
909
+
910
+
911
+    /**
912
+     * inner function because we can't add new params to the public function without breaking any child classes
913
+     *
914
+     * @param string $path
915
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
916
+     * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
917
+     * @return int|float
918
+     */
919
+    protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
920
+        $totalSize = 0;
921
+        if (is_null($entry) || !isset($entry['fileid'])) {
922
+            $entry = $this->get($path);
923
+        }
924
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
925
+            $id = $entry['fileid'];
926
+
927
+            $query = $this->getQueryBuilder();
928
+            $query->select('size', 'unencrypted_size')
929
+                ->from('filecache')
930
+                ->whereParent($id);
931
+            if ($ignoreUnknown) {
932
+                $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
933
+            }
934
+
935
+            $result = $query->execute();
936
+            $rows = $result->fetchAll();
937
+            $result->closeCursor();
938
+
939
+            if ($rows) {
940
+                $sizes = array_map(function (array $row) {
941
+                    return Util::numericToNumber($row['size']);
942
+                }, $rows);
943
+                $unencryptedOnlySizes = array_map(function (array $row) {
944
+                    return Util::numericToNumber($row['unencrypted_size']);
945
+                }, $rows);
946
+                $unencryptedSizes = array_map(function (array $row) {
947
+                    return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
948
+                }, $rows);
949
+
950
+                $sum = array_sum($sizes);
951
+                $min = min($sizes);
952
+
953
+                $unencryptedSum = array_sum($unencryptedSizes);
954
+                $unencryptedMin = min($unencryptedSizes);
955
+                $unencryptedMax = max($unencryptedOnlySizes);
956
+
957
+                $sum = 0 + $sum;
958
+                $min = 0 + $min;
959
+                if ($min === -1) {
960
+                    $totalSize = $min;
961
+                } else {
962
+                    $totalSize = $sum;
963
+                }
964
+                if ($unencryptedMin === -1 || $min === -1) {
965
+                    $unencryptedTotal = $unencryptedMin;
966
+                } else {
967
+                    $unencryptedTotal = $unencryptedSum;
968
+                }
969
+            } else {
970
+                $totalSize = 0;
971
+                $unencryptedTotal = 0;
972
+                $unencryptedMax = 0;
973
+            }
974
+
975
+            // only set unencrypted size for a folder if any child entries have it set, or the folder is empty
976
+            $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || $entry['unencrypted_size'] > 0;
977
+            if ($entry['size'] !== $totalSize || ($entry['unencrypted_size'] !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
978
+                if ($shouldWriteUnEncryptedSize) {
979
+                    // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
980
+                    if ($unencryptedMax === 0) {
981
+                        $unencryptedTotal = 0;
982
+                    }
983
+
984
+                    $this->update($id, [
985
+                        'size' => $totalSize,
986
+                        'unencrypted_size' => $unencryptedTotal,
987
+                    ]);
988
+                } else {
989
+                    $this->update($id, [
990
+                        'size' => $totalSize,
991
+                    ]);
992
+                }
993
+            }
994
+        }
995
+        return $totalSize;
996
+    }
997
+
998
+    /**
999
+     * get all file ids on the files on the storage
1000
+     *
1001
+     * @return int[]
1002
+     */
1003
+    public function getAll() {
1004
+        $query = $this->getQueryBuilder();
1005
+        $query->select('fileid')
1006
+            ->from('filecache')
1007
+            ->whereStorageId($this->getNumericStorageId());
1008
+
1009
+        $result = $query->execute();
1010
+        $files = $result->fetchAll(\PDO::FETCH_COLUMN);
1011
+        $result->closeCursor();
1012
+
1013
+        return array_map(function ($id) {
1014
+            return (int)$id;
1015
+        }, $files);
1016
+    }
1017
+
1018
+    /**
1019
+     * find a folder in the cache which has not been fully scanned
1020
+     *
1021
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1022
+     * use the one with the highest id gives the best result with the background scanner, since that is most
1023
+     * likely the folder where we stopped scanning previously
1024
+     *
1025
+     * @return string|false the path of the folder or false when no folder matched
1026
+     */
1027
+    public function getIncomplete() {
1028
+        $query = $this->getQueryBuilder();
1029
+        $query->select('path')
1030
+            ->from('filecache')
1031
+            ->whereStorageId($this->getNumericStorageId())
1032
+            ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
1033
+            ->orderBy('fileid', 'DESC')
1034
+            ->setMaxResults(1);
1035
+
1036
+        $result = $query->execute();
1037
+        $path = $result->fetchOne();
1038
+        $result->closeCursor();
1039
+
1040
+        if ($path === false) {
1041
+            return false;
1042
+        }
1043
+
1044
+        // Make sure Oracle does not continue with null for empty strings
1045
+        return (string)$path;
1046
+    }
1047
+
1048
+    /**
1049
+     * get the path of a file on this storage by it's file id
1050
+     *
1051
+     * @param int $id the file id of the file or folder to search
1052
+     * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
1053
+     */
1054
+    public function getPathById($id) {
1055
+        $query = $this->getQueryBuilder();
1056
+        $query->select('path')
1057
+            ->from('filecache')
1058
+            ->whereStorageId($this->getNumericStorageId())
1059
+            ->whereFileId($id);
1060
+
1061
+        $result = $query->execute();
1062
+        $path = $result->fetchOne();
1063
+        $result->closeCursor();
1064
+
1065
+        if ($path === false) {
1066
+            return null;
1067
+        }
1068
+
1069
+        return (string)$path;
1070
+    }
1071
+
1072
+    /**
1073
+     * get the storage id of the storage for a file and the internal path of the file
1074
+     * unlike getPathById this does not limit the search to files on this storage and
1075
+     * instead does a global search in the cache table
1076
+     *
1077
+     * @param int $id
1078
+     * @return array first element holding the storage id, second the path
1079
+     * @deprecated use getPathById() instead
1080
+     */
1081
+    public static function getById($id) {
1082
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1083
+        $query->select('path', 'storage')
1084
+            ->from('filecache')
1085
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1086
+
1087
+        $result = $query->execute();
1088
+        $row = $result->fetch();
1089
+        $result->closeCursor();
1090
+
1091
+        if ($row) {
1092
+            $numericId = $row['storage'];
1093
+            $path = $row['path'];
1094
+        } else {
1095
+            return null;
1096
+        }
1097
+
1098
+        if ($id = Storage::getStorageId($numericId)) {
1099
+            return [$id, $path];
1100
+        } else {
1101
+            return null;
1102
+        }
1103
+    }
1104
+
1105
+    /**
1106
+     * normalize the given path
1107
+     *
1108
+     * @param string $path
1109
+     * @return string
1110
+     */
1111
+    public function normalize($path) {
1112
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1113
+    }
1114
+
1115
+    /**
1116
+     * Copy a file or folder in the cache
1117
+     *
1118
+     * @param ICache $sourceCache
1119
+     * @param ICacheEntry $sourceEntry
1120
+     * @param string $targetPath
1121
+     * @return int fileId of copied entry
1122
+     */
1123
+    public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1124
+        if ($sourceEntry->getId() < 0) {
1125
+            throw new \RuntimeException("Invalid source cache entry on copyFromCache");
1126
+        }
1127
+        $data = $this->cacheEntryToArray($sourceEntry);
1128
+
1129
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1130
+        if ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
1131
+            $data['encrypted'] = 0;
1132
+        }
1133
+
1134
+        $fileId = $this->put($targetPath, $data);
1135
+        if ($fileId <= 0) {
1136
+            throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1137
+        }
1138
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1139
+            $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1140
+            foreach ($folderContent as $subEntry) {
1141
+                $subTargetPath = $targetPath . '/' . $subEntry->getName();
1142
+                $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1143
+            }
1144
+        }
1145
+        return $fileId;
1146
+    }
1147
+
1148
+    private function cacheEntryToArray(ICacheEntry $entry): array {
1149
+        return [
1150
+            'size' => $entry->getSize(),
1151
+            'mtime' => $entry->getMTime(),
1152
+            'storage_mtime' => $entry->getStorageMTime(),
1153
+            'mimetype' => $entry->getMimeType(),
1154
+            'mimepart' => $entry->getMimePart(),
1155
+            'etag' => $entry->getEtag(),
1156
+            'permissions' => $entry->getPermissions(),
1157
+            'encrypted' => $entry->isEncrypted(),
1158
+            'creation_time' => $entry->getCreationTime(),
1159
+            'upload_time' => $entry->getUploadTime(),
1160
+            'metadata_etag' => $entry->getMetadataEtag(),
1161
+        ];
1162
+    }
1163
+
1164
+    public function getQueryFilterForStorage(): ISearchOperator {
1165
+        return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1166
+    }
1167
+
1168
+    public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1169
+        if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1170
+            return $rawEntry;
1171
+        } else {
1172
+            return null;
1173
+        }
1174
+    }
1175 1175
 }
Please login to merge, or discard this patch.
Spacing   +37 added lines, -37 removed lines patch added patch discarded remove patch
@@ -188,29 +188,29 @@  discard block
 block discarded – undo
188 188
 	 */
189 189
 	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
190 190
 		//fix types
191
-		$data['name'] = (string)$data['name'];
192
-		$data['path'] = (string)$data['path'];
193
-		$data['fileid'] = (int)$data['fileid'];
194
-		$data['parent'] = (int)$data['parent'];
191
+		$data['name'] = (string) $data['name'];
192
+		$data['path'] = (string) $data['path'];
193
+		$data['fileid'] = (int) $data['fileid'];
194
+		$data['parent'] = (int) $data['parent'];
195 195
 		$data['size'] = Util::numericToNumber($data['size']);
196 196
 		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
197
-		$data['mtime'] = (int)$data['mtime'];
198
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
199
-		$data['encryptedVersion'] = (int)$data['encrypted'];
200
-		$data['encrypted'] = (bool)$data['encrypted'];
197
+		$data['mtime'] = (int) $data['mtime'];
198
+		$data['storage_mtime'] = (int) $data['storage_mtime'];
199
+		$data['encryptedVersion'] = (int) $data['encrypted'];
200
+		$data['encrypted'] = (bool) $data['encrypted'];
201 201
 		$data['storage_id'] = $data['storage'];
202
-		$data['storage'] = (int)$data['storage'];
202
+		$data['storage'] = (int) $data['storage'];
203 203
 		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
204 204
 		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
205 205
 		if ($data['storage_mtime'] == 0) {
206 206
 			$data['storage_mtime'] = $data['mtime'];
207 207
 		}
208
-		$data['permissions'] = (int)$data['permissions'];
208
+		$data['permissions'] = (int) $data['permissions'];
209 209
 		if (isset($data['creation_time'])) {
210
-			$data['creation_time'] = (int)$data['creation_time'];
210
+			$data['creation_time'] = (int) $data['creation_time'];
211 211
 		}
212 212
 		if (isset($data['upload_time'])) {
213
-			$data['upload_time'] = (int)$data['upload_time'];
213
+			$data['upload_time'] = (int) $data['upload_time'];
214 214
 		}
215 215
 		return new CacheEntry($data);
216 216
 	}
@@ -243,7 +243,7 @@  discard block
 block discarded – undo
243 243
 			$files = $result->fetchAll();
244 244
 			$result->closeCursor();
245 245
 
246
-			return array_map(function (array $data) {
246
+			return array_map(function(array $data) {
247 247
 				return self::cacheEntryFromData($data, $this->mimetypeLoader);
248 248
 			}, $files);
249 249
 		}
@@ -372,7 +372,7 @@  discard block
 block discarded – undo
372 372
 
373 373
 			$query->update('filecache')
374 374
 				->whereFileId($id)
375
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
375
+				->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
376 376
 					return $query->expr()->orX(
377 377
 						$query->expr()->neq($key, $query->createNamedParameter($value)),
378 378
 						$query->expr()->isNull($key)
@@ -401,7 +401,7 @@  discard block
 block discarded – undo
401 401
 				$query = $this->getQueryBuilder();
402 402
 				$query->update('filecache_extended')
403 403
 					->whereFileId($id)
404
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
404
+					->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
405 405
 						return $query->expr()->orX(
406 406
 							$query->expr()->neq($key, $query->createNamedParameter($value)),
407 407
 							$query->expr()->isNull($key)
@@ -498,7 +498,7 @@  discard block
 block discarded – undo
498 498
 		$id = $result->fetchOne();
499 499
 		$result->closeCursor();
500 500
 
501
-		return $id === false ? -1 : (int)$id;
501
+		return $id === false ? -1 : (int) $id;
502 502
 	}
503 503
 
504 504
 	/**
@@ -512,7 +512,7 @@  discard block
 block discarded – undo
512 512
 			return -1;
513 513
 		} else {
514 514
 			$parent = $this->getParentPath($file);
515
-			return (int)$this->getId($parent);
515
+			return (int) $this->getId($parent);
516 516
 		}
517 517
 	}
518 518
 
@@ -571,7 +571,7 @@  discard block
 block discarded – undo
571 571
 	 */
572 572
 	private function getSubFolders(ICacheEntry $entry) {
573 573
 		$children = $this->getFolderContentsById($entry->getId());
574
-		return array_filter($children, function ($child) {
574
+		return array_filter($children, function($child) {
575 575
 			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
576 576
 		});
577 577
 	}
@@ -592,10 +592,10 @@  discard block
 block discarded – undo
592 592
 		// and collecting all folder ids to later use to delete the filecache entries
593 593
 		while ($entryId = array_pop($queue)) {
594 594
 			$children = $this->getFolderContentsById($entryId);
595
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
595
+			$childIds = array_map(function(ICacheEntry $cacheEntry) {
596 596
 				return $cacheEntry->getId();
597 597
 			}, $children);
598
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
598
+			$childPaths = array_map(function(ICacheEntry $cacheEntry) {
599 599
 				return $cacheEntry->getPath();
600 600
 			}, $children);
601 601
 
@@ -612,7 +612,7 @@  discard block
 block discarded – undo
612 612
 			}
613 613
 
614 614
 			/** @var ICacheEntry[] $childFolders */
615
-			$childFolders = array_filter($children, function ($child) {
615
+			$childFolders = array_filter($children, function($child) {
616 616
 				return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
617 617
 			});
618 618
 			foreach ($childFolders as $folder) {
@@ -682,7 +682,7 @@  discard block
 block discarded – undo
682 682
 
683 683
 			$sourceData = $sourceCache->get($sourcePath);
684 684
 			if ($sourceData === false) {
685
-				throw new \Exception('Invalid source storage path: ' . $sourcePath);
685
+				throw new \Exception('Invalid source storage path: '.$sourcePath);
686 686
 			}
687 687
 
688 688
 			$sourceId = $sourceData['fileid'];
@@ -692,10 +692,10 @@  discard block
 block discarded – undo
692 692
 			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
693 693
 
694 694
 			if (is_null($sourceStorageId) || $sourceStorageId === false) {
695
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
695
+				throw new \Exception('Invalid source storage id: '.$sourceStorageId);
696 696
 			}
697 697
 			if (is_null($targetStorageId) || $targetStorageId === false) {
698
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
698
+				throw new \Exception('Invalid target storage id: '.$targetStorageId);
699 699
 			}
700 700
 
701 701
 			$this->connection->beginTransaction();
@@ -714,7 +714,7 @@  discard block
 block discarded – undo
714 714
 					->set('path_hash', $fun->md5($newPathFunction))
715 715
 					->set('path', $newPathFunction)
716 716
 					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
717
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
717
+					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath).'/%')));
718 718
 
719 719
 				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
720 720
 				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
@@ -804,7 +804,7 @@  discard block
 block discarded – undo
804 804
 		$result->closeCursor();
805 805
 
806 806
 		if ($size !== false) {
807
-			if ((int)$size === -1) {
807
+			if ((int) $size === -1) {
808 808
 				return self::SHALLOW;
809 809
 			} else {
810 810
 				return self::COMPLETE;
@@ -838,7 +838,7 @@  discard block
 block discarded – undo
838 838
 	 */
839 839
 	public function searchByMime($mimetype) {
840 840
 		if (!str_contains($mimetype, '/')) {
841
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
841
+			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype.'/%');
842 842
 		} else {
843 843
 			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
844 844
 		}
@@ -888,7 +888,7 @@  discard block
 block discarded – undo
888 888
 				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
889 889
 
890 890
 			$result = $query->execute();
891
-			$size = (int)$result->fetchOne();
891
+			$size = (int) $result->fetchOne();
892 892
 			$result->closeCursor();
893 893
 
894 894
 			return $size;
@@ -937,13 +937,13 @@  discard block
 block discarded – undo
937 937
 			$result->closeCursor();
938 938
 
939 939
 			if ($rows) {
940
-				$sizes = array_map(function (array $row) {
940
+				$sizes = array_map(function(array $row) {
941 941
 					return Util::numericToNumber($row['size']);
942 942
 				}, $rows);
943
-				$unencryptedOnlySizes = array_map(function (array $row) {
943
+				$unencryptedOnlySizes = array_map(function(array $row) {
944 944
 					return Util::numericToNumber($row['unencrypted_size']);
945 945
 				}, $rows);
946
-				$unencryptedSizes = array_map(function (array $row) {
946
+				$unencryptedSizes = array_map(function(array $row) {
947 947
 					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
948 948
 				}, $rows);
949 949
 
@@ -1010,8 +1010,8 @@  discard block
 block discarded – undo
1010 1010
 		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1011 1011
 		$result->closeCursor();
1012 1012
 
1013
-		return array_map(function ($id) {
1014
-			return (int)$id;
1013
+		return array_map(function($id) {
1014
+			return (int) $id;
1015 1015
 		}, $files);
1016 1016
 	}
1017 1017
 
@@ -1042,7 +1042,7 @@  discard block
 block discarded – undo
1042 1042
 		}
1043 1043
 
1044 1044
 		// Make sure Oracle does not continue with null for empty strings
1045
-		return (string)$path;
1045
+		return (string) $path;
1046 1046
 	}
1047 1047
 
1048 1048
 	/**
@@ -1066,7 +1066,7 @@  discard block
 block discarded – undo
1066 1066
 			return null;
1067 1067
 		}
1068 1068
 
1069
-		return (string)$path;
1069
+		return (string) $path;
1070 1070
 	}
1071 1071
 
1072 1072
 	/**
@@ -1133,12 +1133,12 @@  discard block
 block discarded – undo
1133 1133
 
1134 1134
 		$fileId = $this->put($targetPath, $data);
1135 1135
 		if ($fileId <= 0) {
1136
-			throw new \RuntimeException("Failed to copy to " . $targetPath . " from cache with source data " . json_encode($data) . " ");
1136
+			throw new \RuntimeException("Failed to copy to ".$targetPath." from cache with source data ".json_encode($data)." ");
1137 1137
 		}
1138 1138
 		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1139 1139
 			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1140 1140
 			foreach ($folderContent as $subEntry) {
1141
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1141
+				$subTargetPath = $targetPath.'/'.$subEntry->getName();
1142 1142
 				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1143 1143
 			}
1144 1144
 		}
Please login to merge, or discard this patch.
lib/private/Files/Cache/LocalRootScanner.php 1 patch
Indentation   +18 added lines, -18 removed lines patch added patch discarded remove patch
@@ -26,24 +26,24 @@
 block discarded – undo
26 26
 namespace OC\Files\Cache;
27 27
 
28 28
 class LocalRootScanner extends Scanner {
29
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
30
-		if ($this->shouldScanPath($file)) {
31
-			return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data);
32
-		} else {
33
-			return null;
34
-		}
35
-	}
29
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
30
+        if ($this->shouldScanPath($file)) {
31
+            return parent::scanFile($file, $reuseExisting, $parentId, $cacheData, $lock, $data);
32
+        } else {
33
+            return null;
34
+        }
35
+    }
36 36
 
37
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
38
-		if ($this->shouldScanPath($path)) {
39
-			return parent::scan($path, $recursive, $reuse, $lock);
40
-		} else {
41
-			return null;
42
-		}
43
-	}
37
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
38
+        if ($this->shouldScanPath($path)) {
39
+            return parent::scan($path, $recursive, $reuse, $lock);
40
+        } else {
41
+            return null;
42
+        }
43
+    }
44 44
 
45
-	private function shouldScanPath(string $path): bool {
46
-		$path = trim($path, '/');
47
-		return $path === '' || str_starts_with($path, 'appdata_') || str_starts_with($path, '__groupfolders');
48
-	}
45
+    private function shouldScanPath(string $path): bool {
46
+        $path = trim($path, '/');
47
+        return $path === '' || str_starts_with($path, 'appdata_') || str_starts_with($path, '__groupfolders');
48
+    }
49 49
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Propagator.php 1 patch
Indentation   +195 added lines, -195 removed lines patch added patch discarded remove patch
@@ -36,199 +36,199 @@
 block discarded – undo
36 36
  * Propagate etags and mtimes within the storage
37 37
  */
38 38
 class Propagator implements IPropagator {
39
-	public const MAX_RETRIES = 3;
40
-	private $inBatch = false;
41
-
42
-	private $batch = [];
43
-
44
-	/**
45
-	 * @var \OC\Files\Storage\Storage
46
-	 */
47
-	protected $storage;
48
-
49
-	/**
50
-	 * @var IDBConnection
51
-	 */
52
-	private $connection;
53
-
54
-	/**
55
-	 * @var array
56
-	 */
57
-	private $ignore = [];
58
-
59
-	public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
60
-		$this->storage = $storage;
61
-		$this->connection = $connection;
62
-		$this->ignore = $ignore;
63
-	}
64
-
65
-
66
-	/**
67
-	 * @param string $internalPath
68
-	 * @param int $time
69
-	 * @param int $sizeDifference number of bytes the file has grown
70
-	 */
71
-	public function propagateChange($internalPath, $time, $sizeDifference = 0) {
72
-		// Do not propagate changes in ignored paths
73
-		foreach ($this->ignore as $ignore) {
74
-			if (str_starts_with($internalPath, $ignore)) {
75
-				return;
76
-			}
77
-		}
78
-
79
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
80
-
81
-		$parents = $this->getParents($internalPath);
82
-
83
-		if ($this->inBatch) {
84
-			foreach ($parents as $parent) {
85
-				$this->addToBatch($parent, $time, $sizeDifference);
86
-			}
87
-			return;
88
-		}
89
-
90
-		$parentHashes = array_map('md5', $parents);
91
-		$etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
92
-
93
-		$builder = $this->connection->getQueryBuilder();
94
-		$hashParams = array_map(function ($hash) use ($builder) {
95
-			return $builder->expr()->literal($hash);
96
-		}, $parentHashes);
97
-
98
-		$builder->update('filecache')
99
-			->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
100
-			->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
101
-			->andWhere($builder->expr()->in('path_hash', $hashParams));
102
-		if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
103
-			$builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
104
-		}
105
-
106
-		if ($sizeDifference !== 0) {
107
-			$hasCalculatedSize = $builder->expr()->gt('size', $builder->expr()->literal(-1, IQUeryBuilder::PARAM_INT));
108
-			$sizeColumn = $builder->getColumnName('size');
109
-			$newSize = $builder->func()->greatest(
110
-				$builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
111
-				$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
112
-			);
113
-
114
-			// Only update if row had a previously calculated size
115
-			$builder->set('size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newSize ELSE $sizeColumn END"));
116
-
117
-			if ($this->storage->instanceOfStorage(Encryption::class)) {
118
-				// in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size
119
-				$hasUnencryptedSize = $builder->expr()->neq('unencrypted_size', $builder->expr()->literal(0, IQueryBuilder::PARAM_INT));
120
-				$sizeColumn = $builder->getColumnName('size');
121
-				$unencryptedSizeColumn = $builder->getColumnName('unencrypted_size');
122
-				$newUnencryptedSize = $builder->func()->greatest(
123
-					$builder->func()->add(
124
-						$builder->createFunction("CASE WHEN $hasUnencryptedSize THEN $unencryptedSizeColumn ELSE $sizeColumn END"),
125
-						$builder->createNamedParameter($sizeDifference)
126
-					),
127
-					$builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
128
-				);
129
-
130
-				// Only update if row had a previously calculated size
131
-				$builder->set('unencrypted_size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newUnencryptedSize ELSE $unencryptedSizeColumn END"));
132
-			}
133
-		}
134
-
135
-		for ($i = 0; $i < self::MAX_RETRIES; $i++) {
136
-			try {
137
-				$builder->executeStatement();
138
-				break;
139
-			} catch (DbalException $e) {
140
-				if (!$e->isRetryable()) {
141
-					throw $e;
142
-				}
143
-
144
-				/** @var LoggerInterface $loggerInterface */
145
-				$loggerInterface = \OCP\Server::get(LoggerInterface::class);
146
-				$loggerInterface->warning('Retrying propagation query after retryable exception.', [ 'exception' => $e ]);
147
-			}
148
-		}
149
-	}
150
-
151
-	protected function getParents($path) {
152
-		$parts = explode('/', $path);
153
-		$parent = '';
154
-		$parents = [];
155
-		foreach ($parts as $part) {
156
-			$parents[] = $parent;
157
-			$parent = trim($parent . '/' . $part, '/');
158
-		}
159
-		return $parents;
160
-	}
161
-
162
-	/**
163
-	 * Mark the beginning of a propagation batch
164
-	 *
165
-	 * Note that not all cache setups support propagation in which case this will be a noop
166
-	 *
167
-	 * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
168
-	 * before the batch is committed.
169
-	 */
170
-	public function beginBatch() {
171
-		$this->inBatch = true;
172
-	}
173
-
174
-	private function addToBatch($internalPath, $time, $sizeDifference) {
175
-		if (!isset($this->batch[$internalPath])) {
176
-			$this->batch[$internalPath] = [
177
-				'hash' => md5($internalPath),
178
-				'time' => $time,
179
-				'size' => $sizeDifference,
180
-			];
181
-		} else {
182
-			$this->batch[$internalPath]['size'] += $sizeDifference;
183
-			if ($time > $this->batch[$internalPath]['time']) {
184
-				$this->batch[$internalPath]['time'] = $time;
185
-			}
186
-		}
187
-	}
188
-
189
-	/**
190
-	 * Commit the active propagation batch
191
-	 */
192
-	public function commitBatch() {
193
-		if (!$this->inBatch) {
194
-			throw new \BadMethodCallException('Not in batch');
195
-		}
196
-		$this->inBatch = false;
197
-
198
-		$this->connection->beginTransaction();
199
-
200
-		$query = $this->connection->getQueryBuilder();
201
-		$storageId = (int)$this->storage->getStorageCache()->getNumericId();
202
-
203
-		$query->update('filecache')
204
-			->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
205
-			->set('etag', $query->expr()->literal(uniqid()))
206
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
207
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
208
-
209
-		$sizeQuery = $this->connection->getQueryBuilder();
210
-		$sizeQuery->update('filecache')
211
-			->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
212
-			->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
213
-			->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
214
-			->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
215
-
216
-		foreach ($this->batch as $item) {
217
-			$query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
218
-			$query->setParameter('hash', $item['hash']);
219
-
220
-			$query->execute();
221
-
222
-			if ($item['size']) {
223
-				$sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
224
-				$sizeQuery->setParameter('hash', $item['hash']);
225
-
226
-				$sizeQuery->execute();
227
-			}
228
-		}
229
-
230
-		$this->batch = [];
231
-
232
-		$this->connection->commit();
233
-	}
39
+    public const MAX_RETRIES = 3;
40
+    private $inBatch = false;
41
+
42
+    private $batch = [];
43
+
44
+    /**
45
+     * @var \OC\Files\Storage\Storage
46
+     */
47
+    protected $storage;
48
+
49
+    /**
50
+     * @var IDBConnection
51
+     */
52
+    private $connection;
53
+
54
+    /**
55
+     * @var array
56
+     */
57
+    private $ignore = [];
58
+
59
+    public function __construct(\OC\Files\Storage\Storage $storage, IDBConnection $connection, array $ignore = []) {
60
+        $this->storage = $storage;
61
+        $this->connection = $connection;
62
+        $this->ignore = $ignore;
63
+    }
64
+
65
+
66
+    /**
67
+     * @param string $internalPath
68
+     * @param int $time
69
+     * @param int $sizeDifference number of bytes the file has grown
70
+     */
71
+    public function propagateChange($internalPath, $time, $sizeDifference = 0) {
72
+        // Do not propagate changes in ignored paths
73
+        foreach ($this->ignore as $ignore) {
74
+            if (str_starts_with($internalPath, $ignore)) {
75
+                return;
76
+            }
77
+        }
78
+
79
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
80
+
81
+        $parents = $this->getParents($internalPath);
82
+
83
+        if ($this->inBatch) {
84
+            foreach ($parents as $parent) {
85
+                $this->addToBatch($parent, $time, $sizeDifference);
86
+            }
87
+            return;
88
+        }
89
+
90
+        $parentHashes = array_map('md5', $parents);
91
+        $etag = uniqid(); // since we give all folders the same etag we don't ask the storage for the etag
92
+
93
+        $builder = $this->connection->getQueryBuilder();
94
+        $hashParams = array_map(function ($hash) use ($builder) {
95
+            return $builder->expr()->literal($hash);
96
+        }, $parentHashes);
97
+
98
+        $builder->update('filecache')
99
+            ->set('mtime', $builder->func()->greatest('mtime', $builder->createNamedParameter((int)$time, IQueryBuilder::PARAM_INT)))
100
+            ->where($builder->expr()->eq('storage', $builder->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
101
+            ->andWhere($builder->expr()->in('path_hash', $hashParams));
102
+        if (!$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
103
+            $builder->set('etag', $builder->createNamedParameter($etag, IQueryBuilder::PARAM_STR));
104
+        }
105
+
106
+        if ($sizeDifference !== 0) {
107
+            $hasCalculatedSize = $builder->expr()->gt('size', $builder->expr()->literal(-1, IQUeryBuilder::PARAM_INT));
108
+            $sizeColumn = $builder->getColumnName('size');
109
+            $newSize = $builder->func()->greatest(
110
+                $builder->func()->add('size', $builder->createNamedParameter($sizeDifference)),
111
+                $builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
112
+            );
113
+
114
+            // Only update if row had a previously calculated size
115
+            $builder->set('size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newSize ELSE $sizeColumn END"));
116
+
117
+            if ($this->storage->instanceOfStorage(Encryption::class)) {
118
+                // in case of encryption being enabled after some files are already uploaded, some entries will have an unencrypted_size of 0 and a non-zero size
119
+                $hasUnencryptedSize = $builder->expr()->neq('unencrypted_size', $builder->expr()->literal(0, IQueryBuilder::PARAM_INT));
120
+                $sizeColumn = $builder->getColumnName('size');
121
+                $unencryptedSizeColumn = $builder->getColumnName('unencrypted_size');
122
+                $newUnencryptedSize = $builder->func()->greatest(
123
+                    $builder->func()->add(
124
+                        $builder->createFunction("CASE WHEN $hasUnencryptedSize THEN $unencryptedSizeColumn ELSE $sizeColumn END"),
125
+                        $builder->createNamedParameter($sizeDifference)
126
+                    ),
127
+                    $builder->createNamedParameter(-1, IQueryBuilder::PARAM_INT)
128
+                );
129
+
130
+                // Only update if row had a previously calculated size
131
+                $builder->set('unencrypted_size', $builder->createFunction("CASE WHEN $hasCalculatedSize THEN $newUnencryptedSize ELSE $unencryptedSizeColumn END"));
132
+            }
133
+        }
134
+
135
+        for ($i = 0; $i < self::MAX_RETRIES; $i++) {
136
+            try {
137
+                $builder->executeStatement();
138
+                break;
139
+            } catch (DbalException $e) {
140
+                if (!$e->isRetryable()) {
141
+                    throw $e;
142
+                }
143
+
144
+                /** @var LoggerInterface $loggerInterface */
145
+                $loggerInterface = \OCP\Server::get(LoggerInterface::class);
146
+                $loggerInterface->warning('Retrying propagation query after retryable exception.', [ 'exception' => $e ]);
147
+            }
148
+        }
149
+    }
150
+
151
+    protected function getParents($path) {
152
+        $parts = explode('/', $path);
153
+        $parent = '';
154
+        $parents = [];
155
+        foreach ($parts as $part) {
156
+            $parents[] = $parent;
157
+            $parent = trim($parent . '/' . $part, '/');
158
+        }
159
+        return $parents;
160
+    }
161
+
162
+    /**
163
+     * Mark the beginning of a propagation batch
164
+     *
165
+     * Note that not all cache setups support propagation in which case this will be a noop
166
+     *
167
+     * Batching for cache setups that do support it has to be explicit since the cache state is not fully consistent
168
+     * before the batch is committed.
169
+     */
170
+    public function beginBatch() {
171
+        $this->inBatch = true;
172
+    }
173
+
174
+    private function addToBatch($internalPath, $time, $sizeDifference) {
175
+        if (!isset($this->batch[$internalPath])) {
176
+            $this->batch[$internalPath] = [
177
+                'hash' => md5($internalPath),
178
+                'time' => $time,
179
+                'size' => $sizeDifference,
180
+            ];
181
+        } else {
182
+            $this->batch[$internalPath]['size'] += $sizeDifference;
183
+            if ($time > $this->batch[$internalPath]['time']) {
184
+                $this->batch[$internalPath]['time'] = $time;
185
+            }
186
+        }
187
+    }
188
+
189
+    /**
190
+     * Commit the active propagation batch
191
+     */
192
+    public function commitBatch() {
193
+        if (!$this->inBatch) {
194
+            throw new \BadMethodCallException('Not in batch');
195
+        }
196
+        $this->inBatch = false;
197
+
198
+        $this->connection->beginTransaction();
199
+
200
+        $query = $this->connection->getQueryBuilder();
201
+        $storageId = (int)$this->storage->getStorageCache()->getNumericId();
202
+
203
+        $query->update('filecache')
204
+            ->set('mtime', $query->func()->greatest('mtime', $query->createParameter('time')))
205
+            ->set('etag', $query->expr()->literal(uniqid()))
206
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
207
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')));
208
+
209
+        $sizeQuery = $this->connection->getQueryBuilder();
210
+        $sizeQuery->update('filecache')
211
+            ->set('size', $sizeQuery->func()->add('size', $sizeQuery->createParameter('size')))
212
+            ->where($query->expr()->eq('storage', $query->expr()->literal($storageId, IQueryBuilder::PARAM_INT)))
213
+            ->andWhere($query->expr()->eq('path_hash', $query->createParameter('hash')))
214
+            ->andWhere($sizeQuery->expr()->gt('size', $sizeQuery->expr()->literal(-1, IQueryBuilder::PARAM_INT)));
215
+
216
+        foreach ($this->batch as $item) {
217
+            $query->setParameter('time', $item['time'], IQueryBuilder::PARAM_INT);
218
+            $query->setParameter('hash', $item['hash']);
219
+
220
+            $query->execute();
221
+
222
+            if ($item['size']) {
223
+                $sizeQuery->setParameter('size', $item['size'], IQueryBuilder::PARAM_INT);
224
+                $sizeQuery->setParameter('hash', $item['hash']);
225
+
226
+                $sizeQuery->execute();
227
+            }
228
+        }
229
+
230
+        $this->batch = [];
231
+
232
+        $this->connection->commit();
233
+    }
234 234
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Scanner.php 1 patch
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|float 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 (str_contains($file, '.part/')) {
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|float 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 (str_contains($file, '.part/')) {
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.
lib/private/Files/Storage/Wrapper/Encryption.php 1 patch
Indentation   +1039 added lines, -1039 removed lines patch added patch discarded remove patch
@@ -56,1043 +56,1043 @@
 block discarded – undo
56 56
 use Psr\Log\LoggerInterface;
57 57
 
58 58
 class Encryption extends Wrapper {
59
-	use LocalTempFileTrait;
60
-
61
-	/** @var string */
62
-	private $mountPoint;
63
-
64
-	/** @var \OC\Encryption\Util */
65
-	private $util;
66
-
67
-	/** @var \OCP\Encryption\IManager */
68
-	private $encryptionManager;
69
-
70
-	private LoggerInterface $logger;
71
-
72
-	/** @var string */
73
-	private $uid;
74
-
75
-	/** @var array */
76
-	protected $unencryptedSize;
77
-
78
-	/** @var \OCP\Encryption\IFile */
79
-	private $fileHelper;
80
-
81
-	/** @var IMountPoint */
82
-	private $mount;
83
-
84
-	/** @var IStorage */
85
-	private $keyStorage;
86
-
87
-	/** @var Update */
88
-	private $update;
89
-
90
-	/** @var Manager */
91
-	private $mountManager;
92
-
93
-	/** @var array remember for which path we execute the repair step to avoid recursions */
94
-	private $fixUnencryptedSizeOf = [];
95
-
96
-	/** @var  ArrayCache */
97
-	private $arrayCache;
98
-
99
-	/** @var CappedMemoryCache<bool> */
100
-	private CappedMemoryCache $encryptedPaths;
101
-
102
-	/**
103
-	 * @param array $parameters
104
-	 */
105
-	public function __construct(
106
-		$parameters,
107
-		IManager $encryptionManager = null,
108
-		Util $util = null,
109
-		LoggerInterface $logger = null,
110
-		IFile $fileHelper = null,
111
-		$uid = null,
112
-		IStorage $keyStorage = null,
113
-		Update $update = null,
114
-		Manager $mountManager = null,
115
-		ArrayCache $arrayCache = null
116
-	) {
117
-		$this->mountPoint = $parameters['mountPoint'];
118
-		$this->mount = $parameters['mount'];
119
-		$this->encryptionManager = $encryptionManager;
120
-		$this->util = $util;
121
-		$this->logger = $logger;
122
-		$this->uid = $uid;
123
-		$this->fileHelper = $fileHelper;
124
-		$this->keyStorage = $keyStorage;
125
-		$this->unencryptedSize = [];
126
-		$this->update = $update;
127
-		$this->mountManager = $mountManager;
128
-		$this->arrayCache = $arrayCache;
129
-		$this->encryptedPaths = new CappedMemoryCache();
130
-		parent::__construct($parameters);
131
-	}
132
-
133
-	/**
134
-	 * see https://www.php.net/manual/en/function.filesize.php
135
-	 * The result for filesize when called on a folder is required to be 0
136
-	 */
137
-	public function filesize($path): false|int|float {
138
-		$fullPath = $this->getFullPath($path);
139
-
140
-		$info = $this->getCache()->get($path);
141
-		if ($info === false) {
142
-			return false;
143
-		}
144
-		if (isset($this->unencryptedSize[$fullPath])) {
145
-			$size = $this->unencryptedSize[$fullPath];
146
-			// update file cache
147
-			if ($info instanceof ICacheEntry) {
148
-				$info['encrypted'] = $info['encryptedVersion'];
149
-			} else {
150
-				if (!is_array($info)) {
151
-					$info = [];
152
-				}
153
-				$info['encrypted'] = true;
154
-				$info = new CacheEntry($info);
155
-			}
156
-
157
-			if ($size !== $info->getUnencryptedSize()) {
158
-				$this->getCache()->update($info->getId(), [
159
-					'unencrypted_size' => $size
160
-				]);
161
-			}
162
-
163
-			return $size;
164
-		}
165
-
166
-		if (isset($info['fileid']) && $info['encrypted']) {
167
-			return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
168
-		}
169
-
170
-		return $this->storage->filesize($path);
171
-	}
172
-
173
-	/**
174
-	 * @param string $path
175
-	 * @param array $data
176
-	 * @return array
177
-	 */
178
-	private function modifyMetaData(string $path, array $data): array {
179
-		$fullPath = $this->getFullPath($path);
180
-		$info = $this->getCache()->get($path);
181
-
182
-		if (isset($this->unencryptedSize[$fullPath])) {
183
-			$data['encrypted'] = true;
184
-			$data['size'] = $this->unencryptedSize[$fullPath];
185
-			$data['unencrypted_size'] = $data['size'];
186
-		} else {
187
-			if (isset($info['fileid']) && $info['encrypted']) {
188
-				$data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
189
-				$data['encrypted'] = true;
190
-				$data['unencrypted_size'] = $data['size'];
191
-			}
192
-		}
193
-
194
-		if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
195
-			$data['encryptedVersion'] = $info['encryptedVersion'];
196
-		}
197
-
198
-		return $data;
199
-	}
200
-
201
-	public function getMetaData($path) {
202
-		$data = $this->storage->getMetaData($path);
203
-		if (is_null($data)) {
204
-			return null;
205
-		}
206
-		return $this->modifyMetaData($path, $data);
207
-	}
208
-
209
-	public function getDirectoryContent($directory): \Traversable {
210
-		$parent = rtrim($directory, '/');
211
-		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
212
-			yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
213
-		}
214
-	}
215
-
216
-	/**
217
-	 * see https://www.php.net/manual/en/function.file_get_contents.php
218
-	 *
219
-	 * @param string $path
220
-	 * @return string|false
221
-	 */
222
-	public function file_get_contents($path) {
223
-		$encryptionModule = $this->getEncryptionModule($path);
224
-
225
-		if ($encryptionModule) {
226
-			$handle = $this->fopen($path, "r");
227
-			if (!$handle) {
228
-				return false;
229
-			}
230
-			$data = stream_get_contents($handle);
231
-			fclose($handle);
232
-			return $data;
233
-		}
234
-		return $this->storage->file_get_contents($path);
235
-	}
236
-
237
-	/**
238
-	 * see https://www.php.net/manual/en/function.file_put_contents.php
239
-	 *
240
-	 * @param string $path
241
-	 * @param mixed $data
242
-	 * @return int|false
243
-	 */
244
-	public function file_put_contents($path, $data) {
245
-		// file put content will always be translated to a stream write
246
-		$handle = $this->fopen($path, 'w');
247
-		if (is_resource($handle)) {
248
-			$written = fwrite($handle, $data);
249
-			fclose($handle);
250
-			return $written;
251
-		}
252
-
253
-		return false;
254
-	}
255
-
256
-	/**
257
-	 * see https://www.php.net/manual/en/function.unlink.php
258
-	 *
259
-	 * @param string $path
260
-	 * @return bool
261
-	 */
262
-	public function unlink($path) {
263
-		$fullPath = $this->getFullPath($path);
264
-		if ($this->util->isExcluded($fullPath)) {
265
-			return $this->storage->unlink($path);
266
-		}
267
-
268
-		$encryptionModule = $this->getEncryptionModule($path);
269
-		if ($encryptionModule) {
270
-			$this->keyStorage->deleteAllFileKeys($fullPath);
271
-		}
272
-
273
-		return $this->storage->unlink($path);
274
-	}
275
-
276
-	/**
277
-	 * see https://www.php.net/manual/en/function.rename.php
278
-	 *
279
-	 * @param string $source
280
-	 * @param string $target
281
-	 * @return bool
282
-	 */
283
-	public function rename($source, $target) {
284
-		$result = $this->storage->rename($source, $target);
285
-
286
-		if ($result &&
287
-			// versions always use the keys from the original file, so we can skip
288
-			// this step for versions
289
-			$this->isVersion($target) === false &&
290
-			$this->encryptionManager->isEnabled()) {
291
-			$sourcePath = $this->getFullPath($source);
292
-			if (!$this->util->isExcluded($sourcePath)) {
293
-				$targetPath = $this->getFullPath($target);
294
-				if (isset($this->unencryptedSize[$sourcePath])) {
295
-					$this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
296
-				}
297
-				$this->keyStorage->renameKeys($sourcePath, $targetPath);
298
-				$module = $this->getEncryptionModule($target);
299
-				if ($module) {
300
-					$module->update($targetPath, $this->uid, []);
301
-				}
302
-			}
303
-		}
304
-
305
-		return $result;
306
-	}
307
-
308
-	/**
309
-	 * see https://www.php.net/manual/en/function.rmdir.php
310
-	 *
311
-	 * @param string $path
312
-	 * @return bool
313
-	 */
314
-	public function rmdir($path) {
315
-		$result = $this->storage->rmdir($path);
316
-		$fullPath = $this->getFullPath($path);
317
-		if ($result &&
318
-			$this->util->isExcluded($fullPath) === false &&
319
-			$this->encryptionManager->isEnabled()
320
-		) {
321
-			$this->keyStorage->deleteAllFileKeys($fullPath);
322
-		}
323
-
324
-		return $result;
325
-	}
326
-
327
-	/**
328
-	 * check if a file can be read
329
-	 *
330
-	 * @param string $path
331
-	 * @return bool
332
-	 */
333
-	public function isReadable($path) {
334
-		$isReadable = true;
335
-
336
-		$metaData = $this->getMetaData($path);
337
-		if (
338
-			!$this->is_dir($path) &&
339
-			isset($metaData['encrypted']) &&
340
-			$metaData['encrypted'] === true
341
-		) {
342
-			$fullPath = $this->getFullPath($path);
343
-			$module = $this->getEncryptionModule($path);
344
-			$isReadable = $module->isReadable($fullPath, $this->uid);
345
-		}
346
-
347
-		return $this->storage->isReadable($path) && $isReadable;
348
-	}
349
-
350
-	/**
351
-	 * see https://www.php.net/manual/en/function.copy.php
352
-	 *
353
-	 * @param string $source
354
-	 * @param string $target
355
-	 */
356
-	public function copy($source, $target): bool {
357
-		$sourcePath = $this->getFullPath($source);
358
-
359
-		if ($this->util->isExcluded($sourcePath)) {
360
-			return $this->storage->copy($source, $target);
361
-		}
362
-
363
-		// need to stream copy file by file in case we copy between a encrypted
364
-		// and a unencrypted storage
365
-		$this->unlink($target);
366
-		return $this->copyFromStorage($this, $source, $target);
367
-	}
368
-
369
-	/**
370
-	 * see https://www.php.net/manual/en/function.fopen.php
371
-	 *
372
-	 * @param string $path
373
-	 * @param string $mode
374
-	 * @return resource|bool
375
-	 * @throws GenericEncryptionException
376
-	 * @throws ModuleDoesNotExistsException
377
-	 */
378
-	public function fopen($path, $mode) {
379
-		// check if the file is stored in the array cache, this means that we
380
-		// copy a file over to the versions folder, in this case we don't want to
381
-		// decrypt it
382
-		if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
383
-			$this->arrayCache->remove('encryption_copy_version_' . $path);
384
-			return $this->storage->fopen($path, $mode);
385
-		}
386
-
387
-		$encryptionEnabled = $this->encryptionManager->isEnabled();
388
-		$shouldEncrypt = false;
389
-		$encryptionModule = null;
390
-		$header = $this->getHeader($path);
391
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
392
-		$fullPath = $this->getFullPath($path);
393
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
394
-
395
-		if ($this->util->isExcluded($fullPath) === false) {
396
-			$size = $unencryptedSize = 0;
397
-			$realFile = $this->util->stripPartialFileExtension($path);
398
-			$targetExists = $this->is_file($realFile) || $this->file_exists($path);
399
-			$targetIsEncrypted = false;
400
-			if ($targetExists) {
401
-				// in case the file exists we require the explicit module as
402
-				// specified in the file header - otherwise we need to fail hard to
403
-				// prevent data loss on client side
404
-				if (!empty($encryptionModuleId)) {
405
-					$targetIsEncrypted = true;
406
-					$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
407
-				}
408
-
409
-				if ($this->file_exists($path)) {
410
-					$size = $this->storage->filesize($path);
411
-					$unencryptedSize = $this->filesize($path);
412
-				} else {
413
-					$size = $unencryptedSize = 0;
414
-				}
415
-			}
416
-
417
-			try {
418
-				if (
419
-					$mode === 'w'
420
-					|| $mode === 'w+'
421
-					|| $mode === 'wb'
422
-					|| $mode === 'wb+'
423
-				) {
424
-					// if we update a encrypted file with a un-encrypted one we change the db flag
425
-					if ($targetIsEncrypted && $encryptionEnabled === false) {
426
-						$cache = $this->storage->getCache();
427
-						if ($cache) {
428
-							$entry = $cache->get($path);
429
-							$cache->update($entry->getId(), ['encrypted' => 0]);
430
-						}
431
-					}
432
-					if ($encryptionEnabled) {
433
-						// if $encryptionModuleId is empty, the default module will be used
434
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
435
-						$shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
436
-						$signed = true;
437
-					}
438
-				} else {
439
-					$info = $this->getCache()->get($path);
440
-					// only get encryption module if we found one in the header
441
-					// or if file should be encrypted according to the file cache
442
-					if (!empty($encryptionModuleId)) {
443
-						$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
444
-						$shouldEncrypt = true;
445
-					} elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
446
-						// we come from a old installation. No header and/or no module defined
447
-						// but the file is encrypted. In this case we need to use the
448
-						// OC_DEFAULT_MODULE to read the file
449
-						$encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
450
-						$shouldEncrypt = true;
451
-						$targetIsEncrypted = true;
452
-					}
453
-				}
454
-			} catch (ModuleDoesNotExistsException $e) {
455
-				$this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
456
-					'exception' => $e,
457
-					'app' => 'core',
458
-				]);
459
-			}
460
-
461
-			// encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
462
-			if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
463
-				if (!$targetExists || !$targetIsEncrypted) {
464
-					$shouldEncrypt = false;
465
-				}
466
-			}
467
-
468
-			if ($shouldEncrypt === true && $encryptionModule !== null) {
469
-				$this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
470
-				$headerSize = $this->getHeaderSize($path);
471
-				$source = $this->storage->fopen($path, $mode);
472
-				if (!is_resource($source)) {
473
-					return false;
474
-				}
475
-				$handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
476
-					$this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
477
-					$size, $unencryptedSize, $headerSize, $signed);
478
-
479
-				return $handle;
480
-			}
481
-		}
482
-
483
-		return $this->storage->fopen($path, $mode);
484
-	}
485
-
486
-
487
-	/**
488
-	 * perform some plausibility checks if the the unencrypted size is correct.
489
-	 * If not, we calculate the correct unencrypted size and return it
490
-	 *
491
-	 * @param string $path internal path relative to the storage root
492
-	 * @param int $unencryptedSize size of the unencrypted file
493
-	 *
494
-	 * @return int unencrypted size
495
-	 */
496
-	protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
497
-		$size = $this->storage->filesize($path);
498
-		$result = $unencryptedSize;
499
-
500
-		if ($unencryptedSize < 0 ||
501
-			($size > 0 && $unencryptedSize === $size) ||
502
-			$unencryptedSize > $size
503
-		) {
504
-			// check if we already calculate the unencrypted size for the
505
-			// given path to avoid recursions
506
-			if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
507
-				$this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
508
-				try {
509
-					$result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
510
-				} catch (\Exception $e) {
511
-					$this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
512
-				}
513
-				unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
514
-			}
515
-		}
516
-
517
-		return $result;
518
-	}
519
-
520
-	/**
521
-	 * calculate the unencrypted size
522
-	 *
523
-	 * @param string $path internal path relative to the storage root
524
-	 * @param int $size size of the physical file
525
-	 * @param int $unencryptedSize size of the unencrypted file
526
-	 *
527
-	 * @return int calculated unencrypted size
528
-	 */
529
-	protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
530
-		$headerSize = $this->getHeaderSize($path);
531
-		$header = $this->getHeader($path);
532
-		$encryptionModule = $this->getEncryptionModule($path);
533
-
534
-		$stream = $this->storage->fopen($path, 'r');
535
-
536
-		// if we couldn't open the file we return the old unencrypted size
537
-		if (!is_resource($stream)) {
538
-			$this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
539
-			return $unencryptedSize;
540
-		}
541
-
542
-		$newUnencryptedSize = 0;
543
-		$size -= $headerSize;
544
-		$blockSize = $this->util->getBlockSize();
545
-
546
-		// if a header exists we skip it
547
-		if ($headerSize > 0) {
548
-			$this->fread_block($stream, $headerSize);
549
-		}
550
-
551
-		// fast path, else the calculation for $lastChunkNr is bogus
552
-		if ($size === 0) {
553
-			return 0;
554
-		}
555
-
556
-		$signed = isset($header['signed']) && $header['signed'] === 'true';
557
-		$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
558
-
559
-		// calculate last chunk nr
560
-		// next highest is end of chunks, one subtracted is last one
561
-		// we have to read the last chunk, we can't just calculate it (because of padding etc)
562
-
563
-		$lastChunkNr = ceil($size / $blockSize) - 1;
564
-		// calculate last chunk position
565
-		$lastChunkPos = ($lastChunkNr * $blockSize);
566
-		// try to fseek to the last chunk, if it fails we have to read the whole file
567
-		if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
568
-			$newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
569
-		}
570
-
571
-		$lastChunkContentEncrypted = '';
572
-		$count = $blockSize;
573
-
574
-		while ($count > 0) {
575
-			$data = $this->fread_block($stream, $blockSize);
576
-			$count = strlen($data);
577
-			$lastChunkContentEncrypted .= $data;
578
-			if (strlen($lastChunkContentEncrypted) > $blockSize) {
579
-				$newUnencryptedSize += $unencryptedBlockSize;
580
-				$lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
581
-			}
582
-		}
583
-
584
-		fclose($stream);
585
-
586
-		// we have to decrypt the last chunk to get it actual size
587
-		$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
588
-		$decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
589
-		$decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
590
-
591
-		// calc the real file size with the size of the last chunk
592
-		$newUnencryptedSize += strlen($decryptedLastChunk);
593
-
594
-		$this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
595
-
596
-		// write to cache if applicable
597
-		$cache = $this->storage->getCache();
598
-		if ($cache) {
599
-			$entry = $cache->get($path);
600
-			$cache->update($entry['fileid'], [
601
-				'unencrypted_size' => $newUnencryptedSize
602
-			]);
603
-		}
604
-
605
-		return $newUnencryptedSize;
606
-	}
607
-
608
-	/**
609
-	 * fread_block
610
-	 *
611
-	 * This function is a wrapper around the fread function.  It is based on the
612
-	 * stream_read_block function from lib/private/Files/Streams/Encryption.php
613
-	 * It calls stream read until the requested $blockSize was received or no remaining data is present.
614
-	 * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
615
-	 * remote storage over the internet and it does not care about the given $blockSize.
616
-	 *
617
-	 * @param handle the stream to read from
618
-	 * @param int $blockSize Length of requested data block in bytes
619
-	 * @return string Data fetched from stream.
620
-	 */
621
-	private function fread_block($handle, int $blockSize): string {
622
-		$remaining = $blockSize;
623
-		$data = '';
624
-
625
-		do {
626
-			$chunk = fread($handle, $remaining);
627
-			$chunk_len = strlen($chunk);
628
-			$data .= $chunk;
629
-			$remaining -= $chunk_len;
630
-		} while (($remaining > 0) && ($chunk_len > 0));
631
-
632
-		return $data;
633
-	}
634
-
635
-	/**
636
-	 * @param Storage\IStorage $sourceStorage
637
-	 * @param string $sourceInternalPath
638
-	 * @param string $targetInternalPath
639
-	 * @param bool $preserveMtime
640
-	 * @return bool
641
-	 */
642
-	public function moveFromStorage(
643
-		Storage\IStorage $sourceStorage,
644
-		$sourceInternalPath,
645
-		$targetInternalPath,
646
-		$preserveMtime = true
647
-	) {
648
-		if ($sourceStorage === $this) {
649
-			return $this->rename($sourceInternalPath, $targetInternalPath);
650
-		}
651
-
652
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
653
-		// - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
654
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
655
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
656
-		// - remove $this->copyBetweenStorage
657
-
658
-		if (!$sourceStorage->isDeletable($sourceInternalPath)) {
659
-			return false;
660
-		}
661
-
662
-		$result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
663
-		if ($result) {
664
-			if ($sourceStorage->is_dir($sourceInternalPath)) {
665
-				$result &= $sourceStorage->rmdir($sourceInternalPath);
666
-			} else {
667
-				$result &= $sourceStorage->unlink($sourceInternalPath);
668
-			}
669
-		}
670
-		return $result;
671
-	}
672
-
673
-
674
-	/**
675
-	 * @param Storage\IStorage $sourceStorage
676
-	 * @param string $sourceInternalPath
677
-	 * @param string $targetInternalPath
678
-	 * @param bool $preserveMtime
679
-	 * @param bool $isRename
680
-	 * @return bool
681
-	 */
682
-	public function copyFromStorage(
683
-		Storage\IStorage $sourceStorage,
684
-		$sourceInternalPath,
685
-		$targetInternalPath,
686
-		$preserveMtime = false,
687
-		$isRename = false
688
-	) {
689
-		// TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
690
-		// - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
691
-		// - copy the file cache update from  $this->copyBetweenStorage to this method
692
-		// - copy the copyKeys() call from  $this->copyBetweenStorage to this method
693
-		// - remove $this->copyBetweenStorage
694
-
695
-		return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
696
-	}
697
-
698
-	/**
699
-	 * Update the encrypted cache version in the database
700
-	 *
701
-	 * @param Storage\IStorage $sourceStorage
702
-	 * @param string $sourceInternalPath
703
-	 * @param string $targetInternalPath
704
-	 * @param bool $isRename
705
-	 * @param bool $keepEncryptionVersion
706
-	 */
707
-	private function updateEncryptedVersion(
708
-		Storage\IStorage $sourceStorage,
709
-		$sourceInternalPath,
710
-		$targetInternalPath,
711
-		$isRename,
712
-		$keepEncryptionVersion
713
-	) {
714
-		$isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
715
-		$cacheInformation = [
716
-			'encrypted' => $isEncrypted,
717
-		];
718
-		if ($isEncrypted) {
719
-			$sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
720
-			$targetCacheEntry = $this->getCache()->get($targetInternalPath);
721
-
722
-			// Rename of the cache already happened, so we do the cleanup on the target
723
-			if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
724
-				$encryptedVersion = $targetCacheEntry['encryptedVersion'];
725
-				$isRename = false;
726
-			} else {
727
-				$encryptedVersion = $sourceCacheEntry['encryptedVersion'];
728
-			}
729
-
730
-			// In case of a move operation from an unencrypted to an encrypted
731
-			// storage the old encrypted version would stay with "0" while the
732
-			// correct value would be "1". Thus we manually set the value to "1"
733
-			// for those cases.
734
-			// See also https://github.com/owncloud/core/issues/23078
735
-			if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
736
-				$encryptedVersion = 1;
737
-			}
738
-
739
-			$cacheInformation['encryptedVersion'] = $encryptedVersion;
740
-		}
741
-
742
-		// in case of a rename we need to manipulate the source cache because
743
-		// this information will be kept for the new target
744
-		if ($isRename) {
745
-			$sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
746
-		} else {
747
-			$this->getCache()->put($targetInternalPath, $cacheInformation);
748
-		}
749
-	}
750
-
751
-	/**
752
-	 * copy file between two storages
753
-	 *
754
-	 * @param Storage\IStorage $sourceStorage
755
-	 * @param string $sourceInternalPath
756
-	 * @param string $targetInternalPath
757
-	 * @param bool $preserveMtime
758
-	 * @param bool $isRename
759
-	 * @return bool
760
-	 * @throws \Exception
761
-	 */
762
-	private function copyBetweenStorage(
763
-		Storage\IStorage $sourceStorage,
764
-		$sourceInternalPath,
765
-		$targetInternalPath,
766
-		$preserveMtime,
767
-		$isRename
768
-	) {
769
-		// for versions we have nothing to do, because versions should always use the
770
-		// key from the original file. Just create a 1:1 copy and done
771
-		if ($this->isVersion($targetInternalPath) ||
772
-			$this->isVersion($sourceInternalPath)) {
773
-			// remember that we try to create a version so that we can detect it during
774
-			// fopen($sourceInternalPath) and by-pass the encryption in order to
775
-			// create a 1:1 copy of the file
776
-			$this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
777
-			$result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
778
-			$this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
779
-			if ($result) {
780
-				$info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
781
-				// make sure that we update the unencrypted size for the version
782
-				if (isset($info['encrypted']) && $info['encrypted'] === true) {
783
-					$this->updateUnencryptedSize(
784
-						$this->getFullPath($targetInternalPath),
785
-						$info->getUnencryptedSize()
786
-					);
787
-				}
788
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
789
-			}
790
-			return $result;
791
-		}
792
-
793
-		// first copy the keys that we reuse the existing file key on the target location
794
-		// and don't create a new one which would break versions for example.
795
-		$mount = $this->mountManager->findByStorageId($sourceStorage->getId());
796
-		if (count($mount) === 1) {
797
-			$mountPoint = $mount[0]->getMountPoint();
798
-			$source = $mountPoint . '/' . $sourceInternalPath;
799
-			$target = $this->getFullPath($targetInternalPath);
800
-			$this->copyKeys($source, $target);
801
-		} else {
802
-			$this->logger->error('Could not find mount point, can\'t keep encryption keys');
803
-		}
804
-
805
-		if ($sourceStorage->is_dir($sourceInternalPath)) {
806
-			$dh = $sourceStorage->opendir($sourceInternalPath);
807
-			if (!$this->is_dir($targetInternalPath)) {
808
-				$result = $this->mkdir($targetInternalPath);
809
-			} else {
810
-				$result = true;
811
-			}
812
-			if (is_resource($dh)) {
813
-				while ($result and ($file = readdir($dh)) !== false) {
814
-					if (!Filesystem::isIgnoredDir($file)) {
815
-						$result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
816
-					}
817
-				}
818
-			}
819
-		} else {
820
-			try {
821
-				$source = $sourceStorage->fopen($sourceInternalPath, 'r');
822
-				$target = $this->fopen($targetInternalPath, 'w');
823
-				[, $result] = \OC_Helper::streamCopy($source, $target);
824
-			} finally {
825
-				if (is_resource($source)) {
826
-					fclose($source);
827
-				}
828
-				if (is_resource($target)) {
829
-					fclose($target);
830
-				}
831
-			}
832
-			if ($result) {
833
-				if ($preserveMtime) {
834
-					$this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
835
-				}
836
-				$this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
837
-			} else {
838
-				// delete partially written target file
839
-				$this->unlink($targetInternalPath);
840
-				// delete cache entry that was created by fopen
841
-				$this->getCache()->remove($targetInternalPath);
842
-			}
843
-		}
844
-		return (bool)$result;
845
-	}
846
-
847
-	public function getLocalFile($path) {
848
-		if ($this->encryptionManager->isEnabled()) {
849
-			$cachedFile = $this->getCachedFile($path);
850
-			if (is_string($cachedFile)) {
851
-				return $cachedFile;
852
-			}
853
-		}
854
-		return $this->storage->getLocalFile($path);
855
-	}
856
-
857
-	public function isLocal() {
858
-		if ($this->encryptionManager->isEnabled()) {
859
-			return false;
860
-		}
861
-		return $this->storage->isLocal();
862
-	}
863
-
864
-	public function stat($path) {
865
-		$stat = $this->storage->stat($path);
866
-		if (!$stat) {
867
-			return false;
868
-		}
869
-		$fileSize = $this->filesize($path);
870
-		$stat['size'] = $fileSize;
871
-		$stat[7] = $fileSize;
872
-		$stat['hasHeader'] = $this->getHeaderSize($path) > 0;
873
-		return $stat;
874
-	}
875
-
876
-	public function hash($type, $path, $raw = false) {
877
-		$fh = $this->fopen($path, 'rb');
878
-		$ctx = hash_init($type);
879
-		hash_update_stream($ctx, $fh);
880
-		fclose($fh);
881
-		return hash_final($ctx, $raw);
882
-	}
883
-
884
-	/**
885
-	 * return full path, including mount point
886
-	 *
887
-	 * @param string $path relative to mount point
888
-	 * @return string full path including mount point
889
-	 */
890
-	protected function getFullPath($path) {
891
-		return Filesystem::normalizePath($this->mountPoint . '/' . $path);
892
-	}
893
-
894
-	/**
895
-	 * read first block of encrypted file, typically this will contain the
896
-	 * encryption header
897
-	 *
898
-	 * @param string $path
899
-	 * @return string
900
-	 */
901
-	protected function readFirstBlock($path) {
902
-		$firstBlock = '';
903
-		if ($this->storage->is_file($path)) {
904
-			$handle = $this->storage->fopen($path, 'r');
905
-			$firstBlock = fread($handle, $this->util->getHeaderSize());
906
-			fclose($handle);
907
-		}
908
-		return $firstBlock;
909
-	}
910
-
911
-	/**
912
-	 * return header size of given file
913
-	 *
914
-	 * @param string $path
915
-	 * @return int
916
-	 */
917
-	protected function getHeaderSize($path) {
918
-		$headerSize = 0;
919
-		$realFile = $this->util->stripPartialFileExtension($path);
920
-		if ($this->storage->is_file($realFile)) {
921
-			$path = $realFile;
922
-		}
923
-		$firstBlock = $this->readFirstBlock($path);
924
-
925
-		if (str_starts_with($firstBlock, Util::HEADER_START)) {
926
-			$headerSize = $this->util->getHeaderSize();
927
-		}
928
-
929
-		return $headerSize;
930
-	}
931
-
932
-	/**
933
-	 * parse raw header to array
934
-	 *
935
-	 * @param string $rawHeader
936
-	 * @return array
937
-	 */
938
-	protected function parseRawHeader($rawHeader) {
939
-		$result = [];
940
-		if (str_starts_with($rawHeader, Util::HEADER_START)) {
941
-			$header = $rawHeader;
942
-			$endAt = strpos($header, Util::HEADER_END);
943
-			if ($endAt !== false) {
944
-				$header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
945
-
946
-				// +1 to not start with an ':' which would result in empty element at the beginning
947
-				$exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
948
-
949
-				$element = array_shift($exploded);
950
-				while ($element !== Util::HEADER_END) {
951
-					$result[$element] = array_shift($exploded);
952
-					$element = array_shift($exploded);
953
-				}
954
-			}
955
-		}
956
-
957
-		return $result;
958
-	}
959
-
960
-	/**
961
-	 * read header from file
962
-	 *
963
-	 * @param string $path
964
-	 * @return array
965
-	 */
966
-	protected function getHeader($path) {
967
-		$realFile = $this->util->stripPartialFileExtension($path);
968
-		$exists = $this->storage->is_file($realFile);
969
-		if ($exists) {
970
-			$path = $realFile;
971
-		}
972
-
973
-		$result = [];
974
-
975
-		$isEncrypted = $this->encryptedPaths->get($realFile);
976
-		if (is_null($isEncrypted)) {
977
-			$info = $this->getCache()->get($path);
978
-			$isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
979
-		}
980
-
981
-		if ($isEncrypted) {
982
-			$firstBlock = $this->readFirstBlock($path);
983
-			$result = $this->parseRawHeader($firstBlock);
984
-
985
-			// if the header doesn't contain a encryption module we check if it is a
986
-			// legacy file. If true, we add the default encryption module
987
-			if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
988
-				$result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
989
-			}
990
-		}
991
-
992
-		return $result;
993
-	}
994
-
995
-	/**
996
-	 * read encryption module needed to read/write the file located at $path
997
-	 *
998
-	 * @param string $path
999
-	 * @return null|\OCP\Encryption\IEncryptionModule
1000
-	 * @throws ModuleDoesNotExistsException
1001
-	 * @throws \Exception
1002
-	 */
1003
-	protected function getEncryptionModule($path) {
1004
-		$encryptionModule = null;
1005
-		$header = $this->getHeader($path);
1006
-		$encryptionModuleId = $this->util->getEncryptionModuleId($header);
1007
-		if (!empty($encryptionModuleId)) {
1008
-			try {
1009
-				$encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1010
-			} catch (ModuleDoesNotExistsException $e) {
1011
-				$this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1012
-				throw $e;
1013
-			}
1014
-		}
1015
-
1016
-		return $encryptionModule;
1017
-	}
1018
-
1019
-	/**
1020
-	 * @param string $path
1021
-	 * @param int $unencryptedSize
1022
-	 */
1023
-	public function updateUnencryptedSize($path, $unencryptedSize) {
1024
-		$this->unencryptedSize[$path] = $unencryptedSize;
1025
-	}
1026
-
1027
-	/**
1028
-	 * copy keys to new location
1029
-	 *
1030
-	 * @param string $source path relative to data/
1031
-	 * @param string $target path relative to data/
1032
-	 * @return bool
1033
-	 */
1034
-	protected function copyKeys($source, $target) {
1035
-		if (!$this->util->isExcluded($source)) {
1036
-			return $this->keyStorage->copyKeys($source, $target);
1037
-		}
1038
-
1039
-		return false;
1040
-	}
1041
-
1042
-	/**
1043
-	 * check if path points to a files version
1044
-	 *
1045
-	 * @param $path
1046
-	 * @return bool
1047
-	 */
1048
-	protected function isVersion($path) {
1049
-		$normalized = Filesystem::normalizePath($path);
1050
-		return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1051
-	}
1052
-
1053
-	/**
1054
-	 * check if the given storage should be encrypted or not
1055
-	 *
1056
-	 * @param $path
1057
-	 * @return bool
1058
-	 */
1059
-	protected function shouldEncrypt($path) {
1060
-		$fullPath = $this->getFullPath($path);
1061
-		$mountPointConfig = $this->mount->getOption('encrypt', true);
1062
-		if ($mountPointConfig === false) {
1063
-			return false;
1064
-		}
1065
-
1066
-		try {
1067
-			$encryptionModule = $this->getEncryptionModule($fullPath);
1068
-		} catch (ModuleDoesNotExistsException $e) {
1069
-			return false;
1070
-		}
1071
-
1072
-		if ($encryptionModule === null) {
1073
-			$encryptionModule = $this->encryptionManager->getEncryptionModule();
1074
-		}
1075
-
1076
-		return $encryptionModule->shouldEncrypt($fullPath);
1077
-	}
1078
-
1079
-	public function writeStream(string $path, $stream, int $size = null): int {
1080
-		// always fall back to fopen
1081
-		$target = $this->fopen($path, 'w');
1082
-		[$count, $result] = \OC_Helper::streamCopy($stream, $target);
1083
-		fclose($stream);
1084
-		fclose($target);
1085
-
1086
-		// object store, stores the size after write and doesn't update this during scan
1087
-		// manually store the unencrypted size
1088
-		if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
1089
-			$this->getCache()->put($path, ['unencrypted_size' => $count]);
1090
-		}
1091
-
1092
-		return $count;
1093
-	}
1094
-
1095
-	public function clearIsEncryptedCache(): void {
1096
-		$this->encryptedPaths->clear();
1097
-	}
59
+    use LocalTempFileTrait;
60
+
61
+    /** @var string */
62
+    private $mountPoint;
63
+
64
+    /** @var \OC\Encryption\Util */
65
+    private $util;
66
+
67
+    /** @var \OCP\Encryption\IManager */
68
+    private $encryptionManager;
69
+
70
+    private LoggerInterface $logger;
71
+
72
+    /** @var string */
73
+    private $uid;
74
+
75
+    /** @var array */
76
+    protected $unencryptedSize;
77
+
78
+    /** @var \OCP\Encryption\IFile */
79
+    private $fileHelper;
80
+
81
+    /** @var IMountPoint */
82
+    private $mount;
83
+
84
+    /** @var IStorage */
85
+    private $keyStorage;
86
+
87
+    /** @var Update */
88
+    private $update;
89
+
90
+    /** @var Manager */
91
+    private $mountManager;
92
+
93
+    /** @var array remember for which path we execute the repair step to avoid recursions */
94
+    private $fixUnencryptedSizeOf = [];
95
+
96
+    /** @var  ArrayCache */
97
+    private $arrayCache;
98
+
99
+    /** @var CappedMemoryCache<bool> */
100
+    private CappedMemoryCache $encryptedPaths;
101
+
102
+    /**
103
+     * @param array $parameters
104
+     */
105
+    public function __construct(
106
+        $parameters,
107
+        IManager $encryptionManager = null,
108
+        Util $util = null,
109
+        LoggerInterface $logger = null,
110
+        IFile $fileHelper = null,
111
+        $uid = null,
112
+        IStorage $keyStorage = null,
113
+        Update $update = null,
114
+        Manager $mountManager = null,
115
+        ArrayCache $arrayCache = null
116
+    ) {
117
+        $this->mountPoint = $parameters['mountPoint'];
118
+        $this->mount = $parameters['mount'];
119
+        $this->encryptionManager = $encryptionManager;
120
+        $this->util = $util;
121
+        $this->logger = $logger;
122
+        $this->uid = $uid;
123
+        $this->fileHelper = $fileHelper;
124
+        $this->keyStorage = $keyStorage;
125
+        $this->unencryptedSize = [];
126
+        $this->update = $update;
127
+        $this->mountManager = $mountManager;
128
+        $this->arrayCache = $arrayCache;
129
+        $this->encryptedPaths = new CappedMemoryCache();
130
+        parent::__construct($parameters);
131
+    }
132
+
133
+    /**
134
+     * see https://www.php.net/manual/en/function.filesize.php
135
+     * The result for filesize when called on a folder is required to be 0
136
+     */
137
+    public function filesize($path): false|int|float {
138
+        $fullPath = $this->getFullPath($path);
139
+
140
+        $info = $this->getCache()->get($path);
141
+        if ($info === false) {
142
+            return false;
143
+        }
144
+        if (isset($this->unencryptedSize[$fullPath])) {
145
+            $size = $this->unencryptedSize[$fullPath];
146
+            // update file cache
147
+            if ($info instanceof ICacheEntry) {
148
+                $info['encrypted'] = $info['encryptedVersion'];
149
+            } else {
150
+                if (!is_array($info)) {
151
+                    $info = [];
152
+                }
153
+                $info['encrypted'] = true;
154
+                $info = new CacheEntry($info);
155
+            }
156
+
157
+            if ($size !== $info->getUnencryptedSize()) {
158
+                $this->getCache()->update($info->getId(), [
159
+                    'unencrypted_size' => $size
160
+                ]);
161
+            }
162
+
163
+            return $size;
164
+        }
165
+
166
+        if (isset($info['fileid']) && $info['encrypted']) {
167
+            return $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
168
+        }
169
+
170
+        return $this->storage->filesize($path);
171
+    }
172
+
173
+    /**
174
+     * @param string $path
175
+     * @param array $data
176
+     * @return array
177
+     */
178
+    private function modifyMetaData(string $path, array $data): array {
179
+        $fullPath = $this->getFullPath($path);
180
+        $info = $this->getCache()->get($path);
181
+
182
+        if (isset($this->unencryptedSize[$fullPath])) {
183
+            $data['encrypted'] = true;
184
+            $data['size'] = $this->unencryptedSize[$fullPath];
185
+            $data['unencrypted_size'] = $data['size'];
186
+        } else {
187
+            if (isset($info['fileid']) && $info['encrypted']) {
188
+                $data['size'] = $this->verifyUnencryptedSize($path, $info->getUnencryptedSize());
189
+                $data['encrypted'] = true;
190
+                $data['unencrypted_size'] = $data['size'];
191
+            }
192
+        }
193
+
194
+        if (isset($info['encryptedVersion']) && $info['encryptedVersion'] > 1) {
195
+            $data['encryptedVersion'] = $info['encryptedVersion'];
196
+        }
197
+
198
+        return $data;
199
+    }
200
+
201
+    public function getMetaData($path) {
202
+        $data = $this->storage->getMetaData($path);
203
+        if (is_null($data)) {
204
+            return null;
205
+        }
206
+        return $this->modifyMetaData($path, $data);
207
+    }
208
+
209
+    public function getDirectoryContent($directory): \Traversable {
210
+        $parent = rtrim($directory, '/');
211
+        foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
212
+            yield $this->modifyMetaData($parent . '/' . $data['name'], $data);
213
+        }
214
+    }
215
+
216
+    /**
217
+     * see https://www.php.net/manual/en/function.file_get_contents.php
218
+     *
219
+     * @param string $path
220
+     * @return string|false
221
+     */
222
+    public function file_get_contents($path) {
223
+        $encryptionModule = $this->getEncryptionModule($path);
224
+
225
+        if ($encryptionModule) {
226
+            $handle = $this->fopen($path, "r");
227
+            if (!$handle) {
228
+                return false;
229
+            }
230
+            $data = stream_get_contents($handle);
231
+            fclose($handle);
232
+            return $data;
233
+        }
234
+        return $this->storage->file_get_contents($path);
235
+    }
236
+
237
+    /**
238
+     * see https://www.php.net/manual/en/function.file_put_contents.php
239
+     *
240
+     * @param string $path
241
+     * @param mixed $data
242
+     * @return int|false
243
+     */
244
+    public function file_put_contents($path, $data) {
245
+        // file put content will always be translated to a stream write
246
+        $handle = $this->fopen($path, 'w');
247
+        if (is_resource($handle)) {
248
+            $written = fwrite($handle, $data);
249
+            fclose($handle);
250
+            return $written;
251
+        }
252
+
253
+        return false;
254
+    }
255
+
256
+    /**
257
+     * see https://www.php.net/manual/en/function.unlink.php
258
+     *
259
+     * @param string $path
260
+     * @return bool
261
+     */
262
+    public function unlink($path) {
263
+        $fullPath = $this->getFullPath($path);
264
+        if ($this->util->isExcluded($fullPath)) {
265
+            return $this->storage->unlink($path);
266
+        }
267
+
268
+        $encryptionModule = $this->getEncryptionModule($path);
269
+        if ($encryptionModule) {
270
+            $this->keyStorage->deleteAllFileKeys($fullPath);
271
+        }
272
+
273
+        return $this->storage->unlink($path);
274
+    }
275
+
276
+    /**
277
+     * see https://www.php.net/manual/en/function.rename.php
278
+     *
279
+     * @param string $source
280
+     * @param string $target
281
+     * @return bool
282
+     */
283
+    public function rename($source, $target) {
284
+        $result = $this->storage->rename($source, $target);
285
+
286
+        if ($result &&
287
+            // versions always use the keys from the original file, so we can skip
288
+            // this step for versions
289
+            $this->isVersion($target) === false &&
290
+            $this->encryptionManager->isEnabled()) {
291
+            $sourcePath = $this->getFullPath($source);
292
+            if (!$this->util->isExcluded($sourcePath)) {
293
+                $targetPath = $this->getFullPath($target);
294
+                if (isset($this->unencryptedSize[$sourcePath])) {
295
+                    $this->unencryptedSize[$targetPath] = $this->unencryptedSize[$sourcePath];
296
+                }
297
+                $this->keyStorage->renameKeys($sourcePath, $targetPath);
298
+                $module = $this->getEncryptionModule($target);
299
+                if ($module) {
300
+                    $module->update($targetPath, $this->uid, []);
301
+                }
302
+            }
303
+        }
304
+
305
+        return $result;
306
+    }
307
+
308
+    /**
309
+     * see https://www.php.net/manual/en/function.rmdir.php
310
+     *
311
+     * @param string $path
312
+     * @return bool
313
+     */
314
+    public function rmdir($path) {
315
+        $result = $this->storage->rmdir($path);
316
+        $fullPath = $this->getFullPath($path);
317
+        if ($result &&
318
+            $this->util->isExcluded($fullPath) === false &&
319
+            $this->encryptionManager->isEnabled()
320
+        ) {
321
+            $this->keyStorage->deleteAllFileKeys($fullPath);
322
+        }
323
+
324
+        return $result;
325
+    }
326
+
327
+    /**
328
+     * check if a file can be read
329
+     *
330
+     * @param string $path
331
+     * @return bool
332
+     */
333
+    public function isReadable($path) {
334
+        $isReadable = true;
335
+
336
+        $metaData = $this->getMetaData($path);
337
+        if (
338
+            !$this->is_dir($path) &&
339
+            isset($metaData['encrypted']) &&
340
+            $metaData['encrypted'] === true
341
+        ) {
342
+            $fullPath = $this->getFullPath($path);
343
+            $module = $this->getEncryptionModule($path);
344
+            $isReadable = $module->isReadable($fullPath, $this->uid);
345
+        }
346
+
347
+        return $this->storage->isReadable($path) && $isReadable;
348
+    }
349
+
350
+    /**
351
+     * see https://www.php.net/manual/en/function.copy.php
352
+     *
353
+     * @param string $source
354
+     * @param string $target
355
+     */
356
+    public function copy($source, $target): bool {
357
+        $sourcePath = $this->getFullPath($source);
358
+
359
+        if ($this->util->isExcluded($sourcePath)) {
360
+            return $this->storage->copy($source, $target);
361
+        }
362
+
363
+        // need to stream copy file by file in case we copy between a encrypted
364
+        // and a unencrypted storage
365
+        $this->unlink($target);
366
+        return $this->copyFromStorage($this, $source, $target);
367
+    }
368
+
369
+    /**
370
+     * see https://www.php.net/manual/en/function.fopen.php
371
+     *
372
+     * @param string $path
373
+     * @param string $mode
374
+     * @return resource|bool
375
+     * @throws GenericEncryptionException
376
+     * @throws ModuleDoesNotExistsException
377
+     */
378
+    public function fopen($path, $mode) {
379
+        // check if the file is stored in the array cache, this means that we
380
+        // copy a file over to the versions folder, in this case we don't want to
381
+        // decrypt it
382
+        if ($this->arrayCache->hasKey('encryption_copy_version_' . $path)) {
383
+            $this->arrayCache->remove('encryption_copy_version_' . $path);
384
+            return $this->storage->fopen($path, $mode);
385
+        }
386
+
387
+        $encryptionEnabled = $this->encryptionManager->isEnabled();
388
+        $shouldEncrypt = false;
389
+        $encryptionModule = null;
390
+        $header = $this->getHeader($path);
391
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
392
+        $fullPath = $this->getFullPath($path);
393
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
394
+
395
+        if ($this->util->isExcluded($fullPath) === false) {
396
+            $size = $unencryptedSize = 0;
397
+            $realFile = $this->util->stripPartialFileExtension($path);
398
+            $targetExists = $this->is_file($realFile) || $this->file_exists($path);
399
+            $targetIsEncrypted = false;
400
+            if ($targetExists) {
401
+                // in case the file exists we require the explicit module as
402
+                // specified in the file header - otherwise we need to fail hard to
403
+                // prevent data loss on client side
404
+                if (!empty($encryptionModuleId)) {
405
+                    $targetIsEncrypted = true;
406
+                    $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
407
+                }
408
+
409
+                if ($this->file_exists($path)) {
410
+                    $size = $this->storage->filesize($path);
411
+                    $unencryptedSize = $this->filesize($path);
412
+                } else {
413
+                    $size = $unencryptedSize = 0;
414
+                }
415
+            }
416
+
417
+            try {
418
+                if (
419
+                    $mode === 'w'
420
+                    || $mode === 'w+'
421
+                    || $mode === 'wb'
422
+                    || $mode === 'wb+'
423
+                ) {
424
+                    // if we update a encrypted file with a un-encrypted one we change the db flag
425
+                    if ($targetIsEncrypted && $encryptionEnabled === false) {
426
+                        $cache = $this->storage->getCache();
427
+                        if ($cache) {
428
+                            $entry = $cache->get($path);
429
+                            $cache->update($entry->getId(), ['encrypted' => 0]);
430
+                        }
431
+                    }
432
+                    if ($encryptionEnabled) {
433
+                        // if $encryptionModuleId is empty, the default module will be used
434
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
435
+                        $shouldEncrypt = $encryptionModule->shouldEncrypt($fullPath);
436
+                        $signed = true;
437
+                    }
438
+                } else {
439
+                    $info = $this->getCache()->get($path);
440
+                    // only get encryption module if we found one in the header
441
+                    // or if file should be encrypted according to the file cache
442
+                    if (!empty($encryptionModuleId)) {
443
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
444
+                        $shouldEncrypt = true;
445
+                    } elseif (empty($encryptionModuleId) && $info['encrypted'] === true) {
446
+                        // we come from a old installation. No header and/or no module defined
447
+                        // but the file is encrypted. In this case we need to use the
448
+                        // OC_DEFAULT_MODULE to read the file
449
+                        $encryptionModule = $this->encryptionManager->getEncryptionModule('OC_DEFAULT_MODULE');
450
+                        $shouldEncrypt = true;
451
+                        $targetIsEncrypted = true;
452
+                    }
453
+                }
454
+            } catch (ModuleDoesNotExistsException $e) {
455
+                $this->logger->warning('Encryption module "' . $encryptionModuleId . '" not found, file will be stored unencrypted', [
456
+                    'exception' => $e,
457
+                    'app' => 'core',
458
+                ]);
459
+            }
460
+
461
+            // encryption disabled on write of new file and write to existing unencrypted file -> don't encrypt
462
+            if (!$encryptionEnabled || !$this->shouldEncrypt($path)) {
463
+                if (!$targetExists || !$targetIsEncrypted) {
464
+                    $shouldEncrypt = false;
465
+                }
466
+            }
467
+
468
+            if ($shouldEncrypt === true && $encryptionModule !== null) {
469
+                $this->encryptedPaths->set($this->util->stripPartialFileExtension($path), true);
470
+                $headerSize = $this->getHeaderSize($path);
471
+                $source = $this->storage->fopen($path, $mode);
472
+                if (!is_resource($source)) {
473
+                    return false;
474
+                }
475
+                $handle = \OC\Files\Stream\Encryption::wrap($source, $path, $fullPath, $header,
476
+                    $this->uid, $encryptionModule, $this->storage, $this, $this->util, $this->fileHelper, $mode,
477
+                    $size, $unencryptedSize, $headerSize, $signed);
478
+
479
+                return $handle;
480
+            }
481
+        }
482
+
483
+        return $this->storage->fopen($path, $mode);
484
+    }
485
+
486
+
487
+    /**
488
+     * perform some plausibility checks if the the unencrypted size is correct.
489
+     * If not, we calculate the correct unencrypted size and return it
490
+     *
491
+     * @param string $path internal path relative to the storage root
492
+     * @param int $unencryptedSize size of the unencrypted file
493
+     *
494
+     * @return int unencrypted size
495
+     */
496
+    protected function verifyUnencryptedSize(string $path, int $unencryptedSize): int {
497
+        $size = $this->storage->filesize($path);
498
+        $result = $unencryptedSize;
499
+
500
+        if ($unencryptedSize < 0 ||
501
+            ($size > 0 && $unencryptedSize === $size) ||
502
+            $unencryptedSize > $size
503
+        ) {
504
+            // check if we already calculate the unencrypted size for the
505
+            // given path to avoid recursions
506
+            if (isset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]) === false) {
507
+                $this->fixUnencryptedSizeOf[$this->getFullPath($path)] = true;
508
+                try {
509
+                    $result = $this->fixUnencryptedSize($path, $size, $unencryptedSize);
510
+                } catch (\Exception $e) {
511
+                    $this->logger->error('Couldn\'t re-calculate unencrypted size for ' . $path, ['exception' => $e]);
512
+                }
513
+                unset($this->fixUnencryptedSizeOf[$this->getFullPath($path)]);
514
+            }
515
+        }
516
+
517
+        return $result;
518
+    }
519
+
520
+    /**
521
+     * calculate the unencrypted size
522
+     *
523
+     * @param string $path internal path relative to the storage root
524
+     * @param int $size size of the physical file
525
+     * @param int $unencryptedSize size of the unencrypted file
526
+     *
527
+     * @return int calculated unencrypted size
528
+     */
529
+    protected function fixUnencryptedSize(string $path, int $size, int $unencryptedSize): int {
530
+        $headerSize = $this->getHeaderSize($path);
531
+        $header = $this->getHeader($path);
532
+        $encryptionModule = $this->getEncryptionModule($path);
533
+
534
+        $stream = $this->storage->fopen($path, 'r');
535
+
536
+        // if we couldn't open the file we return the old unencrypted size
537
+        if (!is_resource($stream)) {
538
+            $this->logger->error('Could not open ' . $path . '. Recalculation of unencrypted size aborted.');
539
+            return $unencryptedSize;
540
+        }
541
+
542
+        $newUnencryptedSize = 0;
543
+        $size -= $headerSize;
544
+        $blockSize = $this->util->getBlockSize();
545
+
546
+        // if a header exists we skip it
547
+        if ($headerSize > 0) {
548
+            $this->fread_block($stream, $headerSize);
549
+        }
550
+
551
+        // fast path, else the calculation for $lastChunkNr is bogus
552
+        if ($size === 0) {
553
+            return 0;
554
+        }
555
+
556
+        $signed = isset($header['signed']) && $header['signed'] === 'true';
557
+        $unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
558
+
559
+        // calculate last chunk nr
560
+        // next highest is end of chunks, one subtracted is last one
561
+        // we have to read the last chunk, we can't just calculate it (because of padding etc)
562
+
563
+        $lastChunkNr = ceil($size / $blockSize) - 1;
564
+        // calculate last chunk position
565
+        $lastChunkPos = ($lastChunkNr * $blockSize);
566
+        // try to fseek to the last chunk, if it fails we have to read the whole file
567
+        if (@fseek($stream, $lastChunkPos, SEEK_CUR) === 0) {
568
+            $newUnencryptedSize += $lastChunkNr * $unencryptedBlockSize;
569
+        }
570
+
571
+        $lastChunkContentEncrypted = '';
572
+        $count = $blockSize;
573
+
574
+        while ($count > 0) {
575
+            $data = $this->fread_block($stream, $blockSize);
576
+            $count = strlen($data);
577
+            $lastChunkContentEncrypted .= $data;
578
+            if (strlen($lastChunkContentEncrypted) > $blockSize) {
579
+                $newUnencryptedSize += $unencryptedBlockSize;
580
+                $lastChunkContentEncrypted = substr($lastChunkContentEncrypted, $blockSize);
581
+            }
582
+        }
583
+
584
+        fclose($stream);
585
+
586
+        // we have to decrypt the last chunk to get it actual size
587
+        $encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
588
+        $decryptedLastChunk = $encryptionModule->decrypt($lastChunkContentEncrypted, $lastChunkNr . 'end');
589
+        $decryptedLastChunk .= $encryptionModule->end($this->getFullPath($path), $lastChunkNr . 'end');
590
+
591
+        // calc the real file size with the size of the last chunk
592
+        $newUnencryptedSize += strlen($decryptedLastChunk);
593
+
594
+        $this->updateUnencryptedSize($this->getFullPath($path), $newUnencryptedSize);
595
+
596
+        // write to cache if applicable
597
+        $cache = $this->storage->getCache();
598
+        if ($cache) {
599
+            $entry = $cache->get($path);
600
+            $cache->update($entry['fileid'], [
601
+                'unencrypted_size' => $newUnencryptedSize
602
+            ]);
603
+        }
604
+
605
+        return $newUnencryptedSize;
606
+    }
607
+
608
+    /**
609
+     * fread_block
610
+     *
611
+     * This function is a wrapper around the fread function.  It is based on the
612
+     * stream_read_block function from lib/private/Files/Streams/Encryption.php
613
+     * It calls stream read until the requested $blockSize was received or no remaining data is present.
614
+     * This is required as stream_read only returns smaller chunks of data when the stream fetches from a
615
+     * remote storage over the internet and it does not care about the given $blockSize.
616
+     *
617
+     * @param handle the stream to read from
618
+     * @param int $blockSize Length of requested data block in bytes
619
+     * @return string Data fetched from stream.
620
+     */
621
+    private function fread_block($handle, int $blockSize): string {
622
+        $remaining = $blockSize;
623
+        $data = '';
624
+
625
+        do {
626
+            $chunk = fread($handle, $remaining);
627
+            $chunk_len = strlen($chunk);
628
+            $data .= $chunk;
629
+            $remaining -= $chunk_len;
630
+        } while (($remaining > 0) && ($chunk_len > 0));
631
+
632
+        return $data;
633
+    }
634
+
635
+    /**
636
+     * @param Storage\IStorage $sourceStorage
637
+     * @param string $sourceInternalPath
638
+     * @param string $targetInternalPath
639
+     * @param bool $preserveMtime
640
+     * @return bool
641
+     */
642
+    public function moveFromStorage(
643
+        Storage\IStorage $sourceStorage,
644
+        $sourceInternalPath,
645
+        $targetInternalPath,
646
+        $preserveMtime = true
647
+    ) {
648
+        if ($sourceStorage === $this) {
649
+            return $this->rename($sourceInternalPath, $targetInternalPath);
650
+        }
651
+
652
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
653
+        // - call $this->storage->moveFromStorage() instead of $this->copyBetweenStorage
654
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
655
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
656
+        // - remove $this->copyBetweenStorage
657
+
658
+        if (!$sourceStorage->isDeletable($sourceInternalPath)) {
659
+            return false;
660
+        }
661
+
662
+        $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true);
663
+        if ($result) {
664
+            if ($sourceStorage->is_dir($sourceInternalPath)) {
665
+                $result &= $sourceStorage->rmdir($sourceInternalPath);
666
+            } else {
667
+                $result &= $sourceStorage->unlink($sourceInternalPath);
668
+            }
669
+        }
670
+        return $result;
671
+    }
672
+
673
+
674
+    /**
675
+     * @param Storage\IStorage $sourceStorage
676
+     * @param string $sourceInternalPath
677
+     * @param string $targetInternalPath
678
+     * @param bool $preserveMtime
679
+     * @param bool $isRename
680
+     * @return bool
681
+     */
682
+    public function copyFromStorage(
683
+        Storage\IStorage $sourceStorage,
684
+        $sourceInternalPath,
685
+        $targetInternalPath,
686
+        $preserveMtime = false,
687
+        $isRename = false
688
+    ) {
689
+        // TODO clean this up once the underlying moveFromStorage in OC\Files\Storage\Wrapper\Common is fixed:
690
+        // - call $this->storage->copyFromStorage() instead of $this->copyBetweenStorage
691
+        // - copy the file cache update from  $this->copyBetweenStorage to this method
692
+        // - copy the copyKeys() call from  $this->copyBetweenStorage to this method
693
+        // - remove $this->copyBetweenStorage
694
+
695
+        return $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, $isRename);
696
+    }
697
+
698
+    /**
699
+     * Update the encrypted cache version in the database
700
+     *
701
+     * @param Storage\IStorage $sourceStorage
702
+     * @param string $sourceInternalPath
703
+     * @param string $targetInternalPath
704
+     * @param bool $isRename
705
+     * @param bool $keepEncryptionVersion
706
+     */
707
+    private function updateEncryptedVersion(
708
+        Storage\IStorage $sourceStorage,
709
+        $sourceInternalPath,
710
+        $targetInternalPath,
711
+        $isRename,
712
+        $keepEncryptionVersion
713
+    ) {
714
+        $isEncrypted = $this->encryptionManager->isEnabled() && $this->shouldEncrypt($targetInternalPath);
715
+        $cacheInformation = [
716
+            'encrypted' => $isEncrypted,
717
+        ];
718
+        if ($isEncrypted) {
719
+            $sourceCacheEntry = $sourceStorage->getCache()->get($sourceInternalPath);
720
+            $targetCacheEntry = $this->getCache()->get($targetInternalPath);
721
+
722
+            // Rename of the cache already happened, so we do the cleanup on the target
723
+            if ($sourceCacheEntry === false && $targetCacheEntry !== false) {
724
+                $encryptedVersion = $targetCacheEntry['encryptedVersion'];
725
+                $isRename = false;
726
+            } else {
727
+                $encryptedVersion = $sourceCacheEntry['encryptedVersion'];
728
+            }
729
+
730
+            // In case of a move operation from an unencrypted to an encrypted
731
+            // storage the old encrypted version would stay with "0" while the
732
+            // correct value would be "1". Thus we manually set the value to "1"
733
+            // for those cases.
734
+            // See also https://github.com/owncloud/core/issues/23078
735
+            if ($encryptedVersion === 0 || !$keepEncryptionVersion) {
736
+                $encryptedVersion = 1;
737
+            }
738
+
739
+            $cacheInformation['encryptedVersion'] = $encryptedVersion;
740
+        }
741
+
742
+        // in case of a rename we need to manipulate the source cache because
743
+        // this information will be kept for the new target
744
+        if ($isRename) {
745
+            $sourceStorage->getCache()->put($sourceInternalPath, $cacheInformation);
746
+        } else {
747
+            $this->getCache()->put($targetInternalPath, $cacheInformation);
748
+        }
749
+    }
750
+
751
+    /**
752
+     * copy file between two storages
753
+     *
754
+     * @param Storage\IStorage $sourceStorage
755
+     * @param string $sourceInternalPath
756
+     * @param string $targetInternalPath
757
+     * @param bool $preserveMtime
758
+     * @param bool $isRename
759
+     * @return bool
760
+     * @throws \Exception
761
+     */
762
+    private function copyBetweenStorage(
763
+        Storage\IStorage $sourceStorage,
764
+        $sourceInternalPath,
765
+        $targetInternalPath,
766
+        $preserveMtime,
767
+        $isRename
768
+    ) {
769
+        // for versions we have nothing to do, because versions should always use the
770
+        // key from the original file. Just create a 1:1 copy and done
771
+        if ($this->isVersion($targetInternalPath) ||
772
+            $this->isVersion($sourceInternalPath)) {
773
+            // remember that we try to create a version so that we can detect it during
774
+            // fopen($sourceInternalPath) and by-pass the encryption in order to
775
+            // create a 1:1 copy of the file
776
+            $this->arrayCache->set('encryption_copy_version_' . $sourceInternalPath, true);
777
+            $result = $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
778
+            $this->arrayCache->remove('encryption_copy_version_' . $sourceInternalPath);
779
+            if ($result) {
780
+                $info = $this->getCache('', $sourceStorage)->get($sourceInternalPath);
781
+                // make sure that we update the unencrypted size for the version
782
+                if (isset($info['encrypted']) && $info['encrypted'] === true) {
783
+                    $this->updateUnencryptedSize(
784
+                        $this->getFullPath($targetInternalPath),
785
+                        $info->getUnencryptedSize()
786
+                    );
787
+                }
788
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true);
789
+            }
790
+            return $result;
791
+        }
792
+
793
+        // first copy the keys that we reuse the existing file key on the target location
794
+        // and don't create a new one which would break versions for example.
795
+        $mount = $this->mountManager->findByStorageId($sourceStorage->getId());
796
+        if (count($mount) === 1) {
797
+            $mountPoint = $mount[0]->getMountPoint();
798
+            $source = $mountPoint . '/' . $sourceInternalPath;
799
+            $target = $this->getFullPath($targetInternalPath);
800
+            $this->copyKeys($source, $target);
801
+        } else {
802
+            $this->logger->error('Could not find mount point, can\'t keep encryption keys');
803
+        }
804
+
805
+        if ($sourceStorage->is_dir($sourceInternalPath)) {
806
+            $dh = $sourceStorage->opendir($sourceInternalPath);
807
+            if (!$this->is_dir($targetInternalPath)) {
808
+                $result = $this->mkdir($targetInternalPath);
809
+            } else {
810
+                $result = true;
811
+            }
812
+            if (is_resource($dh)) {
813
+                while ($result and ($file = readdir($dh)) !== false) {
814
+                    if (!Filesystem::isIgnoredDir($file)) {
815
+                        $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename);
816
+                    }
817
+                }
818
+            }
819
+        } else {
820
+            try {
821
+                $source = $sourceStorage->fopen($sourceInternalPath, 'r');
822
+                $target = $this->fopen($targetInternalPath, 'w');
823
+                [, $result] = \OC_Helper::streamCopy($source, $target);
824
+            } finally {
825
+                if (is_resource($source)) {
826
+                    fclose($source);
827
+                }
828
+                if (is_resource($target)) {
829
+                    fclose($target);
830
+                }
831
+            }
832
+            if ($result) {
833
+                if ($preserveMtime) {
834
+                    $this->touch($targetInternalPath, $sourceStorage->filemtime($sourceInternalPath));
835
+                }
836
+                $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, false);
837
+            } else {
838
+                // delete partially written target file
839
+                $this->unlink($targetInternalPath);
840
+                // delete cache entry that was created by fopen
841
+                $this->getCache()->remove($targetInternalPath);
842
+            }
843
+        }
844
+        return (bool)$result;
845
+    }
846
+
847
+    public function getLocalFile($path) {
848
+        if ($this->encryptionManager->isEnabled()) {
849
+            $cachedFile = $this->getCachedFile($path);
850
+            if (is_string($cachedFile)) {
851
+                return $cachedFile;
852
+            }
853
+        }
854
+        return $this->storage->getLocalFile($path);
855
+    }
856
+
857
+    public function isLocal() {
858
+        if ($this->encryptionManager->isEnabled()) {
859
+            return false;
860
+        }
861
+        return $this->storage->isLocal();
862
+    }
863
+
864
+    public function stat($path) {
865
+        $stat = $this->storage->stat($path);
866
+        if (!$stat) {
867
+            return false;
868
+        }
869
+        $fileSize = $this->filesize($path);
870
+        $stat['size'] = $fileSize;
871
+        $stat[7] = $fileSize;
872
+        $stat['hasHeader'] = $this->getHeaderSize($path) > 0;
873
+        return $stat;
874
+    }
875
+
876
+    public function hash($type, $path, $raw = false) {
877
+        $fh = $this->fopen($path, 'rb');
878
+        $ctx = hash_init($type);
879
+        hash_update_stream($ctx, $fh);
880
+        fclose($fh);
881
+        return hash_final($ctx, $raw);
882
+    }
883
+
884
+    /**
885
+     * return full path, including mount point
886
+     *
887
+     * @param string $path relative to mount point
888
+     * @return string full path including mount point
889
+     */
890
+    protected function getFullPath($path) {
891
+        return Filesystem::normalizePath($this->mountPoint . '/' . $path);
892
+    }
893
+
894
+    /**
895
+     * read first block of encrypted file, typically this will contain the
896
+     * encryption header
897
+     *
898
+     * @param string $path
899
+     * @return string
900
+     */
901
+    protected function readFirstBlock($path) {
902
+        $firstBlock = '';
903
+        if ($this->storage->is_file($path)) {
904
+            $handle = $this->storage->fopen($path, 'r');
905
+            $firstBlock = fread($handle, $this->util->getHeaderSize());
906
+            fclose($handle);
907
+        }
908
+        return $firstBlock;
909
+    }
910
+
911
+    /**
912
+     * return header size of given file
913
+     *
914
+     * @param string $path
915
+     * @return int
916
+     */
917
+    protected function getHeaderSize($path) {
918
+        $headerSize = 0;
919
+        $realFile = $this->util->stripPartialFileExtension($path);
920
+        if ($this->storage->is_file($realFile)) {
921
+            $path = $realFile;
922
+        }
923
+        $firstBlock = $this->readFirstBlock($path);
924
+
925
+        if (str_starts_with($firstBlock, Util::HEADER_START)) {
926
+            $headerSize = $this->util->getHeaderSize();
927
+        }
928
+
929
+        return $headerSize;
930
+    }
931
+
932
+    /**
933
+     * parse raw header to array
934
+     *
935
+     * @param string $rawHeader
936
+     * @return array
937
+     */
938
+    protected function parseRawHeader($rawHeader) {
939
+        $result = [];
940
+        if (str_starts_with($rawHeader, Util::HEADER_START)) {
941
+            $header = $rawHeader;
942
+            $endAt = strpos($header, Util::HEADER_END);
943
+            if ($endAt !== false) {
944
+                $header = substr($header, 0, $endAt + strlen(Util::HEADER_END));
945
+
946
+                // +1 to not start with an ':' which would result in empty element at the beginning
947
+                $exploded = explode(':', substr($header, strlen(Util::HEADER_START) + 1));
948
+
949
+                $element = array_shift($exploded);
950
+                while ($element !== Util::HEADER_END) {
951
+                    $result[$element] = array_shift($exploded);
952
+                    $element = array_shift($exploded);
953
+                }
954
+            }
955
+        }
956
+
957
+        return $result;
958
+    }
959
+
960
+    /**
961
+     * read header from file
962
+     *
963
+     * @param string $path
964
+     * @return array
965
+     */
966
+    protected function getHeader($path) {
967
+        $realFile = $this->util->stripPartialFileExtension($path);
968
+        $exists = $this->storage->is_file($realFile);
969
+        if ($exists) {
970
+            $path = $realFile;
971
+        }
972
+
973
+        $result = [];
974
+
975
+        $isEncrypted = $this->encryptedPaths->get($realFile);
976
+        if (is_null($isEncrypted)) {
977
+            $info = $this->getCache()->get($path);
978
+            $isEncrypted = isset($info['encrypted']) && $info['encrypted'] === true;
979
+        }
980
+
981
+        if ($isEncrypted) {
982
+            $firstBlock = $this->readFirstBlock($path);
983
+            $result = $this->parseRawHeader($firstBlock);
984
+
985
+            // if the header doesn't contain a encryption module we check if it is a
986
+            // legacy file. If true, we add the default encryption module
987
+            if (!isset($result[Util::HEADER_ENCRYPTION_MODULE_KEY]) && (!empty($result) || $exists)) {
988
+                $result[Util::HEADER_ENCRYPTION_MODULE_KEY] = 'OC_DEFAULT_MODULE';
989
+            }
990
+        }
991
+
992
+        return $result;
993
+    }
994
+
995
+    /**
996
+     * read encryption module needed to read/write the file located at $path
997
+     *
998
+     * @param string $path
999
+     * @return null|\OCP\Encryption\IEncryptionModule
1000
+     * @throws ModuleDoesNotExistsException
1001
+     * @throws \Exception
1002
+     */
1003
+    protected function getEncryptionModule($path) {
1004
+        $encryptionModule = null;
1005
+        $header = $this->getHeader($path);
1006
+        $encryptionModuleId = $this->util->getEncryptionModuleId($header);
1007
+        if (!empty($encryptionModuleId)) {
1008
+            try {
1009
+                $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId);
1010
+            } catch (ModuleDoesNotExistsException $e) {
1011
+                $this->logger->critical('Encryption module defined in "' . $path . '" not loaded!');
1012
+                throw $e;
1013
+            }
1014
+        }
1015
+
1016
+        return $encryptionModule;
1017
+    }
1018
+
1019
+    /**
1020
+     * @param string $path
1021
+     * @param int $unencryptedSize
1022
+     */
1023
+    public function updateUnencryptedSize($path, $unencryptedSize) {
1024
+        $this->unencryptedSize[$path] = $unencryptedSize;
1025
+    }
1026
+
1027
+    /**
1028
+     * copy keys to new location
1029
+     *
1030
+     * @param string $source path relative to data/
1031
+     * @param string $target path relative to data/
1032
+     * @return bool
1033
+     */
1034
+    protected function copyKeys($source, $target) {
1035
+        if (!$this->util->isExcluded($source)) {
1036
+            return $this->keyStorage->copyKeys($source, $target);
1037
+        }
1038
+
1039
+        return false;
1040
+    }
1041
+
1042
+    /**
1043
+     * check if path points to a files version
1044
+     *
1045
+     * @param $path
1046
+     * @return bool
1047
+     */
1048
+    protected function isVersion($path) {
1049
+        $normalized = Filesystem::normalizePath($path);
1050
+        return substr($normalized, 0, strlen('/files_versions/')) === '/files_versions/';
1051
+    }
1052
+
1053
+    /**
1054
+     * check if the given storage should be encrypted or not
1055
+     *
1056
+     * @param $path
1057
+     * @return bool
1058
+     */
1059
+    protected function shouldEncrypt($path) {
1060
+        $fullPath = $this->getFullPath($path);
1061
+        $mountPointConfig = $this->mount->getOption('encrypt', true);
1062
+        if ($mountPointConfig === false) {
1063
+            return false;
1064
+        }
1065
+
1066
+        try {
1067
+            $encryptionModule = $this->getEncryptionModule($fullPath);
1068
+        } catch (ModuleDoesNotExistsException $e) {
1069
+            return false;
1070
+        }
1071
+
1072
+        if ($encryptionModule === null) {
1073
+            $encryptionModule = $this->encryptionManager->getEncryptionModule();
1074
+        }
1075
+
1076
+        return $encryptionModule->shouldEncrypt($fullPath);
1077
+    }
1078
+
1079
+    public function writeStream(string $path, $stream, int $size = null): int {
1080
+        // always fall back to fopen
1081
+        $target = $this->fopen($path, 'w');
1082
+        [$count, $result] = \OC_Helper::streamCopy($stream, $target);
1083
+        fclose($stream);
1084
+        fclose($target);
1085
+
1086
+        // object store, stores the size after write and doesn't update this during scan
1087
+        // manually store the unencrypted size
1088
+        if ($result && $this->getWrapperStorage()->instanceOfStorage(ObjectStoreStorage::class)) {
1089
+            $this->getCache()->put($path, ['unencrypted_size' => $count]);
1090
+        }
1091
+
1092
+        return $count;
1093
+    }
1094
+
1095
+    public function clearIsEncryptedCache(): void {
1096
+        $this->encryptedPaths->clear();
1097
+    }
1098 1098
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Wrapper/Quota.php 1 patch
Indentation   +210 added lines, -210 removed lines patch added patch discarded remove patch
@@ -39,234 +39,234 @@
 block discarded – undo
39 39
 use OCP\Files\Storage\IStorage;
40 40
 
41 41
 class Quota extends Wrapper {
42
-	/** @var callable|null */
43
-	protected $quotaCallback;
44
-	/** @var int|float|null int on 64bits, float on 32bits for bigint */
45
-	protected int|float|null $quota;
46
-	protected string $sizeRoot;
47
-	private SystemConfig $config;
42
+    /** @var callable|null */
43
+    protected $quotaCallback;
44
+    /** @var int|float|null int on 64bits, float on 32bits for bigint */
45
+    protected int|float|null $quota;
46
+    protected string $sizeRoot;
47
+    private SystemConfig $config;
48 48
 
49
-	/**
50
-	 * @param array $parameters
51
-	 */
52
-	public function __construct($parameters) {
53
-		parent::__construct($parameters);
54
-		$this->quota = $parameters['quota'] ?? null;
55
-		$this->quotaCallback = $parameters['quotaCallback'] ?? null;
56
-		$this->sizeRoot = $parameters['root'] ?? '';
57
-		$this->config = \OC::$server->get(SystemConfig::class);
58
-	}
49
+    /**
50
+     * @param array $parameters
51
+     */
52
+    public function __construct($parameters) {
53
+        parent::__construct($parameters);
54
+        $this->quota = $parameters['quota'] ?? null;
55
+        $this->quotaCallback = $parameters['quotaCallback'] ?? null;
56
+        $this->sizeRoot = $parameters['root'] ?? '';
57
+        $this->config = \OC::$server->get(SystemConfig::class);
58
+    }
59 59
 
60
-	/**
61
-	 * @return int|float quota value
62
-	 */
63
-	public function getQuota(): int|float {
64
-		if ($this->quota === null) {
65
-			$quotaCallback = $this->quotaCallback;
66
-			if ($quotaCallback === null) {
67
-				throw new \Exception("No quota or quota callback provider");
68
-			}
69
-			$this->quota = $quotaCallback();
70
-		}
60
+    /**
61
+     * @return int|float quota value
62
+     */
63
+    public function getQuota(): int|float {
64
+        if ($this->quota === null) {
65
+            $quotaCallback = $this->quotaCallback;
66
+            if ($quotaCallback === null) {
67
+                throw new \Exception("No quota or quota callback provider");
68
+            }
69
+            $this->quota = $quotaCallback();
70
+        }
71 71
 
72
-		return $this->quota;
73
-	}
72
+        return $this->quota;
73
+    }
74 74
 
75
-	private function hasQuota(): bool {
76
-		return $this->getQuota() !== FileInfo::SPACE_UNLIMITED;
77
-	}
75
+    private function hasQuota(): bool {
76
+        return $this->getQuota() !== FileInfo::SPACE_UNLIMITED;
77
+    }
78 78
 
79
-	/**
80
-	 * @param string $path
81
-	 * @param IStorage $storage
82
-	 * @return int|float
83
-	 */
84
-	protected function getSize($path, $storage = null) {
85
-		if ($this->config->getValue('quota_include_external_storage', false)) {
86
-			$rootInfo = Filesystem::getFileInfo('', 'ext');
87
-			if ($rootInfo) {
88
-				return $rootInfo->getSize(true);
89
-			}
90
-			return FileInfo::SPACE_NOT_COMPUTED;
91
-		} else {
92
-			$cache = is_null($storage) ? $this->getCache() : $storage->getCache();
93
-			$data = $cache->get($path);
94
-			if ($data instanceof ICacheEntry && isset($data['size'])) {
95
-				return $data['size'];
96
-			} else {
97
-				return FileInfo::SPACE_NOT_COMPUTED;
98
-			}
99
-		}
100
-	}
79
+    /**
80
+     * @param string $path
81
+     * @param IStorage $storage
82
+     * @return int|float
83
+     */
84
+    protected function getSize($path, $storage = null) {
85
+        if ($this->config->getValue('quota_include_external_storage', false)) {
86
+            $rootInfo = Filesystem::getFileInfo('', 'ext');
87
+            if ($rootInfo) {
88
+                return $rootInfo->getSize(true);
89
+            }
90
+            return FileInfo::SPACE_NOT_COMPUTED;
91
+        } else {
92
+            $cache = is_null($storage) ? $this->getCache() : $storage->getCache();
93
+            $data = $cache->get($path);
94
+            if ($data instanceof ICacheEntry && isset($data['size'])) {
95
+                return $data['size'];
96
+            } else {
97
+                return FileInfo::SPACE_NOT_COMPUTED;
98
+            }
99
+        }
100
+    }
101 101
 
102
-	/**
103
-	 * Get free space as limited by the quota
104
-	 *
105
-	 * @param string $path
106
-	 * @return int|float|bool
107
-	 */
108
-	public function free_space($path) {
109
-		if (!$this->hasQuota()) {
110
-			return $this->storage->free_space($path);
111
-		}
112
-		if ($this->getQuota() < 0 || str_starts_with($path, 'cache') || str_starts_with($path, 'uploads')) {
113
-			return $this->storage->free_space($path);
114
-		} else {
115
-			$used = $this->getSize($this->sizeRoot);
116
-			if ($used < 0) {
117
-				return FileInfo::SPACE_NOT_COMPUTED;
118
-			} else {
119
-				$free = $this->storage->free_space($path);
120
-				$quotaFree = max($this->getQuota() - $used, 0);
121
-				// if free space is known
122
-				$free = $free >= 0 ? min($free, $quotaFree) : $quotaFree;
123
-				return $free;
124
-			}
125
-		}
126
-	}
102
+    /**
103
+     * Get free space as limited by the quota
104
+     *
105
+     * @param string $path
106
+     * @return int|float|bool
107
+     */
108
+    public function free_space($path) {
109
+        if (!$this->hasQuota()) {
110
+            return $this->storage->free_space($path);
111
+        }
112
+        if ($this->getQuota() < 0 || str_starts_with($path, 'cache') || str_starts_with($path, 'uploads')) {
113
+            return $this->storage->free_space($path);
114
+        } else {
115
+            $used = $this->getSize($this->sizeRoot);
116
+            if ($used < 0) {
117
+                return FileInfo::SPACE_NOT_COMPUTED;
118
+            } else {
119
+                $free = $this->storage->free_space($path);
120
+                $quotaFree = max($this->getQuota() - $used, 0);
121
+                // if free space is known
122
+                $free = $free >= 0 ? min($free, $quotaFree) : $quotaFree;
123
+                return $free;
124
+            }
125
+        }
126
+    }
127 127
 
128
-	/**
129
-	 * see https://www.php.net/manual/en/function.file_put_contents.php
130
-	 *
131
-	 * @param string $path
132
-	 * @param mixed $data
133
-	 * @return int|float|false
134
-	 */
135
-	public function file_put_contents($path, $data) {
136
-		if (!$this->hasQuota()) {
137
-			return $this->storage->file_put_contents($path, $data);
138
-		}
139
-		$free = $this->free_space($path);
140
-		if ($free < 0 || strlen($data) < $free) {
141
-			return $this->storage->file_put_contents($path, $data);
142
-		} else {
143
-			return false;
144
-		}
145
-	}
128
+    /**
129
+     * see https://www.php.net/manual/en/function.file_put_contents.php
130
+     *
131
+     * @param string $path
132
+     * @param mixed $data
133
+     * @return int|float|false
134
+     */
135
+    public function file_put_contents($path, $data) {
136
+        if (!$this->hasQuota()) {
137
+            return $this->storage->file_put_contents($path, $data);
138
+        }
139
+        $free = $this->free_space($path);
140
+        if ($free < 0 || strlen($data) < $free) {
141
+            return $this->storage->file_put_contents($path, $data);
142
+        } else {
143
+            return false;
144
+        }
145
+    }
146 146
 
147
-	/**
148
-	 * see https://www.php.net/manual/en/function.copy.php
149
-	 *
150
-	 * @param string $source
151
-	 * @param string $target
152
-	 * @return bool
153
-	 */
154
-	public function copy($source, $target) {
155
-		if (!$this->hasQuota()) {
156
-			return $this->storage->copy($source, $target);
157
-		}
158
-		$free = $this->free_space($target);
159
-		if ($free < 0 || $this->getSize($source) < $free) {
160
-			return $this->storage->copy($source, $target);
161
-		} else {
162
-			return false;
163
-		}
164
-	}
147
+    /**
148
+     * see https://www.php.net/manual/en/function.copy.php
149
+     *
150
+     * @param string $source
151
+     * @param string $target
152
+     * @return bool
153
+     */
154
+    public function copy($source, $target) {
155
+        if (!$this->hasQuota()) {
156
+            return $this->storage->copy($source, $target);
157
+        }
158
+        $free = $this->free_space($target);
159
+        if ($free < 0 || $this->getSize($source) < $free) {
160
+            return $this->storage->copy($source, $target);
161
+        } else {
162
+            return false;
163
+        }
164
+    }
165 165
 
166
-	/**
167
-	 * see https://www.php.net/manual/en/function.fopen.php
168
-	 *
169
-	 * @param string $path
170
-	 * @param string $mode
171
-	 * @return resource|bool
172
-	 */
173
-	public function fopen($path, $mode) {
174
-		if (!$this->hasQuota()) {
175
-			return $this->storage->fopen($path, $mode);
176
-		}
177
-		$source = $this->storage->fopen($path, $mode);
166
+    /**
167
+     * see https://www.php.net/manual/en/function.fopen.php
168
+     *
169
+     * @param string $path
170
+     * @param string $mode
171
+     * @return resource|bool
172
+     */
173
+    public function fopen($path, $mode) {
174
+        if (!$this->hasQuota()) {
175
+            return $this->storage->fopen($path, $mode);
176
+        }
177
+        $source = $this->storage->fopen($path, $mode);
178 178
 
179
-		// don't apply quota for part files
180
-		if (!$this->isPartFile($path)) {
181
-			$free = $this->free_space($path);
182
-			if ($source && (is_int($free) || is_float($free)) && $free >= 0 && $mode !== 'r' && $mode !== 'rb') {
183
-				// only apply quota for files, not metadata, trash or others
184
-				if ($this->shouldApplyQuota($path)) {
185
-					return \OC\Files\Stream\Quota::wrap($source, $free);
186
-				}
187
-			}
188
-		}
179
+        // don't apply quota for part files
180
+        if (!$this->isPartFile($path)) {
181
+            $free = $this->free_space($path);
182
+            if ($source && (is_int($free) || is_float($free)) && $free >= 0 && $mode !== 'r' && $mode !== 'rb') {
183
+                // only apply quota for files, not metadata, trash or others
184
+                if ($this->shouldApplyQuota($path)) {
185
+                    return \OC\Files\Stream\Quota::wrap($source, $free);
186
+                }
187
+            }
188
+        }
189 189
 
190
-		return $source;
191
-	}
190
+        return $source;
191
+    }
192 192
 
193
-	/**
194
-	 * Checks whether the given path is a part file
195
-	 *
196
-	 * @param string $path Path that may identify a .part file
197
-	 * @return bool
198
-	 * @note this is needed for reusing keys
199
-	 */
200
-	private function isPartFile($path) {
201
-		$extension = pathinfo($path, PATHINFO_EXTENSION);
193
+    /**
194
+     * Checks whether the given path is a part file
195
+     *
196
+     * @param string $path Path that may identify a .part file
197
+     * @return bool
198
+     * @note this is needed for reusing keys
199
+     */
200
+    private function isPartFile($path) {
201
+        $extension = pathinfo($path, PATHINFO_EXTENSION);
202 202
 
203
-		return ($extension === 'part');
204
-	}
203
+        return ($extension === 'part');
204
+    }
205 205
 
206
-	/**
207
-	 * Only apply quota for files, not metadata, trash or others
208
-	 */
209
-	private function shouldApplyQuota(string $path): bool {
210
-		return str_starts_with(ltrim($path, '/'), 'files/');
211
-	}
206
+    /**
207
+     * Only apply quota for files, not metadata, trash or others
208
+     */
209
+    private function shouldApplyQuota(string $path): bool {
210
+        return str_starts_with(ltrim($path, '/'), 'files/');
211
+    }
212 212
 
213
-	/**
214
-	 * @param IStorage $sourceStorage
215
-	 * @param string $sourceInternalPath
216
-	 * @param string $targetInternalPath
217
-	 * @return bool
218
-	 */
219
-	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
220
-		if (!$this->hasQuota()) {
221
-			return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
222
-		}
223
-		$free = $this->free_space($targetInternalPath);
224
-		if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
225
-			return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
226
-		} else {
227
-			return false;
228
-		}
229
-	}
213
+    /**
214
+     * @param IStorage $sourceStorage
215
+     * @param string $sourceInternalPath
216
+     * @param string $targetInternalPath
217
+     * @return bool
218
+     */
219
+    public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
220
+        if (!$this->hasQuota()) {
221
+            return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
222
+        }
223
+        $free = $this->free_space($targetInternalPath);
224
+        if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
225
+            return $this->storage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
226
+        } else {
227
+            return false;
228
+        }
229
+    }
230 230
 
231
-	/**
232
-	 * @param IStorage $sourceStorage
233
-	 * @param string $sourceInternalPath
234
-	 * @param string $targetInternalPath
235
-	 * @return bool
236
-	 */
237
-	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
238
-		if (!$this->hasQuota()) {
239
-			return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
240
-		}
241
-		$free = $this->free_space($targetInternalPath);
242
-		if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
243
-			return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
244
-		} else {
245
-			return false;
246
-		}
247
-	}
231
+    /**
232
+     * @param IStorage $sourceStorage
233
+     * @param string $sourceInternalPath
234
+     * @param string $targetInternalPath
235
+     * @return bool
236
+     */
237
+    public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
238
+        if (!$this->hasQuota()) {
239
+            return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
240
+        }
241
+        $free = $this->free_space($targetInternalPath);
242
+        if ($free < 0 || $this->getSize($sourceInternalPath, $sourceStorage) < $free) {
243
+            return $this->storage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
244
+        } else {
245
+            return false;
246
+        }
247
+    }
248 248
 
249
-	public function mkdir($path) {
250
-		if (!$this->hasQuota()) {
251
-			return $this->storage->mkdir($path);
252
-		}
253
-		$free = $this->free_space($path);
254
-		if ($this->shouldApplyQuota($path) && $free == 0) {
255
-			return false;
256
-		}
249
+    public function mkdir($path) {
250
+        if (!$this->hasQuota()) {
251
+            return $this->storage->mkdir($path);
252
+        }
253
+        $free = $this->free_space($path);
254
+        if ($this->shouldApplyQuota($path) && $free == 0) {
255
+            return false;
256
+        }
257 257
 
258
-		return parent::mkdir($path);
259
-	}
258
+        return parent::mkdir($path);
259
+    }
260 260
 
261
-	public function touch($path, $mtime = null) {
262
-		if (!$this->hasQuota()) {
263
-			return $this->storage->touch($path, $mtime);
264
-		}
265
-		$free = $this->free_space($path);
266
-		if ($free == 0) {
267
-			return false;
268
-		}
261
+    public function touch($path, $mtime = null) {
262
+        if (!$this->hasQuota()) {
263
+            return $this->storage->touch($path, $mtime);
264
+        }
265
+        $free = $this->free_space($path);
266
+        if ($free == 0) {
267
+            return false;
268
+        }
269 269
 
270
-		return parent::touch($path, $mtime);
271
-	}
270
+        return parent::touch($path, $mtime);
271
+    }
272 272
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Wrapper/Jail.php 2 patches
Indentation   +493 added lines, -493 removed lines patch added patch discarded remove patch
@@ -41,497 +41,497 @@
 block discarded – undo
41 41
  * This restricts access to a subfolder of the wrapped storage with the subfolder becoming the root folder new storage
42 42
  */
43 43
 class Jail extends Wrapper {
44
-	/**
45
-	 * @var string
46
-	 */
47
-	protected $rootPath;
48
-
49
-	/**
50
-	 * @param array $arguments ['storage' => $storage, 'root' => $root]
51
-	 *
52
-	 * $storage: The storage that will be wrapper
53
-	 * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage
54
-	 */
55
-	public function __construct($arguments) {
56
-		parent::__construct($arguments);
57
-		$this->rootPath = $arguments['root'];
58
-	}
59
-
60
-	public function getUnjailedPath($path) {
61
-		return trim(Filesystem::normalizePath($this->rootPath . '/' . $path), '/');
62
-	}
63
-
64
-	/**
65
-	 * This is separate from Wrapper::getWrapperStorage so we can get the jailed storage consistently even if the jail is inside another wrapper
66
-	 */
67
-	public function getUnjailedStorage() {
68
-		return $this->storage;
69
-	}
70
-
71
-
72
-	public function getJailedPath($path) {
73
-		$root = rtrim($this->rootPath, '/') . '/';
74
-
75
-		if ($path !== $this->rootPath && !str_starts_with($path, $root)) {
76
-			return null;
77
-		} else {
78
-			$path = substr($path, strlen($this->rootPath));
79
-			return trim($path, '/');
80
-		}
81
-	}
82
-
83
-	public function getId() {
84
-		return parent::getId();
85
-	}
86
-
87
-	/**
88
-	 * see https://www.php.net/manual/en/function.mkdir.php
89
-	 *
90
-	 * @param string $path
91
-	 * @return bool
92
-	 */
93
-	public function mkdir($path) {
94
-		return $this->getWrapperStorage()->mkdir($this->getUnjailedPath($path));
95
-	}
96
-
97
-	/**
98
-	 * see https://www.php.net/manual/en/function.rmdir.php
99
-	 *
100
-	 * @param string $path
101
-	 * @return bool
102
-	 */
103
-	public function rmdir($path) {
104
-		return $this->getWrapperStorage()->rmdir($this->getUnjailedPath($path));
105
-	}
106
-
107
-	/**
108
-	 * see https://www.php.net/manual/en/function.opendir.php
109
-	 *
110
-	 * @param string $path
111
-	 * @return resource|false
112
-	 */
113
-	public function opendir($path) {
114
-		return $this->getWrapperStorage()->opendir($this->getUnjailedPath($path));
115
-	}
116
-
117
-	/**
118
-	 * see https://www.php.net/manual/en/function.is_dir.php
119
-	 *
120
-	 * @param string $path
121
-	 * @return bool
122
-	 */
123
-	public function is_dir($path) {
124
-		return $this->getWrapperStorage()->is_dir($this->getUnjailedPath($path));
125
-	}
126
-
127
-	/**
128
-	 * see https://www.php.net/manual/en/function.is_file.php
129
-	 *
130
-	 * @param string $path
131
-	 * @return bool
132
-	 */
133
-	public function is_file($path) {
134
-		return $this->getWrapperStorage()->is_file($this->getUnjailedPath($path));
135
-	}
136
-
137
-	/**
138
-	 * see https://www.php.net/manual/en/function.stat.php
139
-	 * only the following keys are required in the result: size and mtime
140
-	 *
141
-	 * @param string $path
142
-	 * @return array|bool
143
-	 */
144
-	public function stat($path) {
145
-		return $this->getWrapperStorage()->stat($this->getUnjailedPath($path));
146
-	}
147
-
148
-	/**
149
-	 * see https://www.php.net/manual/en/function.filetype.php
150
-	 *
151
-	 * @param string $path
152
-	 * @return bool
153
-	 */
154
-	public function filetype($path) {
155
-		return $this->getWrapperStorage()->filetype($this->getUnjailedPath($path));
156
-	}
157
-
158
-	/**
159
-	 * see https://www.php.net/manual/en/function.filesize.php
160
-	 * The result for filesize when called on a folder is required to be 0
161
-	 */
162
-	public function filesize($path): false|int|float {
163
-		return $this->getWrapperStorage()->filesize($this->getUnjailedPath($path));
164
-	}
165
-
166
-	/**
167
-	 * check if a file can be created in $path
168
-	 *
169
-	 * @param string $path
170
-	 * @return bool
171
-	 */
172
-	public function isCreatable($path) {
173
-		return $this->getWrapperStorage()->isCreatable($this->getUnjailedPath($path));
174
-	}
175
-
176
-	/**
177
-	 * check if a file can be read
178
-	 *
179
-	 * @param string $path
180
-	 * @return bool
181
-	 */
182
-	public function isReadable($path) {
183
-		return $this->getWrapperStorage()->isReadable($this->getUnjailedPath($path));
184
-	}
185
-
186
-	/**
187
-	 * check if a file can be written to
188
-	 *
189
-	 * @param string $path
190
-	 * @return bool
191
-	 */
192
-	public function isUpdatable($path) {
193
-		return $this->getWrapperStorage()->isUpdatable($this->getUnjailedPath($path));
194
-	}
195
-
196
-	/**
197
-	 * check if a file can be deleted
198
-	 *
199
-	 * @param string $path
200
-	 * @return bool
201
-	 */
202
-	public function isDeletable($path) {
203
-		return $this->getWrapperStorage()->isDeletable($this->getUnjailedPath($path));
204
-	}
205
-
206
-	/**
207
-	 * check if a file can be shared
208
-	 *
209
-	 * @param string $path
210
-	 * @return bool
211
-	 */
212
-	public function isSharable($path) {
213
-		return $this->getWrapperStorage()->isSharable($this->getUnjailedPath($path));
214
-	}
215
-
216
-	/**
217
-	 * get the full permissions of a path.
218
-	 * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
219
-	 *
220
-	 * @param string $path
221
-	 * @return int
222
-	 */
223
-	public function getPermissions($path) {
224
-		return $this->getWrapperStorage()->getPermissions($this->getUnjailedPath($path));
225
-	}
226
-
227
-	/**
228
-	 * see https://www.php.net/manual/en/function.file_exists.php
229
-	 *
230
-	 * @param string $path
231
-	 * @return bool
232
-	 */
233
-	public function file_exists($path) {
234
-		return $this->getWrapperStorage()->file_exists($this->getUnjailedPath($path));
235
-	}
236
-
237
-	/**
238
-	 * see https://www.php.net/manual/en/function.filemtime.php
239
-	 *
240
-	 * @param string $path
241
-	 * @return int|bool
242
-	 */
243
-	public function filemtime($path) {
244
-		return $this->getWrapperStorage()->filemtime($this->getUnjailedPath($path));
245
-	}
246
-
247
-	/**
248
-	 * see https://www.php.net/manual/en/function.file_get_contents.php
249
-	 *
250
-	 * @param string $path
251
-	 * @return string|false
252
-	 */
253
-	public function file_get_contents($path) {
254
-		return $this->getWrapperStorage()->file_get_contents($this->getUnjailedPath($path));
255
-	}
256
-
257
-	/**
258
-	 * see https://www.php.net/manual/en/function.file_put_contents.php
259
-	 *
260
-	 * @param string $path
261
-	 * @param mixed $data
262
-	 * @return int|float|false
263
-	 */
264
-	public function file_put_contents($path, $data) {
265
-		return $this->getWrapperStorage()->file_put_contents($this->getUnjailedPath($path), $data);
266
-	}
267
-
268
-	/**
269
-	 * see https://www.php.net/manual/en/function.unlink.php
270
-	 *
271
-	 * @param string $path
272
-	 * @return bool
273
-	 */
274
-	public function unlink($path) {
275
-		return $this->getWrapperStorage()->unlink($this->getUnjailedPath($path));
276
-	}
277
-
278
-	/**
279
-	 * see https://www.php.net/manual/en/function.rename.php
280
-	 *
281
-	 * @param string $source
282
-	 * @param string $target
283
-	 * @return bool
284
-	 */
285
-	public function rename($source, $target) {
286
-		return $this->getWrapperStorage()->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
287
-	}
288
-
289
-	/**
290
-	 * see https://www.php.net/manual/en/function.copy.php
291
-	 *
292
-	 * @param string $source
293
-	 * @param string $target
294
-	 * @return bool
295
-	 */
296
-	public function copy($source, $target) {
297
-		return $this->getWrapperStorage()->copy($this->getUnjailedPath($source), $this->getUnjailedPath($target));
298
-	}
299
-
300
-	/**
301
-	 * see https://www.php.net/manual/en/function.fopen.php
302
-	 *
303
-	 * @param string $path
304
-	 * @param string $mode
305
-	 * @return resource|bool
306
-	 */
307
-	public function fopen($path, $mode) {
308
-		return $this->getWrapperStorage()->fopen($this->getUnjailedPath($path), $mode);
309
-	}
310
-
311
-	/**
312
-	 * get the mimetype for a file or folder
313
-	 * The mimetype for a folder is required to be "httpd/unix-directory"
314
-	 *
315
-	 * @param string $path
316
-	 * @return string|bool
317
-	 */
318
-	public function getMimeType($path) {
319
-		return $this->getWrapperStorage()->getMimeType($this->getUnjailedPath($path));
320
-	}
321
-
322
-	/**
323
-	 * see https://www.php.net/manual/en/function.hash.php
324
-	 *
325
-	 * @param string $type
326
-	 * @param string $path
327
-	 * @param bool $raw
328
-	 * @return string|bool
329
-	 */
330
-	public function hash($type, $path, $raw = false) {
331
-		return $this->getWrapperStorage()->hash($type, $this->getUnjailedPath($path), $raw);
332
-	}
333
-
334
-	/**
335
-	 * see https://www.php.net/manual/en/function.free_space.php
336
-	 *
337
-	 * @param string $path
338
-	 * @return int|float|bool
339
-	 */
340
-	public function free_space($path) {
341
-		return $this->getWrapperStorage()->free_space($this->getUnjailedPath($path));
342
-	}
343
-
344
-	/**
345
-	 * search for occurrences of $query in file names
346
-	 *
347
-	 * @param string $query
348
-	 * @return array|bool
349
-	 */
350
-	public function search($query) {
351
-		return $this->getWrapperStorage()->search($query);
352
-	}
353
-
354
-	/**
355
-	 * see https://www.php.net/manual/en/function.touch.php
356
-	 * If the backend does not support the operation, false should be returned
357
-	 *
358
-	 * @param string $path
359
-	 * @param int $mtime
360
-	 * @return bool
361
-	 */
362
-	public function touch($path, $mtime = null) {
363
-		return $this->getWrapperStorage()->touch($this->getUnjailedPath($path), $mtime);
364
-	}
365
-
366
-	/**
367
-	 * get the path to a local version of the file.
368
-	 * The local version of the file can be temporary and doesn't have to be persistent across requests
369
-	 *
370
-	 * @param string $path
371
-	 * @return string|false
372
-	 */
373
-	public function getLocalFile($path) {
374
-		return $this->getWrapperStorage()->getLocalFile($this->getUnjailedPath($path));
375
-	}
376
-
377
-	/**
378
-	 * check if a file or folder has been updated since $time
379
-	 *
380
-	 * @param string $path
381
-	 * @param int $time
382
-	 * @return bool
383
-	 *
384
-	 * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
385
-	 * returning true for other changes in the folder is optional
386
-	 */
387
-	public function hasUpdated($path, $time) {
388
-		return $this->getWrapperStorage()->hasUpdated($this->getUnjailedPath($path), $time);
389
-	}
390
-
391
-	/**
392
-	 * get a cache instance for the storage
393
-	 *
394
-	 * @param string $path
395
-	 * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache
396
-	 * @return \OC\Files\Cache\Cache
397
-	 */
398
-	public function getCache($path = '', $storage = null) {
399
-		if (!$storage) {
400
-			$storage = $this->getWrapperStorage();
401
-		}
402
-		$sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path), $storage);
403
-		return new CacheJail($sourceCache, $this->rootPath);
404
-	}
405
-
406
-	/**
407
-	 * get the user id of the owner of a file or folder
408
-	 *
409
-	 * @param string $path
410
-	 * @return string
411
-	 */
412
-	public function getOwner($path) {
413
-		return $this->getWrapperStorage()->getOwner($this->getUnjailedPath($path));
414
-	}
415
-
416
-	/**
417
-	 * get a watcher instance for the cache
418
-	 *
419
-	 * @param string $path
420
-	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
421
-	 * @return \OC\Files\Cache\Watcher
422
-	 */
423
-	public function getWatcher($path = '', $storage = null) {
424
-		if (!$storage) {
425
-			$storage = $this;
426
-		}
427
-		return $this->getWrapperStorage()->getWatcher($this->getUnjailedPath($path), $storage);
428
-	}
429
-
430
-	/**
431
-	 * get the ETag for a file or folder
432
-	 *
433
-	 * @param string $path
434
-	 * @return string|false
435
-	 */
436
-	public function getETag($path) {
437
-		return $this->getWrapperStorage()->getETag($this->getUnjailedPath($path));
438
-	}
439
-
440
-	public function getMetaData($path) {
441
-		return $this->getWrapperStorage()->getMetaData($this->getUnjailedPath($path));
442
-	}
443
-
444
-	/**
445
-	 * @param string $path
446
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
447
-	 * @param \OCP\Lock\ILockingProvider $provider
448
-	 * @throws \OCP\Lock\LockedException
449
-	 */
450
-	public function acquireLock($path, $type, ILockingProvider $provider) {
451
-		$this->getWrapperStorage()->acquireLock($this->getUnjailedPath($path), $type, $provider);
452
-	}
453
-
454
-	/**
455
-	 * @param string $path
456
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
457
-	 * @param \OCP\Lock\ILockingProvider $provider
458
-	 */
459
-	public function releaseLock($path, $type, ILockingProvider $provider) {
460
-		$this->getWrapperStorage()->releaseLock($this->getUnjailedPath($path), $type, $provider);
461
-	}
462
-
463
-	/**
464
-	 * @param string $path
465
-	 * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
466
-	 * @param \OCP\Lock\ILockingProvider $provider
467
-	 */
468
-	public function changeLock($path, $type, ILockingProvider $provider) {
469
-		$this->getWrapperStorage()->changeLock($this->getUnjailedPath($path), $type, $provider);
470
-	}
471
-
472
-	/**
473
-	 * Resolve the path for the source of the share
474
-	 *
475
-	 * @param string $path
476
-	 * @return array
477
-	 */
478
-	public function resolvePath($path) {
479
-		return [$this->getWrapperStorage(), $this->getUnjailedPath($path)];
480
-	}
481
-
482
-	/**
483
-	 * @param IStorage $sourceStorage
484
-	 * @param string $sourceInternalPath
485
-	 * @param string $targetInternalPath
486
-	 * @return bool
487
-	 */
488
-	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
489
-		if ($sourceStorage === $this) {
490
-			return $this->copy($sourceInternalPath, $targetInternalPath);
491
-		}
492
-		return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
493
-	}
494
-
495
-	/**
496
-	 * @param IStorage $sourceStorage
497
-	 * @param string $sourceInternalPath
498
-	 * @param string $targetInternalPath
499
-	 * @return bool
500
-	 */
501
-	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
502
-		if ($sourceStorage === $this) {
503
-			return $this->rename($sourceInternalPath, $targetInternalPath);
504
-		}
505
-		return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
506
-	}
507
-
508
-	public function getPropagator($storage = null) {
509
-		if (isset($this->propagator)) {
510
-			return $this->propagator;
511
-		}
512
-
513
-		if (!$storage) {
514
-			$storage = $this;
515
-		}
516
-		$this->propagator = new JailPropagator($storage, \OC::$server->getDatabaseConnection());
517
-		return $this->propagator;
518
-	}
519
-
520
-	public function writeStream(string $path, $stream, int $size = null): int {
521
-		$storage = $this->getWrapperStorage();
522
-		if ($storage->instanceOfStorage(IWriteStreamStorage::class)) {
523
-			/** @var IWriteStreamStorage $storage */
524
-			return $storage->writeStream($this->getUnjailedPath($path), $stream, $size);
525
-		} else {
526
-			$target = $this->fopen($path, 'w');
527
-			[$count, $result] = \OC_Helper::streamCopy($stream, $target);
528
-			fclose($stream);
529
-			fclose($target);
530
-			return $count;
531
-		}
532
-	}
533
-
534
-	public function getDirectoryContent($directory): \Traversable {
535
-		return $this->getWrapperStorage()->getDirectoryContent($this->getUnjailedPath($directory));
536
-	}
44
+    /**
45
+     * @var string
46
+     */
47
+    protected $rootPath;
48
+
49
+    /**
50
+     * @param array $arguments ['storage' => $storage, 'root' => $root]
51
+     *
52
+     * $storage: The storage that will be wrapper
53
+     * $root: The folder in the wrapped storage that will become the root folder of the wrapped storage
54
+     */
55
+    public function __construct($arguments) {
56
+        parent::__construct($arguments);
57
+        $this->rootPath = $arguments['root'];
58
+    }
59
+
60
+    public function getUnjailedPath($path) {
61
+        return trim(Filesystem::normalizePath($this->rootPath . '/' . $path), '/');
62
+    }
63
+
64
+    /**
65
+     * This is separate from Wrapper::getWrapperStorage so we can get the jailed storage consistently even if the jail is inside another wrapper
66
+     */
67
+    public function getUnjailedStorage() {
68
+        return $this->storage;
69
+    }
70
+
71
+
72
+    public function getJailedPath($path) {
73
+        $root = rtrim($this->rootPath, '/') . '/';
74
+
75
+        if ($path !== $this->rootPath && !str_starts_with($path, $root)) {
76
+            return null;
77
+        } else {
78
+            $path = substr($path, strlen($this->rootPath));
79
+            return trim($path, '/');
80
+        }
81
+    }
82
+
83
+    public function getId() {
84
+        return parent::getId();
85
+    }
86
+
87
+    /**
88
+     * see https://www.php.net/manual/en/function.mkdir.php
89
+     *
90
+     * @param string $path
91
+     * @return bool
92
+     */
93
+    public function mkdir($path) {
94
+        return $this->getWrapperStorage()->mkdir($this->getUnjailedPath($path));
95
+    }
96
+
97
+    /**
98
+     * see https://www.php.net/manual/en/function.rmdir.php
99
+     *
100
+     * @param string $path
101
+     * @return bool
102
+     */
103
+    public function rmdir($path) {
104
+        return $this->getWrapperStorage()->rmdir($this->getUnjailedPath($path));
105
+    }
106
+
107
+    /**
108
+     * see https://www.php.net/manual/en/function.opendir.php
109
+     *
110
+     * @param string $path
111
+     * @return resource|false
112
+     */
113
+    public function opendir($path) {
114
+        return $this->getWrapperStorage()->opendir($this->getUnjailedPath($path));
115
+    }
116
+
117
+    /**
118
+     * see https://www.php.net/manual/en/function.is_dir.php
119
+     *
120
+     * @param string $path
121
+     * @return bool
122
+     */
123
+    public function is_dir($path) {
124
+        return $this->getWrapperStorage()->is_dir($this->getUnjailedPath($path));
125
+    }
126
+
127
+    /**
128
+     * see https://www.php.net/manual/en/function.is_file.php
129
+     *
130
+     * @param string $path
131
+     * @return bool
132
+     */
133
+    public function is_file($path) {
134
+        return $this->getWrapperStorage()->is_file($this->getUnjailedPath($path));
135
+    }
136
+
137
+    /**
138
+     * see https://www.php.net/manual/en/function.stat.php
139
+     * only the following keys are required in the result: size and mtime
140
+     *
141
+     * @param string $path
142
+     * @return array|bool
143
+     */
144
+    public function stat($path) {
145
+        return $this->getWrapperStorage()->stat($this->getUnjailedPath($path));
146
+    }
147
+
148
+    /**
149
+     * see https://www.php.net/manual/en/function.filetype.php
150
+     *
151
+     * @param string $path
152
+     * @return bool
153
+     */
154
+    public function filetype($path) {
155
+        return $this->getWrapperStorage()->filetype($this->getUnjailedPath($path));
156
+    }
157
+
158
+    /**
159
+     * see https://www.php.net/manual/en/function.filesize.php
160
+     * The result for filesize when called on a folder is required to be 0
161
+     */
162
+    public function filesize($path): false|int|float {
163
+        return $this->getWrapperStorage()->filesize($this->getUnjailedPath($path));
164
+    }
165
+
166
+    /**
167
+     * check if a file can be created in $path
168
+     *
169
+     * @param string $path
170
+     * @return bool
171
+     */
172
+    public function isCreatable($path) {
173
+        return $this->getWrapperStorage()->isCreatable($this->getUnjailedPath($path));
174
+    }
175
+
176
+    /**
177
+     * check if a file can be read
178
+     *
179
+     * @param string $path
180
+     * @return bool
181
+     */
182
+    public function isReadable($path) {
183
+        return $this->getWrapperStorage()->isReadable($this->getUnjailedPath($path));
184
+    }
185
+
186
+    /**
187
+     * check if a file can be written to
188
+     *
189
+     * @param string $path
190
+     * @return bool
191
+     */
192
+    public function isUpdatable($path) {
193
+        return $this->getWrapperStorage()->isUpdatable($this->getUnjailedPath($path));
194
+    }
195
+
196
+    /**
197
+     * check if a file can be deleted
198
+     *
199
+     * @param string $path
200
+     * @return bool
201
+     */
202
+    public function isDeletable($path) {
203
+        return $this->getWrapperStorage()->isDeletable($this->getUnjailedPath($path));
204
+    }
205
+
206
+    /**
207
+     * check if a file can be shared
208
+     *
209
+     * @param string $path
210
+     * @return bool
211
+     */
212
+    public function isSharable($path) {
213
+        return $this->getWrapperStorage()->isSharable($this->getUnjailedPath($path));
214
+    }
215
+
216
+    /**
217
+     * get the full permissions of a path.
218
+     * Should return a combination of the PERMISSION_ constants defined in lib/public/constants.php
219
+     *
220
+     * @param string $path
221
+     * @return int
222
+     */
223
+    public function getPermissions($path) {
224
+        return $this->getWrapperStorage()->getPermissions($this->getUnjailedPath($path));
225
+    }
226
+
227
+    /**
228
+     * see https://www.php.net/manual/en/function.file_exists.php
229
+     *
230
+     * @param string $path
231
+     * @return bool
232
+     */
233
+    public function file_exists($path) {
234
+        return $this->getWrapperStorage()->file_exists($this->getUnjailedPath($path));
235
+    }
236
+
237
+    /**
238
+     * see https://www.php.net/manual/en/function.filemtime.php
239
+     *
240
+     * @param string $path
241
+     * @return int|bool
242
+     */
243
+    public function filemtime($path) {
244
+        return $this->getWrapperStorage()->filemtime($this->getUnjailedPath($path));
245
+    }
246
+
247
+    /**
248
+     * see https://www.php.net/manual/en/function.file_get_contents.php
249
+     *
250
+     * @param string $path
251
+     * @return string|false
252
+     */
253
+    public function file_get_contents($path) {
254
+        return $this->getWrapperStorage()->file_get_contents($this->getUnjailedPath($path));
255
+    }
256
+
257
+    /**
258
+     * see https://www.php.net/manual/en/function.file_put_contents.php
259
+     *
260
+     * @param string $path
261
+     * @param mixed $data
262
+     * @return int|float|false
263
+     */
264
+    public function file_put_contents($path, $data) {
265
+        return $this->getWrapperStorage()->file_put_contents($this->getUnjailedPath($path), $data);
266
+    }
267
+
268
+    /**
269
+     * see https://www.php.net/manual/en/function.unlink.php
270
+     *
271
+     * @param string $path
272
+     * @return bool
273
+     */
274
+    public function unlink($path) {
275
+        return $this->getWrapperStorage()->unlink($this->getUnjailedPath($path));
276
+    }
277
+
278
+    /**
279
+     * see https://www.php.net/manual/en/function.rename.php
280
+     *
281
+     * @param string $source
282
+     * @param string $target
283
+     * @return bool
284
+     */
285
+    public function rename($source, $target) {
286
+        return $this->getWrapperStorage()->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target));
287
+    }
288
+
289
+    /**
290
+     * see https://www.php.net/manual/en/function.copy.php
291
+     *
292
+     * @param string $source
293
+     * @param string $target
294
+     * @return bool
295
+     */
296
+    public function copy($source, $target) {
297
+        return $this->getWrapperStorage()->copy($this->getUnjailedPath($source), $this->getUnjailedPath($target));
298
+    }
299
+
300
+    /**
301
+     * see https://www.php.net/manual/en/function.fopen.php
302
+     *
303
+     * @param string $path
304
+     * @param string $mode
305
+     * @return resource|bool
306
+     */
307
+    public function fopen($path, $mode) {
308
+        return $this->getWrapperStorage()->fopen($this->getUnjailedPath($path), $mode);
309
+    }
310
+
311
+    /**
312
+     * get the mimetype for a file or folder
313
+     * The mimetype for a folder is required to be "httpd/unix-directory"
314
+     *
315
+     * @param string $path
316
+     * @return string|bool
317
+     */
318
+    public function getMimeType($path) {
319
+        return $this->getWrapperStorage()->getMimeType($this->getUnjailedPath($path));
320
+    }
321
+
322
+    /**
323
+     * see https://www.php.net/manual/en/function.hash.php
324
+     *
325
+     * @param string $type
326
+     * @param string $path
327
+     * @param bool $raw
328
+     * @return string|bool
329
+     */
330
+    public function hash($type, $path, $raw = false) {
331
+        return $this->getWrapperStorage()->hash($type, $this->getUnjailedPath($path), $raw);
332
+    }
333
+
334
+    /**
335
+     * see https://www.php.net/manual/en/function.free_space.php
336
+     *
337
+     * @param string $path
338
+     * @return int|float|bool
339
+     */
340
+    public function free_space($path) {
341
+        return $this->getWrapperStorage()->free_space($this->getUnjailedPath($path));
342
+    }
343
+
344
+    /**
345
+     * search for occurrences of $query in file names
346
+     *
347
+     * @param string $query
348
+     * @return array|bool
349
+     */
350
+    public function search($query) {
351
+        return $this->getWrapperStorage()->search($query);
352
+    }
353
+
354
+    /**
355
+     * see https://www.php.net/manual/en/function.touch.php
356
+     * If the backend does not support the operation, false should be returned
357
+     *
358
+     * @param string $path
359
+     * @param int $mtime
360
+     * @return bool
361
+     */
362
+    public function touch($path, $mtime = null) {
363
+        return $this->getWrapperStorage()->touch($this->getUnjailedPath($path), $mtime);
364
+    }
365
+
366
+    /**
367
+     * get the path to a local version of the file.
368
+     * The local version of the file can be temporary and doesn't have to be persistent across requests
369
+     *
370
+     * @param string $path
371
+     * @return string|false
372
+     */
373
+    public function getLocalFile($path) {
374
+        return $this->getWrapperStorage()->getLocalFile($this->getUnjailedPath($path));
375
+    }
376
+
377
+    /**
378
+     * check if a file or folder has been updated since $time
379
+     *
380
+     * @param string $path
381
+     * @param int $time
382
+     * @return bool
383
+     *
384
+     * hasUpdated for folders should return at least true if a file inside the folder is add, removed or renamed.
385
+     * returning true for other changes in the folder is optional
386
+     */
387
+    public function hasUpdated($path, $time) {
388
+        return $this->getWrapperStorage()->hasUpdated($this->getUnjailedPath($path), $time);
389
+    }
390
+
391
+    /**
392
+     * get a cache instance for the storage
393
+     *
394
+     * @param string $path
395
+     * @param \OC\Files\Storage\Storage|null (optional) the storage to pass to the cache
396
+     * @return \OC\Files\Cache\Cache
397
+     */
398
+    public function getCache($path = '', $storage = null) {
399
+        if (!$storage) {
400
+            $storage = $this->getWrapperStorage();
401
+        }
402
+        $sourceCache = $this->getWrapperStorage()->getCache($this->getUnjailedPath($path), $storage);
403
+        return new CacheJail($sourceCache, $this->rootPath);
404
+    }
405
+
406
+    /**
407
+     * get the user id of the owner of a file or folder
408
+     *
409
+     * @param string $path
410
+     * @return string
411
+     */
412
+    public function getOwner($path) {
413
+        return $this->getWrapperStorage()->getOwner($this->getUnjailedPath($path));
414
+    }
415
+
416
+    /**
417
+     * get a watcher instance for the cache
418
+     *
419
+     * @param string $path
420
+     * @param \OC\Files\Storage\Storage (optional) the storage to pass to the watcher
421
+     * @return \OC\Files\Cache\Watcher
422
+     */
423
+    public function getWatcher($path = '', $storage = null) {
424
+        if (!$storage) {
425
+            $storage = $this;
426
+        }
427
+        return $this->getWrapperStorage()->getWatcher($this->getUnjailedPath($path), $storage);
428
+    }
429
+
430
+    /**
431
+     * get the ETag for a file or folder
432
+     *
433
+     * @param string $path
434
+     * @return string|false
435
+     */
436
+    public function getETag($path) {
437
+        return $this->getWrapperStorage()->getETag($this->getUnjailedPath($path));
438
+    }
439
+
440
+    public function getMetaData($path) {
441
+        return $this->getWrapperStorage()->getMetaData($this->getUnjailedPath($path));
442
+    }
443
+
444
+    /**
445
+     * @param string $path
446
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
447
+     * @param \OCP\Lock\ILockingProvider $provider
448
+     * @throws \OCP\Lock\LockedException
449
+     */
450
+    public function acquireLock($path, $type, ILockingProvider $provider) {
451
+        $this->getWrapperStorage()->acquireLock($this->getUnjailedPath($path), $type, $provider);
452
+    }
453
+
454
+    /**
455
+     * @param string $path
456
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
457
+     * @param \OCP\Lock\ILockingProvider $provider
458
+     */
459
+    public function releaseLock($path, $type, ILockingProvider $provider) {
460
+        $this->getWrapperStorage()->releaseLock($this->getUnjailedPath($path), $type, $provider);
461
+    }
462
+
463
+    /**
464
+     * @param string $path
465
+     * @param int $type \OCP\Lock\ILockingProvider::LOCK_SHARED or \OCP\Lock\ILockingProvider::LOCK_EXCLUSIVE
466
+     * @param \OCP\Lock\ILockingProvider $provider
467
+     */
468
+    public function changeLock($path, $type, ILockingProvider $provider) {
469
+        $this->getWrapperStorage()->changeLock($this->getUnjailedPath($path), $type, $provider);
470
+    }
471
+
472
+    /**
473
+     * Resolve the path for the source of the share
474
+     *
475
+     * @param string $path
476
+     * @return array
477
+     */
478
+    public function resolvePath($path) {
479
+        return [$this->getWrapperStorage(), $this->getUnjailedPath($path)];
480
+    }
481
+
482
+    /**
483
+     * @param IStorage $sourceStorage
484
+     * @param string $sourceInternalPath
485
+     * @param string $targetInternalPath
486
+     * @return bool
487
+     */
488
+    public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
489
+        if ($sourceStorage === $this) {
490
+            return $this->copy($sourceInternalPath, $targetInternalPath);
491
+        }
492
+        return $this->getWrapperStorage()->copyFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
493
+    }
494
+
495
+    /**
496
+     * @param IStorage $sourceStorage
497
+     * @param string $sourceInternalPath
498
+     * @param string $targetInternalPath
499
+     * @return bool
500
+     */
501
+    public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
502
+        if ($sourceStorage === $this) {
503
+            return $this->rename($sourceInternalPath, $targetInternalPath);
504
+        }
505
+        return $this->getWrapperStorage()->moveFromStorage($sourceStorage, $sourceInternalPath, $this->getUnjailedPath($targetInternalPath));
506
+    }
507
+
508
+    public function getPropagator($storage = null) {
509
+        if (isset($this->propagator)) {
510
+            return $this->propagator;
511
+        }
512
+
513
+        if (!$storage) {
514
+            $storage = $this;
515
+        }
516
+        $this->propagator = new JailPropagator($storage, \OC::$server->getDatabaseConnection());
517
+        return $this->propagator;
518
+    }
519
+
520
+    public function writeStream(string $path, $stream, int $size = null): int {
521
+        $storage = $this->getWrapperStorage();
522
+        if ($storage->instanceOfStorage(IWriteStreamStorage::class)) {
523
+            /** @var IWriteStreamStorage $storage */
524
+            return $storage->writeStream($this->getUnjailedPath($path), $stream, $size);
525
+        } else {
526
+            $target = $this->fopen($path, 'w');
527
+            [$count, $result] = \OC_Helper::streamCopy($stream, $target);
528
+            fclose($stream);
529
+            fclose($target);
530
+            return $count;
531
+        }
532
+    }
533
+
534
+    public function getDirectoryContent($directory): \Traversable {
535
+        return $this->getWrapperStorage()->getDirectoryContent($this->getUnjailedPath($directory));
536
+    }
537 537
 }
Please login to merge, or discard this patch.
Spacing   +3 added lines, -3 removed lines patch added patch discarded remove patch
@@ -58,7 +58,7 @@  discard block
 block discarded – undo
58 58
 	}
59 59
 
60 60
 	public function getUnjailedPath($path) {
61
-		return trim(Filesystem::normalizePath($this->rootPath . '/' . $path), '/');
61
+		return trim(Filesystem::normalizePath($this->rootPath.'/'.$path), '/');
62 62
 	}
63 63
 
64 64
 	/**
@@ -70,7 +70,7 @@  discard block
 block discarded – undo
70 70
 
71 71
 
72 72
 	public function getJailedPath($path) {
73
-		$root = rtrim($this->rootPath, '/') . '/';
73
+		$root = rtrim($this->rootPath, '/').'/';
74 74
 
75 75
 		if ($path !== $this->rootPath && !str_starts_with($path, $root)) {
76 76
 			return null;
@@ -159,7 +159,7 @@  discard block
 block discarded – undo
159 159
 	 * see https://www.php.net/manual/en/function.filesize.php
160 160
 	 * The result for filesize when called on a folder is required to be 0
161 161
 	 */
162
-	public function filesize($path): false|int|float {
162
+	public function filesize($path): false | int | float {
163 163
 		return $this->getWrapperStorage()->filesize($this->getUnjailedPath($path));
164 164
 	}
165 165
 
Please login to merge, or discard this patch.