1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* @author Andreas Fischer <[email protected]> |
4
|
|
|
* @author Björn Schießle <[email protected]> |
5
|
|
|
* @author Florin Peter <[email protected]> |
6
|
|
|
* @author Jens-Christian Fischer <[email protected]> |
7
|
|
|
* @author Joas Schilling <[email protected]> |
8
|
|
|
* @author Jörn Friedrich Dreyer <[email protected]> |
9
|
|
|
* @author Lukas Reschke <[email protected]> |
10
|
|
|
* @author Michael Gapczynski <[email protected]> |
11
|
|
|
* @author Morris Jobke <[email protected]> |
12
|
|
|
* @author Robin Appelman <[email protected]> |
13
|
|
|
* @author Robin McCorkell <[email protected]> |
14
|
|
|
* @author Roeland Jago Douma <[email protected]> |
15
|
|
|
* @author TheSFReader <[email protected]> |
16
|
|
|
* @author Thomas Müller <[email protected]> |
17
|
|
|
* @author Vincent Petry <[email protected]> |
18
|
|
|
* |
19
|
|
|
* @copyright Copyright (c) 2018, ownCloud GmbH |
20
|
|
|
* @license AGPL-3.0 |
21
|
|
|
* |
22
|
|
|
* This code is free software: you can redistribute it and/or modify |
23
|
|
|
* it under the terms of the GNU Affero General Public License, version 3, |
24
|
|
|
* as published by the Free Software Foundation. |
25
|
|
|
* |
26
|
|
|
* This program is distributed in the hope that it will be useful, |
27
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
28
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
29
|
|
|
* GNU Affero General Public License for more details. |
30
|
|
|
* |
31
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3, |
32
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/> |
33
|
|
|
* |
34
|
|
|
*/ |
35
|
|
|
|
36
|
|
|
namespace OC\Files\Cache; |
37
|
|
|
|
38
|
|
|
use Doctrine\DBAL\Platforms\OraclePlatform; |
39
|
|
|
use OCP\Files\Cache\ICache; |
40
|
|
|
use OCP\Files\Cache\ICacheEntry; |
41
|
|
|
use \OCP\Files\IMimeTypeLoader; |
42
|
|
|
use OCP\IDBConnection; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* Metadata cache for a storage |
46
|
|
|
* |
47
|
|
|
* The cache stores the metadata for all files and folders in a storage and is kept up to date trough the following mechanisms: |
48
|
|
|
* |
49
|
|
|
* - Scanner: scans the storage and updates the cache where needed |
50
|
|
|
* - Watcher: checks for changes made to the filesystem outside of the ownCloud instance and rescans files and folder when a change is detected |
51
|
|
|
* - Updater: listens to changes made to the filesystem inside of the ownCloud instance and updates the cache where needed |
52
|
|
|
* - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater |
53
|
|
|
*/ |
54
|
|
|
class Cache implements ICache { |
55
|
|
|
use MoveFromCacheTrait { |
56
|
|
|
MoveFromCacheTrait::moveFromCache as moveFromCacheFallback; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var array partial data for the cache |
61
|
|
|
*/ |
62
|
|
|
protected $partial = []; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var string |
66
|
|
|
*/ |
67
|
|
|
protected $storageId; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var Storage $storageCache |
71
|
|
|
*/ |
72
|
|
|
protected $storageCache; |
73
|
|
|
|
74
|
|
|
/** @var IMimeTypeLoader */ |
75
|
|
|
protected $mimetypeLoader; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @var IDBConnection |
79
|
|
|
*/ |
80
|
|
|
protected $connection; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @param \OC\Files\Storage\Storage|string $storage |
84
|
|
|
*/ |
85
|
|
|
public function __construct($storage) { |
86
|
|
|
if ($storage instanceof \OC\Files\Storage\Storage) { |
87
|
|
|
$this->storageId = $storage->getId(); |
88
|
|
|
} else { |
89
|
|
|
$this->storageId = $storage; |
90
|
|
|
} |
91
|
|
|
if (\strlen($this->storageId) > 64) { |
92
|
|
|
$this->storageId = \md5($this->storageId); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
$this->storageCache = new Storage($storage); |
96
|
|
|
$this->mimetypeLoader = \OC::$server->getMimeTypeLoader(); |
97
|
|
|
$this->connection = \OC::$server->getDatabaseConnection(); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Get the numeric storage id for this cache's storage |
102
|
|
|
* |
103
|
|
|
* @return int |
104
|
|
|
*/ |
105
|
|
|
public function getNumericStorageId() { |
106
|
|
|
return $this->storageCache->getNumericId(); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* get the stored metadata of a file or folder |
111
|
|
|
* |
112
|
|
|
* @param string | int $file either the path of a file or folder or the file id for a file or folder |
113
|
|
|
* @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache |
114
|
|
|
*/ |
115
|
|
|
public function get($file) { |
116
|
|
|
if (\is_string($file) or $file == '') { |
117
|
|
|
// normalize file |
118
|
|
|
$file = $this->normalize($file); |
119
|
|
|
|
120
|
|
|
$where = 'WHERE `storage` = ? AND `path_hash` = ?'; |
121
|
|
|
$params = [$this->getNumericStorageId(), \md5($file)]; |
122
|
|
|
} else { //file id |
123
|
|
|
$where = 'WHERE `fileid` = ?'; |
124
|
|
|
$params = [$file]; |
125
|
|
|
} |
126
|
|
|
$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, |
127
|
|
|
`storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum` |
128
|
|
|
FROM `*PREFIX*filecache` ' . $where; |
129
|
|
|
$result = $this->connection->executeQuery($sql, $params); |
130
|
|
|
$data = $result->fetch(); |
131
|
|
|
|
132
|
|
|
//FIXME hide this HACK in the next database layer, or just use doctrine and get rid of MDB2 and PDO |
133
|
|
|
//PDO returns false, MDB2 returns null, oracle always uses MDB2, so convert null to false |
134
|
|
|
if ($data === null) { |
135
|
|
|
$data = false; |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
//merge partial data |
139
|
|
|
if ($data) { |
140
|
|
|
//fix types |
141
|
|
|
$data['fileid'] = (int)$data['fileid']; |
142
|
|
|
$data['parent'] = (int)$data['parent']; |
143
|
|
|
$data['size'] = 0 + $data['size']; |
144
|
|
|
$data['mtime'] = (int)$data['mtime']; |
145
|
|
|
$data['storage_mtime'] = (int)$data['storage_mtime']; |
146
|
|
|
$data['encryptedVersion'] = (int)$data['encrypted']; |
147
|
|
|
$data['encrypted'] = (bool)$data['encrypted']; |
148
|
|
|
$data['storage'] = $this->storageId; |
149
|
|
|
$data['mimetype'] = $this->mimetypeLoader->getMimetypeById($data['mimetype']); |
150
|
|
|
$data['mimepart'] = $this->mimetypeLoader->getMimetypeById($data['mimepart']); |
151
|
|
|
if ($data['storage_mtime'] == 0) { |
152
|
|
|
$data['storage_mtime'] = $data['mtime']; |
153
|
|
|
} |
154
|
|
|
$data['permissions'] = (int)$data['permissions']; |
155
|
|
|
// Oracle stores empty strings as null... |
156
|
|
|
if ($data['name'] === null) { |
157
|
|
|
$data['name'] = ''; |
158
|
|
|
} |
159
|
|
|
if ($data['path'] === null) { |
160
|
|
|
$data['path'] = ''; |
161
|
|
|
} |
162
|
|
|
return new CacheEntry($data); |
163
|
|
|
} elseif (!$data and \is_string($file)) { |
164
|
|
|
if (isset($this->partial[$file])) { |
165
|
|
|
$data = $this->partial[$file]; |
166
|
|
|
} |
167
|
|
|
return $data; |
168
|
|
|
} else { |
169
|
|
|
return false; |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* get the metadata of all files stored in $folder |
175
|
|
|
* |
176
|
|
|
* @param string $folder |
177
|
|
|
* @return ICacheEntry[] |
178
|
|
|
*/ |
179
|
|
|
public function getFolderContents($folder) { |
180
|
|
|
$fileId = $this->getId($folder); |
181
|
|
|
return $this->getFolderContentsById($fileId); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
/** |
185
|
|
|
* get the metadata of all files stored in $folder |
186
|
|
|
* |
187
|
|
|
* @param int $fileId the file id of the folder |
188
|
|
|
* @return ICacheEntry[] |
189
|
|
|
*/ |
190
|
|
|
public function getFolderContentsById($fileId) { |
191
|
|
|
if ($fileId > -1) { |
192
|
|
|
$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, |
193
|
|
|
`storage_mtime`, `encrypted`, `etag`, `permissions`, `checksum` |
194
|
|
|
FROM `*PREFIX*filecache` WHERE `parent` = ? ORDER BY `name` ASC'; |
195
|
|
|
$result = $this->connection->executeQuery($sql, [$fileId]); |
196
|
|
|
$files = $result->fetchAll(); |
197
|
|
|
foreach ($files as &$file) { |
198
|
|
|
$file['mimetype'] = $this->mimetypeLoader->getMimetypeById($file['mimetype']); |
199
|
|
|
$file['mimepart'] = $this->mimetypeLoader->getMimetypeById($file['mimepart']); |
200
|
|
|
if ($file['storage_mtime'] == 0) { |
201
|
|
|
$file['storage_mtime'] = $file['mtime']; |
202
|
|
|
} |
203
|
|
|
$file['permissions'] = (int)$file['permissions']; |
204
|
|
|
$file['mtime'] = (int)$file['mtime']; |
205
|
|
|
$file['storage_mtime'] = (int)$file['storage_mtime']; |
206
|
|
|
$file['size'] = 0 + $file['size']; |
207
|
|
|
} |
208
|
|
|
return \array_map(function (array $data) { |
209
|
|
|
return new CacheEntry($data); |
210
|
|
|
}, $files); |
211
|
|
|
} else { |
212
|
|
|
return []; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* insert or update meta data for a file or folder |
218
|
|
|
* |
219
|
|
|
* @param string $file |
220
|
|
|
* @param array $data |
221
|
|
|
* |
222
|
|
|
* @return int file id |
223
|
|
|
* @throws \RuntimeException |
224
|
|
|
*/ |
225
|
|
View Code Duplication |
public function put($file, array $data) { |
226
|
|
|
if (($id = $this->getId($file)) > -1) { |
227
|
|
|
$this->update($id, $data); |
228
|
|
|
return $id; |
229
|
|
|
} else { |
230
|
|
|
return $this->insert($file, $data); |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* insert meta data for a new file or folder |
236
|
|
|
* |
237
|
|
|
* @param string $file |
238
|
|
|
* @param array $data |
239
|
|
|
* |
240
|
|
|
* @return int file id |
241
|
|
|
* @throws \RuntimeException |
242
|
|
|
*/ |
243
|
|
|
public function insert($file, array $data) { |
244
|
|
|
// normalize file |
245
|
|
|
$file = $this->normalize($file); |
246
|
|
|
|
247
|
|
|
if (isset($this->partial[$file])) { //add any saved partial data |
248
|
|
|
$data = \array_merge($this->partial[$file], $data); |
249
|
|
|
unset($this->partial[$file]); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$requiredFields = ['size', 'mtime', 'mimetype']; |
253
|
|
|
foreach ($requiredFields as $field) { |
254
|
|
|
if (!isset($data[$field])) { //data not complete save as partial and return |
255
|
|
|
$this->partial[$file] = $data; |
256
|
|
|
return -1; |
257
|
|
|
} |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
$data['path'] = $file; |
261
|
|
|
$data['parent'] = $this->getParentId($file); |
262
|
|
|
$data['name'] = \OC_Util::basename($file); |
263
|
|
|
|
264
|
|
|
list($queryParts, $params) = $this->buildParts($data); |
265
|
|
|
$queryParts[] = '`storage`'; |
266
|
|
|
$params[] = $this->getNumericStorageId(); |
267
|
|
|
|
268
|
|
|
$queryParts = \array_map(function ($item) { |
269
|
|
|
return \trim($item, "`"); |
270
|
|
|
}, $queryParts); |
271
|
|
|
$values = \array_combine($queryParts, $params); |
272
|
|
|
// Update or insert this to the filecache |
273
|
|
|
\OC::$server->getDatabaseConnection()->upsert( |
274
|
|
|
'*PREFIX*filecache', |
275
|
|
|
$values, |
276
|
|
|
[ |
277
|
|
|
'storage', |
278
|
|
|
'path_hash', |
279
|
|
|
] |
280
|
|
|
); |
281
|
|
|
// Now return the id for this row - crappy that we have to select here |
282
|
|
|
// GetID should already return a value if upsert returned a positive value |
283
|
|
|
return (int)$this->getId($file); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* update the metadata of an existing file or folder in the cache |
288
|
|
|
* |
289
|
|
|
* @param int $id the fileid of the existing file or folder |
290
|
|
|
* @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 |
291
|
|
|
*/ |
292
|
|
|
public function update($id, array $data) { |
293
|
|
|
if (isset($data['path'])) { |
294
|
|
|
// normalize path |
295
|
|
|
$data['path'] = $this->normalize($data['path']); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (isset($data['name'])) { |
299
|
|
|
// normalize path |
300
|
|
|
$data['name'] = $this->normalize($data['name']); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
list($queryParts, $params) = $this->buildParts($data); |
304
|
|
|
// duplicate $params because we need the parts twice in the SQL statement |
305
|
|
|
// once for the SET part, once in the WHERE clause |
306
|
|
|
$params = \array_merge($params, $params); |
307
|
|
|
$params[] = $id; |
308
|
|
|
|
309
|
|
|
// Oracle does not support empty string values so we convert them to nulls |
310
|
|
|
// https://github.com/owncloud/core/issues/31692 |
311
|
|
|
if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { |
312
|
|
|
foreach ($data as $param => $value) { |
313
|
|
|
if ($value === '') { |
314
|
|
|
$data[$param] = null; |
315
|
|
|
} |
316
|
|
|
} |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
// don't update if the data we try to set is the same as the one in the record |
320
|
|
|
// some databases (Postgres) don't like superfluous updates |
321
|
|
|
$sql = 'UPDATE `*PREFIX*filecache` SET ' . \implode(' = ?, ', $queryParts) . '=? ' . |
322
|
|
|
'WHERE (' . |
323
|
|
|
\implode(' <> ? OR ', $queryParts) . ' <> ? OR ' . |
324
|
|
|
\implode(' IS NULL OR ', $queryParts) . ' IS NULL' . |
325
|
|
|
') AND `fileid` = ? '; |
326
|
|
|
$this->connection->executeQuery($sql, $params); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* extract query parts and params array from data array |
331
|
|
|
* |
332
|
|
|
* @param array $data |
333
|
|
|
* @return array [$queryParts, $params] |
334
|
|
|
* $queryParts: string[], the (escaped) column names to be set in the query |
335
|
|
|
* $params: mixed[], the new values for the columns, to be passed as params to the query |
336
|
|
|
*/ |
337
|
|
|
protected function buildParts(array $data) { |
338
|
|
|
$fields = [ |
339
|
|
|
'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted', |
340
|
|
|
'etag', 'permissions', 'checksum']; |
341
|
|
|
|
342
|
|
|
$doNotCopyStorageMTime = false; |
343
|
|
|
if (\array_key_exists('mtime', $data) && $data['mtime'] === null) { |
344
|
|
|
// this horrific magic tells it to not copy storage_mtime to mtime |
345
|
|
|
unset($data['mtime']); |
346
|
|
|
$doNotCopyStorageMTime = true; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
$params = []; |
350
|
|
|
$queryParts = []; |
351
|
|
|
foreach ($data as $name => $value) { |
352
|
|
|
if (\array_search($name, $fields) !== false) { |
353
|
|
|
if ($name === 'path') { |
354
|
|
|
$params[] = \md5($value); |
355
|
|
|
$queryParts[] = '`path_hash`'; |
356
|
|
|
} elseif ($name === 'mimetype') { |
357
|
|
|
$params[] = $this->mimetypeLoader->getId(\substr($value, 0, \strpos($value, '/'))); |
358
|
|
|
$queryParts[] = '`mimepart`'; |
359
|
|
|
$value = $this->mimetypeLoader->getId($value); |
360
|
|
|
} elseif ($name === 'storage_mtime') { |
361
|
|
|
if (!$doNotCopyStorageMTime && !isset($data['mtime'])) { |
362
|
|
|
$params[] = $value; |
363
|
|
|
$queryParts[] = '`mtime`'; |
364
|
|
|
} |
365
|
|
|
} elseif ($name === 'encrypted') { |
366
|
|
|
if (isset($data['encryptedVersion'])) { |
367
|
|
|
$value = $data['encryptedVersion']; |
368
|
|
|
} else { |
369
|
|
|
// Boolean to integer conversion |
370
|
|
|
$value = $value ? 1 : 0; |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
$params[] = $value; |
374
|
|
|
$queryParts[] = '`' . $name . '`'; |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
return [$queryParts, $params]; |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* get the file id for a file |
382
|
|
|
* |
383
|
|
|
* 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 |
384
|
|
|
* |
385
|
|
|
* File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing |
386
|
|
|
* |
387
|
|
|
* @param string $file |
388
|
|
|
* @return int |
389
|
|
|
*/ |
390
|
|
|
public function getId($file) { |
391
|
|
|
// normalize file |
392
|
|
|
$file = $this->normalize($file); |
393
|
|
|
|
394
|
|
|
$pathHash = \md5($file); |
395
|
|
|
|
396
|
|
|
$sql = 'SELECT `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?'; |
397
|
|
|
$result = $this->connection->executeQuery($sql, [$this->getNumericStorageId(), $pathHash]); |
398
|
|
|
if ($row = $result->fetch()) { |
399
|
|
|
return $row['fileid']; |
400
|
|
|
} else { |
401
|
|
|
return -1; |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* get the id of the parent folder of a file |
407
|
|
|
* |
408
|
|
|
* @param string $file |
409
|
|
|
* @return int |
410
|
|
|
*/ |
411
|
|
|
public function getParentId($file) { |
412
|
|
|
if ($file === '') { |
413
|
|
|
return -1; |
414
|
|
|
} else { |
415
|
|
|
$parent = $this->getParentPath($file); |
416
|
|
|
return (int)$this->getId($parent); |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
private function getParentPath($path) { |
421
|
|
|
$parent = \dirname($path); |
422
|
|
|
if ($parent === '.') { |
423
|
|
|
$parent = ''; |
424
|
|
|
} |
425
|
|
|
return $parent; |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* check if a file is available in the cache |
430
|
|
|
* |
431
|
|
|
* @param string $file |
432
|
|
|
* @return bool |
433
|
|
|
*/ |
434
|
|
|
public function inCache($file) { |
435
|
|
|
return $this->getId($file) != -1; |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
/** |
439
|
|
|
* remove a file or folder from the cache |
440
|
|
|
* |
441
|
|
|
* when removing a folder from the cache all files and folders inside the folder will be removed as well |
442
|
|
|
* |
443
|
|
|
* @param string $file |
444
|
|
|
*/ |
445
|
|
View Code Duplication |
public function remove($file) { |
446
|
|
|
$entry = $this->get($file); |
447
|
|
|
$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `fileid` = ?'; |
448
|
|
|
$this->connection->executeQuery($sql, [$entry['fileid']]); |
449
|
|
|
if ($entry['mimetype'] === 'httpd/unix-directory') { |
450
|
|
|
$this->removeChildren($entry); |
|
|
|
|
451
|
|
|
} |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* Get all sub folders of a folder |
456
|
|
|
* |
457
|
|
|
* @param array $entry the cache entry of the folder to get the subfolders for |
458
|
|
|
* @return array[] the cache entries for the subfolders |
459
|
|
|
*/ |
460
|
|
|
private function getSubFolders($entry) { |
461
|
|
|
$children = $this->getFolderContentsById($entry['fileid']); |
462
|
|
|
return \array_filter($children, function ($child) { |
463
|
|
|
return $child['mimetype'] === 'httpd/unix-directory'; |
464
|
|
|
}); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* Recursively remove all children of a folder |
469
|
|
|
* |
470
|
|
|
* @param array $entry the cache entry of the folder to remove the children of |
471
|
|
|
* @throws \OC\DatabaseException |
472
|
|
|
*/ |
473
|
|
View Code Duplication |
private function removeChildren($entry) { |
474
|
|
|
$subFolders = $this->getSubFolders($entry); |
475
|
|
|
foreach ($subFolders as $folder) { |
476
|
|
|
$this->removeChildren($folder); |
477
|
|
|
} |
478
|
|
|
$sql = 'DELETE FROM `*PREFIX*filecache` WHERE `parent` = ?'; |
479
|
|
|
$this->connection->executeQuery($sql, [$entry['fileid']]); |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Move a file or folder in the cache |
484
|
|
|
* |
485
|
|
|
* @param string $source |
486
|
|
|
* @param string $target |
487
|
|
|
*/ |
488
|
|
|
public function move($source, $target) { |
489
|
|
|
$this->moveFromCache($this, $source, $target); |
490
|
|
|
} |
491
|
|
|
|
492
|
|
|
/** |
493
|
|
|
* Get the storage id and path needed for a move |
494
|
|
|
* |
495
|
|
|
* @param string $path |
496
|
|
|
* @return array [$storageId, $internalPath] |
497
|
|
|
*/ |
498
|
|
|
protected function getMoveInfo($path) { |
499
|
|
|
return [$this->getNumericStorageId(), $path]; |
500
|
|
|
} |
501
|
|
|
|
502
|
|
|
/** |
503
|
|
|
* Move a file or folder in the cache |
504
|
|
|
* |
505
|
|
|
* @param \OCP\Files\Cache\ICache $sourceCache |
506
|
|
|
* @param string $sourcePath |
507
|
|
|
* @param string $targetPath |
508
|
|
|
* @throws \OC\DatabaseException |
509
|
|
|
* @throws \Exception if the given storages have an invalid id |
510
|
|
|
*/ |
511
|
|
|
public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) { |
512
|
|
|
if ($sourceCache instanceof Cache) { |
513
|
|
|
// normalize source and target |
514
|
|
|
$sourcePath = $this->normalize($sourcePath); |
515
|
|
|
$targetPath = $this->normalize($targetPath); |
516
|
|
|
|
517
|
|
|
$sourceData = $sourceCache->get($sourcePath); |
518
|
|
|
$sourceId = $sourceData['fileid']; |
519
|
|
|
$newParentId = $this->getParentId($targetPath); |
520
|
|
|
|
521
|
|
|
list($sourceStorageId, $sourcePath) = $sourceCache->getMoveInfo($sourcePath); |
522
|
|
|
list($targetStorageId, $targetPath) = $this->getMoveInfo($targetPath); |
523
|
|
|
|
524
|
|
|
if ($sourceStorageId === null || $sourceStorageId === false) { |
525
|
|
|
throw new \Exception('Invalid source storage id: ' . $sourceStorageId); |
526
|
|
|
} |
527
|
|
|
if ($targetStorageId === null || $targetStorageId === false) { |
528
|
|
|
throw new \Exception('Invalid target storage id: ' . $targetStorageId); |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
// sql for final update |
532
|
|
|
$moveSql = 'UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ?, `name` = ?, `parent` =? WHERE `fileid` = ?'; |
533
|
|
|
|
534
|
|
|
if ($sourceData['mimetype'] === 'httpd/unix-directory') { |
535
|
|
|
//find all child entries |
536
|
|
|
$sql = 'SELECT `path`, `fileid` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path` LIKE ?'; |
537
|
|
|
$result = $this->connection->executeQuery($sql, [$sourceStorageId, $this->connection->escapeLikeParameter($sourcePath) . '/%']); |
538
|
|
|
$childEntries = $result->fetchAll(); |
539
|
|
|
$sourceLength = \strlen($sourcePath); |
540
|
|
|
$this->connection->beginTransaction(); |
541
|
|
|
$query = $this->connection->prepare('UPDATE `*PREFIX*filecache` SET `storage` = ?, `path` = ?, `path_hash` = ? WHERE `fileid` = ?'); |
542
|
|
|
|
543
|
|
|
foreach ($childEntries as $child) { |
544
|
|
|
$newTargetPath = $targetPath . \substr($child['path'], $sourceLength); |
545
|
|
|
$query->execute([$targetStorageId, $newTargetPath, \md5($newTargetPath), $child['fileid']]); |
546
|
|
|
} |
547
|
|
|
$this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, \md5($targetPath), \basename($targetPath), $newParentId, $sourceId]); |
548
|
|
|
$this->connection->commit(); |
549
|
|
|
} else { |
550
|
|
|
$this->connection->executeQuery($moveSql, [$targetStorageId, $targetPath, \md5($targetPath), \basename($targetPath), $newParentId, $sourceId]); |
551
|
|
|
} |
552
|
|
|
} else { |
553
|
|
|
$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath); |
554
|
|
|
} |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* remove all entries for files that are stored on the storage from the cache |
559
|
|
|
*/ |
560
|
|
|
public function clear() { |
561
|
|
|
Storage::remove($this->storageId); |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
/** |
565
|
|
|
* Get the scan status of a file |
566
|
|
|
* |
567
|
|
|
* - Cache::NOT_FOUND: File is not in the cache |
568
|
|
|
* - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known |
569
|
|
|
* - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned |
570
|
|
|
* - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned |
571
|
|
|
* |
572
|
|
|
* @param string $file |
573
|
|
|
* |
574
|
|
|
* @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE |
575
|
|
|
*/ |
576
|
|
|
public function getStatus($file) { |
577
|
|
|
// normalize file |
578
|
|
|
$file = $this->normalize($file); |
579
|
|
|
|
580
|
|
|
$pathHash = \md5($file); |
581
|
|
|
$sql = 'SELECT `size` FROM `*PREFIX*filecache` WHERE `storage` = ? AND `path_hash` = ?'; |
582
|
|
|
$result = $this->connection->executeQuery($sql, [$this->getNumericStorageId(), $pathHash]); |
583
|
|
|
if ($row = $result->fetch()) { |
584
|
|
|
if ((int)$row['size'] === -1) { |
585
|
|
|
return self::SHALLOW; |
586
|
|
|
} else { |
587
|
|
|
return self::COMPLETE; |
588
|
|
|
} |
589
|
|
|
} else { |
590
|
|
|
if (isset($this->partial[$file])) { |
591
|
|
|
return self::PARTIAL; |
592
|
|
|
} else { |
593
|
|
|
return self::NOT_FOUND; |
594
|
|
|
} |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* search for files matching $pattern |
600
|
|
|
* |
601
|
|
|
* @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%') |
602
|
|
|
* @return ICacheEntry[] an array of cache entries where the name matches the search pattern |
603
|
|
|
*/ |
604
|
|
|
public function search($pattern) { |
605
|
|
|
// normalize pattern |
606
|
|
|
$pattern = $this->normalize($pattern); |
607
|
|
|
|
608
|
|
|
$sql = ' |
609
|
|
|
SELECT `fileid`, `storage`, `path`, `parent`, `name`, |
610
|
|
|
`mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, |
611
|
|
|
`etag`, `permissions`, `checksum` |
612
|
|
|
FROM `*PREFIX*filecache` |
613
|
|
|
WHERE `storage` = ? AND `name` ILIKE ?'; |
614
|
|
|
$result = $this->connection->executeQuery($sql, |
615
|
|
|
[$this->getNumericStorageId(), $pattern] |
616
|
|
|
); |
617
|
|
|
|
618
|
|
|
$files = []; |
619
|
|
View Code Duplication |
while ($row = $result->fetch()) { |
|
|
|
|
620
|
|
|
$row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']); |
621
|
|
|
$row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']); |
622
|
|
|
$files[] = $row; |
623
|
|
|
} |
624
|
|
|
return \array_map(function (array $data) { |
625
|
|
|
return new CacheEntry($data); |
626
|
|
|
}, $files); |
627
|
|
|
} |
628
|
|
|
|
629
|
|
|
/** |
630
|
|
|
* search for files by mimetype |
631
|
|
|
* |
632
|
|
|
* @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image') |
633
|
|
|
* where it will search for all mimetypes in the group ('image/*') |
634
|
|
|
* @return ICacheEntry[] an array of cache entries where the mimetype matches the search |
635
|
|
|
*/ |
636
|
|
|
public function searchByMime($mimetype) { |
637
|
|
|
if (\strpos($mimetype, '/')) { |
638
|
|
|
$where = '`mimetype` = ?'; |
639
|
|
|
} else { |
640
|
|
|
$where = '`mimepart` = ?'; |
641
|
|
|
} |
642
|
|
|
$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, `mimetype`, `mimepart`, `size`, `mtime`, `encrypted`, `etag`, `permissions`, `checksum` |
643
|
|
|
FROM `*PREFIX*filecache` WHERE ' . $where . ' AND `storage` = ?'; |
644
|
|
|
$mimetype = $this->mimetypeLoader->getId($mimetype); |
645
|
|
|
$result = $this->connection->executeQuery($sql, [$mimetype, $this->getNumericStorageId()]); |
646
|
|
|
$files = []; |
647
|
|
View Code Duplication |
while ($row = $result->fetch()) { |
|
|
|
|
648
|
|
|
$row['mimetype'] = $this->mimetypeLoader->getMimetypeById($row['mimetype']); |
649
|
|
|
$row['mimepart'] = $this->mimetypeLoader->getMimetypeById($row['mimepart']); |
650
|
|
|
$files[] = $row; |
651
|
|
|
} |
652
|
|
|
return \array_map(function (array $data) { |
653
|
|
|
return new CacheEntry($data); |
654
|
|
|
}, $files); |
655
|
|
|
} |
656
|
|
|
|
657
|
|
|
/** |
658
|
|
|
* Search for files by tag of a given users. |
659
|
|
|
* |
660
|
|
|
* Note that every user can tag files differently. |
661
|
|
|
* |
662
|
|
|
* @param string|int $tag name or tag id |
663
|
|
|
* @param string $userId owner of the tags |
664
|
|
|
* @return ICacheEntry[] file data |
665
|
|
|
*/ |
666
|
|
|
public function searchByTag($tag, $userId) { |
667
|
|
|
$sql = 'SELECT `fileid`, `storage`, `path`, `parent`, `name`, ' . |
668
|
|
|
'`mimetype`, `mimepart`, `size`, `mtime`, ' . |
669
|
|
|
'`encrypted`, `etag`, `permissions`, `checksum` ' . |
670
|
|
|
'FROM `*PREFIX*filecache` `file`, ' . |
671
|
|
|
'`*PREFIX*vcategory_to_object` `tagmap`, ' . |
672
|
|
|
'`*PREFIX*vcategory` `tag` ' . |
673
|
|
|
// JOIN filecache to vcategory_to_object |
674
|
|
|
'WHERE `file`.`fileid` = `tagmap`.`objid` ' . |
675
|
|
|
// JOIN vcategory_to_object to vcategory |
676
|
|
|
'AND `tagmap`.`type` = `tag`.`type` ' . |
677
|
|
|
'AND `tagmap`.`categoryid` = `tag`.`id` ' . |
678
|
|
|
// conditions |
679
|
|
|
'AND `file`.`storage` = ? ' . |
680
|
|
|
'AND `tag`.`type` = \'files\' ' . |
681
|
|
|
'AND `tag`.`uid` = ? '; |
682
|
|
|
if (\is_int($tag)) { |
683
|
|
|
$sql .= 'AND `tag`.`id` = ? '; |
684
|
|
|
} else { |
685
|
|
|
$sql .= 'AND `tag`.`category` = ? '; |
686
|
|
|
} |
687
|
|
|
$result = $this->connection->executeQuery( |
688
|
|
|
$sql, |
689
|
|
|
[ |
690
|
|
|
$this->getNumericStorageId(), |
691
|
|
|
$userId, |
692
|
|
|
$tag |
693
|
|
|
] |
694
|
|
|
); |
695
|
|
|
$files = []; |
696
|
|
|
while ($row = $result->fetch()) { |
697
|
|
|
$files[] = $row; |
698
|
|
|
} |
699
|
|
|
return \array_map(function (array $data) { |
700
|
|
|
return new CacheEntry($data); |
701
|
|
|
}, $files); |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
/** |
705
|
|
|
* Re-calculate the folder size and the size of all parent folders |
706
|
|
|
* |
707
|
|
|
* @param string|boolean $path |
708
|
|
|
* @param array $data (optional) meta data of the folder |
709
|
|
|
*/ |
710
|
|
|
public function correctFolderSize($path, $data = null) { |
711
|
|
|
$this->calculateFolderSize($path, $data); |
|
|
|
|
712
|
|
|
if ($path !== '') { |
713
|
|
|
$parent = \dirname($path); |
714
|
|
|
if ($parent === '.' or $parent === '/') { |
715
|
|
|
$parent = ''; |
716
|
|
|
} |
717
|
|
|
$this->correctFolderSize($parent); |
718
|
|
|
} |
719
|
|
|
} |
720
|
|
|
|
721
|
|
|
/** |
722
|
|
|
* calculate the size of a folder and set it in the cache |
723
|
|
|
* |
724
|
|
|
* @param string $path |
725
|
|
|
* @param array $entry (optional) meta data of the folder |
726
|
|
|
* @return int |
727
|
|
|
*/ |
728
|
|
|
public function calculateFolderSize($path, $entry = null) { |
729
|
|
|
$totalSize = 0; |
730
|
|
|
if ($entry === null or !isset($entry['fileid'])) { |
731
|
|
|
$entry = $this->get($path); |
732
|
|
|
} |
733
|
|
|
if (isset($entry['mimetype']) && $entry['mimetype'] === 'httpd/unix-directory') { |
734
|
|
|
$id = $entry['fileid']; |
735
|
|
|
$sql = 'SELECT SUM(`size`) AS f1, MIN(`size`) AS f2 ' . |
736
|
|
|
'FROM `*PREFIX*filecache` ' . |
737
|
|
|
'WHERE `parent` = ? AND `storage` = ?'; |
738
|
|
|
$result = $this->connection->executeQuery($sql, [$id, $this->getNumericStorageId()]); |
739
|
|
|
if ($row = $result->fetch()) { |
740
|
|
|
$result->closeCursor(); |
741
|
|
|
list($sum, $min) = \array_values($row); |
742
|
|
|
$sum = 0 + $sum; |
743
|
|
|
$min = 0 + $min; |
744
|
|
|
if ($min === -1) { |
745
|
|
|
$totalSize = $min; |
746
|
|
|
} else { |
747
|
|
|
$totalSize = $sum; |
748
|
|
|
} |
749
|
|
|
$update = []; |
750
|
|
|
if ($entry['size'] !== $totalSize) { |
751
|
|
|
$update['size'] = $totalSize; |
752
|
|
|
} |
753
|
|
|
if (\count($update) > 0) { |
754
|
|
|
$this->update($id, $update); |
755
|
|
|
} |
756
|
|
|
} else { |
757
|
|
|
$result->closeCursor(); |
758
|
|
|
} |
759
|
|
|
} |
760
|
|
|
return $totalSize; |
761
|
|
|
} |
762
|
|
|
|
763
|
|
|
/** |
764
|
|
|
* find a folder in the cache which has not been fully scanned |
765
|
|
|
* |
766
|
|
|
* If multiple incomplete folders are in the cache, the one with the highest id will be returned, |
767
|
|
|
* use the one with the highest id gives the best result with the background scanner, since that is most |
768
|
|
|
* likely the folder where we stopped scanning previously |
769
|
|
|
* |
770
|
|
|
* @return string|bool the path of the folder or false when no folder matched |
771
|
|
|
*/ |
772
|
|
|
public function getIncomplete() { |
773
|
|
|
$query = $this->connection->prepare('SELECT `path` FROM `*PREFIX*filecache`' |
774
|
|
|
. ' WHERE `storage` = ? AND `size` = -1 ORDER BY `fileid` DESC', 1); |
775
|
|
|
$query->execute([$this->getNumericStorageId()]); |
776
|
|
|
if ($row = $query->fetch()) { |
777
|
|
|
return $row['path']; |
778
|
|
|
} else { |
779
|
|
|
return false; |
780
|
|
|
} |
781
|
|
|
} |
782
|
|
|
|
783
|
|
|
/** |
784
|
|
|
* get the path of a file on this storage by it's file id |
785
|
|
|
* |
786
|
|
|
* @param int $id the file id of the file or folder to search |
787
|
|
|
* @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 |
788
|
|
|
*/ |
789
|
|
|
public function getPathById($id) { |
790
|
|
|
$sql = 'SELECT `path` FROM `*PREFIX*filecache` WHERE `fileid` = ? AND `storage` = ?'; |
791
|
|
|
$result = $this->connection->executeQuery($sql, [$id, $this->getNumericStorageId()]); |
792
|
|
|
if ($row = $result->fetch()) { |
793
|
|
|
// Oracle stores empty strings as null... |
794
|
|
|
if ($row['path'] === null) { |
795
|
|
|
return ''; |
796
|
|
|
} |
797
|
|
|
return $row['path']; |
798
|
|
|
} else { |
799
|
|
|
return null; |
800
|
|
|
} |
801
|
|
|
} |
802
|
|
|
|
803
|
|
|
/** |
804
|
|
|
* normalize the given path |
805
|
|
|
* |
806
|
|
|
* @param string $path |
807
|
|
|
* @return string |
808
|
|
|
*/ |
809
|
|
|
public function normalize($path) { |
810
|
|
|
return \trim(\OC_Util::normalizeUnicode($path), '/'); |
811
|
|
|
} |
812
|
|
|
} |
813
|
|
|
|
It seems like the type of the argument is not accepted by the function/method which you are calling.
In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.
We suggest to add an explicit type cast like in the following example: