Completed
Push — master ( cc3fdf...f4cb78 )
by
unknown
29:45 queued 33s
created
lib/private/Files/ObjectStore/ObjectStoreStorage.php 2 patches
Indentation   +792 added lines, -792 removed lines patch added patch discarded remove patch
@@ -29,799 +29,799 @@
 block discarded – undo
29 29
 use Psr\Log\LoggerInterface;
30 30
 
31 31
 class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
32
-	use CopyDirectory;
33
-
34
-	protected IObjectStore $objectStore;
35
-	protected string $id;
36
-	private string $objectPrefix = 'urn:oid:';
37
-
38
-	private LoggerInterface $logger;
39
-
40
-	private bool $handleCopiesAsOwned;
41
-	protected bool $validateWrites = true;
42
-	private bool $preserveCacheItemsOnDelete = false;
43
-
44
-	/**
45
-	 * @param array $parameters
46
-	 * @throws \Exception
47
-	 */
48
-	public function __construct(array $parameters) {
49
-		if (isset($parameters['objectstore']) && $parameters['objectstore'] instanceof IObjectStore) {
50
-			$this->objectStore = $parameters['objectstore'];
51
-		} else {
52
-			throw new \Exception('missing IObjectStore instance');
53
-		}
54
-		if (isset($parameters['storageid'])) {
55
-			$this->id = 'object::store:' . $parameters['storageid'];
56
-		} else {
57
-			$this->id = 'object::store:' . $this->objectStore->getStorageId();
58
-		}
59
-		if (isset($parameters['objectPrefix'])) {
60
-			$this->objectPrefix = $parameters['objectPrefix'];
61
-		}
62
-		if (isset($parameters['validateWrites'])) {
63
-			$this->validateWrites = (bool)$parameters['validateWrites'];
64
-		}
65
-		$this->handleCopiesAsOwned = (bool)($parameters['handleCopiesAsOwned'] ?? false);
66
-
67
-		$this->logger = \OCP\Server::get(LoggerInterface::class);
68
-	}
69
-
70
-	public function mkdir(string $path, bool $force = false, array $metadata = []): bool {
71
-		$path = $this->normalizePath($path);
72
-		if (!$force && $this->file_exists($path)) {
73
-			$this->logger->warning("Tried to create an object store folder that already exists: $path");
74
-			return false;
75
-		}
76
-
77
-		$mTime = time();
78
-		$data = [
79
-			'mimetype' => 'httpd/unix-directory',
80
-			'size' => $metadata['size'] ?? 0,
81
-			'mtime' => $mTime,
82
-			'storage_mtime' => $mTime,
83
-			'permissions' => \OCP\Constants::PERMISSION_ALL,
84
-		];
85
-		if ($path === '') {
86
-			//create root on the fly
87
-			$data['etag'] = $this->getETag('');
88
-			$this->getCache()->put('', $data);
89
-			return true;
90
-		} else {
91
-			// if parent does not exist, create it
92
-			$parent = $this->normalizePath(dirname($path));
93
-			$parentType = $this->filetype($parent);
94
-			if ($parentType === false) {
95
-				if (!$this->mkdir($parent)) {
96
-					// something went wrong
97
-					$this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created");
98
-					return false;
99
-				}
100
-			} elseif ($parentType === 'file') {
101
-				// parent is a file
102
-				$this->logger->warning("Parent ($parent) is a file");
103
-				return false;
104
-			}
105
-			// finally create the new dir
106
-			$mTime = time(); // update mtime
107
-			$data['mtime'] = $mTime;
108
-			$data['storage_mtime'] = $mTime;
109
-			$data['etag'] = $this->getETag($path);
110
-			$this->getCache()->put($path, $data);
111
-			return true;
112
-		}
113
-	}
114
-
115
-	private function normalizePath(string $path): string {
116
-		$path = trim($path, '/');
117
-		//FIXME why do we sometimes get a path like 'files//username'?
118
-		$path = str_replace('//', '/', $path);
119
-
120
-		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
121
-		if (!$path || $path === '.') {
122
-			$path = '';
123
-		}
124
-
125
-		return $path;
126
-	}
127
-
128
-	/**
129
-	 * Object Stores use a NoopScanner because metadata is directly stored in
130
-	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
131
-	 */
132
-	public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
133
-		if (!$storage) {
134
-			$storage = $this;
135
-		}
136
-		if (!isset($this->scanner)) {
137
-			$this->scanner = new ObjectStoreScanner($storage);
138
-		}
139
-		/** @var \OC\Files\ObjectStore\ObjectStoreScanner */
140
-		return $this->scanner;
141
-	}
142
-
143
-	public function getId(): string {
144
-		return $this->id;
145
-	}
146
-
147
-	public function rmdir(string $path): bool {
148
-		$path = $this->normalizePath($path);
149
-		$entry = $this->getCache()->get($path);
150
-
151
-		if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
152
-			return false;
153
-		}
154
-
155
-		return $this->rmObjects($entry);
156
-	}
157
-
158
-	private function rmObjects(ICacheEntry $entry): bool {
159
-		$children = $this->getCache()->getFolderContentsById($entry->getId());
160
-		foreach ($children as $child) {
161
-			if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
162
-				if (!$this->rmObjects($child)) {
163
-					return false;
164
-				}
165
-			} else {
166
-				if (!$this->rmObject($child)) {
167
-					return false;
168
-				}
169
-			}
170
-		}
171
-
172
-		if (!$this->preserveCacheItemsOnDelete) {
173
-			$this->getCache()->remove($entry->getPath());
174
-		}
175
-
176
-		return true;
177
-	}
178
-
179
-	public function unlink(string $path): bool {
180
-		$path = $this->normalizePath($path);
181
-		$entry = $this->getCache()->get($path);
182
-
183
-		if ($entry instanceof ICacheEntry) {
184
-			if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
185
-				return $this->rmObjects($entry);
186
-			} else {
187
-				return $this->rmObject($entry);
188
-			}
189
-		}
190
-		return false;
191
-	}
192
-
193
-	public function rmObject(ICacheEntry $entry): bool {
194
-		try {
195
-			$this->objectStore->deleteObject($this->getURN($entry->getId()));
196
-		} catch (\Exception $ex) {
197
-			if ($ex->getCode() !== 404) {
198
-				$this->logger->error(
199
-					'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
200
-					[
201
-						'app' => 'objectstore',
202
-						'exception' => $ex,
203
-					]
204
-				);
205
-				return false;
206
-			}
207
-			//removing from cache is ok as it does not exist in the objectstore anyway
208
-		}
209
-		if (!$this->preserveCacheItemsOnDelete) {
210
-			$this->getCache()->remove($entry->getPath());
211
-		}
212
-		return true;
213
-	}
214
-
215
-	public function stat(string $path): array|false {
216
-		$path = $this->normalizePath($path);
217
-		$cacheEntry = $this->getCache()->get($path);
218
-		if ($cacheEntry instanceof CacheEntry) {
219
-			return $cacheEntry->getData();
220
-		} else {
221
-			if ($path === '') {
222
-				$this->mkdir('', true);
223
-				$cacheEntry = $this->getCache()->get($path);
224
-				if ($cacheEntry instanceof CacheEntry) {
225
-					return $cacheEntry->getData();
226
-				}
227
-			}
228
-			return false;
229
-		}
230
-	}
231
-
232
-	public function getPermissions(string $path): int {
233
-		$stat = $this->stat($path);
234
-
235
-		if (is_array($stat) && isset($stat['permissions'])) {
236
-			return $stat['permissions'];
237
-		}
238
-
239
-		return parent::getPermissions($path);
240
-	}
241
-
242
-	/**
243
-	 * Override this method if you need a different unique resource identifier for your object storage implementation.
244
-	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
245
-	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
246
-	 *
247
-	 * @return string the unified resource name used to identify the object
248
-	 */
249
-	public function getURN(int $fileId): string {
250
-		return $this->objectPrefix . $fileId;
251
-	}
252
-
253
-	public function opendir(string $path) {
254
-		$path = $this->normalizePath($path);
255
-
256
-		try {
257
-			$files = [];
258
-			$folderContents = $this->getCache()->getFolderContents($path);
259
-			foreach ($folderContents as $file) {
260
-				$files[] = $file['name'];
261
-			}
262
-
263
-			return IteratorDirectory::wrap($files);
264
-		} catch (\Exception $e) {
265
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
266
-			return false;
267
-		}
268
-	}
269
-
270
-	public function filetype(string $path): string|false {
271
-		$path = $this->normalizePath($path);
272
-		$stat = $this->stat($path);
273
-		if ($stat) {
274
-			if ($stat['mimetype'] === 'httpd/unix-directory') {
275
-				return 'dir';
276
-			}
277
-			return 'file';
278
-		} else {
279
-			return false;
280
-		}
281
-	}
282
-
283
-	public function fopen(string $path, string $mode) {
284
-		$path = $this->normalizePath($path);
285
-
286
-		if (strrpos($path, '.') !== false) {
287
-			$ext = substr($path, strrpos($path, '.'));
288
-		} else {
289
-			$ext = '';
290
-		}
291
-
292
-		switch ($mode) {
293
-			case 'r':
294
-			case 'rb':
295
-				$stat = $this->stat($path);
296
-				if (is_array($stat)) {
297
-					$filesize = $stat['size'] ?? 0;
298
-					// Reading 0 sized files is a waste of time
299
-					if ($filesize === 0) {
300
-						return fopen('php://memory', $mode);
301
-					}
302
-
303
-					try {
304
-						$handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
305
-						if ($handle === false) {
306
-							return false; // keep backward compatibility
307
-						}
308
-						$streamStat = fstat($handle);
309
-						$actualSize = $streamStat['size'] ?? -1;
310
-						if ($actualSize > -1 && $actualSize !== $filesize) {
311
-							$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
312
-						}
313
-						return $handle;
314
-					} catch (NotFoundException $e) {
315
-						$this->logger->error(
316
-							'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
317
-							[
318
-								'app' => 'objectstore',
319
-								'exception' => $e,
320
-							]
321
-						);
322
-						throw $e;
323
-					} catch (\Exception $e) {
324
-						$this->logger->error(
325
-							'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
326
-							[
327
-								'app' => 'objectstore',
328
-								'exception' => $e,
329
-							]
330
-						);
331
-						return false;
332
-					}
333
-				} else {
334
-					return false;
335
-				}
336
-				// no break
337
-			case 'w':
338
-			case 'wb':
339
-			case 'w+':
340
-			case 'wb+':
341
-				$dirName = dirname($path);
342
-				$parentExists = $this->is_dir($dirName);
343
-				if (!$parentExists) {
344
-					return false;
345
-				}
346
-
347
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
348
-				$handle = fopen($tmpFile, $mode);
349
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
350
-					$this->writeBack($tmpFile, $path);
351
-					unlink($tmpFile);
352
-				});
353
-			case 'a':
354
-			case 'ab':
355
-			case 'r+':
356
-			case 'a+':
357
-			case 'x':
358
-			case 'x+':
359
-			case 'c':
360
-			case 'c+':
361
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
362
-				if ($this->file_exists($path)) {
363
-					$source = $this->fopen($path, 'r');
364
-					file_put_contents($tmpFile, $source);
365
-				}
366
-				$handle = fopen($tmpFile, $mode);
367
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
368
-					$this->writeBack($tmpFile, $path);
369
-					unlink($tmpFile);
370
-				});
371
-		}
372
-		return false;
373
-	}
374
-
375
-	public function file_exists(string $path): bool {
376
-		$path = $this->normalizePath($path);
377
-		return (bool)$this->stat($path);
378
-	}
379
-
380
-	public function rename(string $source, string $target): bool {
381
-		$source = $this->normalizePath($source);
382
-		$target = $this->normalizePath($target);
383
-		$this->remove($target);
384
-		$this->getCache()->move($source, $target);
385
-		$this->touch(dirname($target));
386
-		return true;
387
-	}
388
-
389
-	public function getMimeType(string $path): string|false {
390
-		$path = $this->normalizePath($path);
391
-		return parent::getMimeType($path);
392
-	}
393
-
394
-	public function touch(string $path, ?int $mtime = null): bool {
395
-		if (is_null($mtime)) {
396
-			$mtime = time();
397
-		}
398
-
399
-		$path = $this->normalizePath($path);
400
-		$dirName = dirname($path);
401
-		$parentExists = $this->is_dir($dirName);
402
-		if (!$parentExists) {
403
-			return false;
404
-		}
405
-
406
-		$stat = $this->stat($path);
407
-		if (is_array($stat)) {
408
-			// update existing mtime in db
409
-			$stat['mtime'] = $mtime;
410
-			$this->getCache()->update($stat['fileid'], $stat);
411
-		} else {
412
-			try {
413
-				//create a empty file, need to have at least on char to make it
414
-				// work with all object storage implementations
415
-				$this->file_put_contents($path, ' ');
416
-				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
417
-				$stat = [
418
-					'etag' => $this->getETag($path),
419
-					'mimetype' => $mimeType,
420
-					'size' => 0,
421
-					'mtime' => $mtime,
422
-					'storage_mtime' => $mtime,
423
-					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
424
-				];
425
-				$this->getCache()->put($path, $stat);
426
-			} catch (\Exception $ex) {
427
-				$this->logger->error(
428
-					'Could not create object for ' . $path,
429
-					[
430
-						'app' => 'objectstore',
431
-						'exception' => $ex,
432
-					]
433
-				);
434
-				throw $ex;
435
-			}
436
-		}
437
-		return true;
438
-	}
439
-
440
-	public function writeBack(string $tmpFile, string $path) {
441
-		$size = filesize($tmpFile);
442
-		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
443
-	}
444
-
445
-	public function hasUpdated(string $path, int $time): bool {
446
-		return false;
447
-	}
448
-
449
-	public function needsPartFile(): bool {
450
-		return false;
451
-	}
452
-
453
-	public function file_put_contents(string $path, mixed $data): int {
454
-		$fh = fopen('php://temp', 'w+');
455
-		fwrite($fh, $data);
456
-		rewind($fh);
457
-		return $this->writeStream($path, $fh, strlen($data));
458
-	}
459
-
460
-	public function writeStream(string $path, $stream, ?int $size = null): int {
461
-		if ($size === null) {
462
-			$stats = fstat($stream);
463
-			if (is_array($stats) && isset($stats['size'])) {
464
-				$size = $stats['size'];
465
-			}
466
-		}
467
-
468
-		$stat = $this->stat($path);
469
-		if (empty($stat)) {
470
-			// create new file
471
-			$stat = [
472
-				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
473
-			];
474
-		}
475
-		// update stat with new data
476
-		$mTime = time();
477
-		$stat['size'] = (int)$size;
478
-		$stat['mtime'] = $mTime;
479
-		$stat['storage_mtime'] = $mTime;
480
-
481
-		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
482
-		$mimetype = $mimetypeDetector->detectPath($path);
483
-		$metadata = [
484
-			'mimetype' => $mimetype,
485
-			'original-storage' => $this->getId(),
486
-			'original-path' => $path,
487
-		];
488
-
489
-		$stat['mimetype'] = $mimetype;
490
-		$stat['etag'] = $this->getETag($path);
491
-		$stat['checksum'] = '';
492
-
493
-		$exists = $this->getCache()->inCache($path);
494
-		$uploadPath = $exists ? $path : $path . '.part';
495
-
496
-		if ($exists) {
497
-			$fileId = $stat['fileid'];
498
-		} else {
499
-			$parent = $this->normalizePath(dirname($path));
500
-			if (!$this->is_dir($parent)) {
501
-				throw new \InvalidArgumentException("trying to upload a file ($path) inside a non-directory ($parent)");
502
-			}
503
-			$fileId = $this->getCache()->put($uploadPath, $stat);
504
-		}
505
-
506
-		$urn = $this->getURN($fileId);
507
-		try {
508
-			//upload to object storage
509
-			if ($size === null) {
510
-				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
511
-					$this->getCache()->update($fileId, [
512
-						'size' => $writtenSize,
513
-					]);
514
-					$size = $writtenSize;
515
-				});
516
-				if ($this->objectStore instanceof IObjectStoreMetaData) {
517
-					$this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata);
518
-				} else {
519
-					$this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']);
520
-				}
521
-				if (is_resource($countStream)) {
522
-					fclose($countStream);
523
-				}
524
-				$stat['size'] = $size;
525
-			} else {
526
-				if ($this->objectStore instanceof IObjectStoreMetaData) {
527
-					$this->objectStore->writeObjectWithMetaData($urn, $stream, $metadata);
528
-				} else {
529
-					$this->objectStore->writeObject($urn, $stream, $metadata['mimetype']);
530
-				}
531
-				if (is_resource($stream)) {
532
-					fclose($stream);
533
-				}
534
-			}
535
-		} catch (\Exception $ex) {
536
-			if (!$exists) {
537
-				/*
32
+    use CopyDirectory;
33
+
34
+    protected IObjectStore $objectStore;
35
+    protected string $id;
36
+    private string $objectPrefix = 'urn:oid:';
37
+
38
+    private LoggerInterface $logger;
39
+
40
+    private bool $handleCopiesAsOwned;
41
+    protected bool $validateWrites = true;
42
+    private bool $preserveCacheItemsOnDelete = false;
43
+
44
+    /**
45
+     * @param array $parameters
46
+     * @throws \Exception
47
+     */
48
+    public function __construct(array $parameters) {
49
+        if (isset($parameters['objectstore']) && $parameters['objectstore'] instanceof IObjectStore) {
50
+            $this->objectStore = $parameters['objectstore'];
51
+        } else {
52
+            throw new \Exception('missing IObjectStore instance');
53
+        }
54
+        if (isset($parameters['storageid'])) {
55
+            $this->id = 'object::store:' . $parameters['storageid'];
56
+        } else {
57
+            $this->id = 'object::store:' . $this->objectStore->getStorageId();
58
+        }
59
+        if (isset($parameters['objectPrefix'])) {
60
+            $this->objectPrefix = $parameters['objectPrefix'];
61
+        }
62
+        if (isset($parameters['validateWrites'])) {
63
+            $this->validateWrites = (bool)$parameters['validateWrites'];
64
+        }
65
+        $this->handleCopiesAsOwned = (bool)($parameters['handleCopiesAsOwned'] ?? false);
66
+
67
+        $this->logger = \OCP\Server::get(LoggerInterface::class);
68
+    }
69
+
70
+    public function mkdir(string $path, bool $force = false, array $metadata = []): bool {
71
+        $path = $this->normalizePath($path);
72
+        if (!$force && $this->file_exists($path)) {
73
+            $this->logger->warning("Tried to create an object store folder that already exists: $path");
74
+            return false;
75
+        }
76
+
77
+        $mTime = time();
78
+        $data = [
79
+            'mimetype' => 'httpd/unix-directory',
80
+            'size' => $metadata['size'] ?? 0,
81
+            'mtime' => $mTime,
82
+            'storage_mtime' => $mTime,
83
+            'permissions' => \OCP\Constants::PERMISSION_ALL,
84
+        ];
85
+        if ($path === '') {
86
+            //create root on the fly
87
+            $data['etag'] = $this->getETag('');
88
+            $this->getCache()->put('', $data);
89
+            return true;
90
+        } else {
91
+            // if parent does not exist, create it
92
+            $parent = $this->normalizePath(dirname($path));
93
+            $parentType = $this->filetype($parent);
94
+            if ($parentType === false) {
95
+                if (!$this->mkdir($parent)) {
96
+                    // something went wrong
97
+                    $this->logger->warning("Parent folder ($parent) doesn't exist and couldn't be created");
98
+                    return false;
99
+                }
100
+            } elseif ($parentType === 'file') {
101
+                // parent is a file
102
+                $this->logger->warning("Parent ($parent) is a file");
103
+                return false;
104
+            }
105
+            // finally create the new dir
106
+            $mTime = time(); // update mtime
107
+            $data['mtime'] = $mTime;
108
+            $data['storage_mtime'] = $mTime;
109
+            $data['etag'] = $this->getETag($path);
110
+            $this->getCache()->put($path, $data);
111
+            return true;
112
+        }
113
+    }
114
+
115
+    private function normalizePath(string $path): string {
116
+        $path = trim($path, '/');
117
+        //FIXME why do we sometimes get a path like 'files//username'?
118
+        $path = str_replace('//', '/', $path);
119
+
120
+        // dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
121
+        if (!$path || $path === '.') {
122
+            $path = '';
123
+        }
124
+
125
+        return $path;
126
+    }
127
+
128
+    /**
129
+     * Object Stores use a NoopScanner because metadata is directly stored in
130
+     * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
131
+     */
132
+    public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
133
+        if (!$storage) {
134
+            $storage = $this;
135
+        }
136
+        if (!isset($this->scanner)) {
137
+            $this->scanner = new ObjectStoreScanner($storage);
138
+        }
139
+        /** @var \OC\Files\ObjectStore\ObjectStoreScanner */
140
+        return $this->scanner;
141
+    }
142
+
143
+    public function getId(): string {
144
+        return $this->id;
145
+    }
146
+
147
+    public function rmdir(string $path): bool {
148
+        $path = $this->normalizePath($path);
149
+        $entry = $this->getCache()->get($path);
150
+
151
+        if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
152
+            return false;
153
+        }
154
+
155
+        return $this->rmObjects($entry);
156
+    }
157
+
158
+    private function rmObjects(ICacheEntry $entry): bool {
159
+        $children = $this->getCache()->getFolderContentsById($entry->getId());
160
+        foreach ($children as $child) {
161
+            if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
162
+                if (!$this->rmObjects($child)) {
163
+                    return false;
164
+                }
165
+            } else {
166
+                if (!$this->rmObject($child)) {
167
+                    return false;
168
+                }
169
+            }
170
+        }
171
+
172
+        if (!$this->preserveCacheItemsOnDelete) {
173
+            $this->getCache()->remove($entry->getPath());
174
+        }
175
+
176
+        return true;
177
+    }
178
+
179
+    public function unlink(string $path): bool {
180
+        $path = $this->normalizePath($path);
181
+        $entry = $this->getCache()->get($path);
182
+
183
+        if ($entry instanceof ICacheEntry) {
184
+            if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
185
+                return $this->rmObjects($entry);
186
+            } else {
187
+                return $this->rmObject($entry);
188
+            }
189
+        }
190
+        return false;
191
+    }
192
+
193
+    public function rmObject(ICacheEntry $entry): bool {
194
+        try {
195
+            $this->objectStore->deleteObject($this->getURN($entry->getId()));
196
+        } catch (\Exception $ex) {
197
+            if ($ex->getCode() !== 404) {
198
+                $this->logger->error(
199
+                    'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
200
+                    [
201
+                        'app' => 'objectstore',
202
+                        'exception' => $ex,
203
+                    ]
204
+                );
205
+                return false;
206
+            }
207
+            //removing from cache is ok as it does not exist in the objectstore anyway
208
+        }
209
+        if (!$this->preserveCacheItemsOnDelete) {
210
+            $this->getCache()->remove($entry->getPath());
211
+        }
212
+        return true;
213
+    }
214
+
215
+    public function stat(string $path): array|false {
216
+        $path = $this->normalizePath($path);
217
+        $cacheEntry = $this->getCache()->get($path);
218
+        if ($cacheEntry instanceof CacheEntry) {
219
+            return $cacheEntry->getData();
220
+        } else {
221
+            if ($path === '') {
222
+                $this->mkdir('', true);
223
+                $cacheEntry = $this->getCache()->get($path);
224
+                if ($cacheEntry instanceof CacheEntry) {
225
+                    return $cacheEntry->getData();
226
+                }
227
+            }
228
+            return false;
229
+        }
230
+    }
231
+
232
+    public function getPermissions(string $path): int {
233
+        $stat = $this->stat($path);
234
+
235
+        if (is_array($stat) && isset($stat['permissions'])) {
236
+            return $stat['permissions'];
237
+        }
238
+
239
+        return parent::getPermissions($path);
240
+    }
241
+
242
+    /**
243
+     * Override this method if you need a different unique resource identifier for your object storage implementation.
244
+     * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
245
+     * You may need a mapping table to store your URN if it cannot be generated from the fileid.
246
+     *
247
+     * @return string the unified resource name used to identify the object
248
+     */
249
+    public function getURN(int $fileId): string {
250
+        return $this->objectPrefix . $fileId;
251
+    }
252
+
253
+    public function opendir(string $path) {
254
+        $path = $this->normalizePath($path);
255
+
256
+        try {
257
+            $files = [];
258
+            $folderContents = $this->getCache()->getFolderContents($path);
259
+            foreach ($folderContents as $file) {
260
+                $files[] = $file['name'];
261
+            }
262
+
263
+            return IteratorDirectory::wrap($files);
264
+        } catch (\Exception $e) {
265
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
266
+            return false;
267
+        }
268
+    }
269
+
270
+    public function filetype(string $path): string|false {
271
+        $path = $this->normalizePath($path);
272
+        $stat = $this->stat($path);
273
+        if ($stat) {
274
+            if ($stat['mimetype'] === 'httpd/unix-directory') {
275
+                return 'dir';
276
+            }
277
+            return 'file';
278
+        } else {
279
+            return false;
280
+        }
281
+    }
282
+
283
+    public function fopen(string $path, string $mode) {
284
+        $path = $this->normalizePath($path);
285
+
286
+        if (strrpos($path, '.') !== false) {
287
+            $ext = substr($path, strrpos($path, '.'));
288
+        } else {
289
+            $ext = '';
290
+        }
291
+
292
+        switch ($mode) {
293
+            case 'r':
294
+            case 'rb':
295
+                $stat = $this->stat($path);
296
+                if (is_array($stat)) {
297
+                    $filesize = $stat['size'] ?? 0;
298
+                    // Reading 0 sized files is a waste of time
299
+                    if ($filesize === 0) {
300
+                        return fopen('php://memory', $mode);
301
+                    }
302
+
303
+                    try {
304
+                        $handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
305
+                        if ($handle === false) {
306
+                            return false; // keep backward compatibility
307
+                        }
308
+                        $streamStat = fstat($handle);
309
+                        $actualSize = $streamStat['size'] ?? -1;
310
+                        if ($actualSize > -1 && $actualSize !== $filesize) {
311
+                            $this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
312
+                        }
313
+                        return $handle;
314
+                    } catch (NotFoundException $e) {
315
+                        $this->logger->error(
316
+                            'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
317
+                            [
318
+                                'app' => 'objectstore',
319
+                                'exception' => $e,
320
+                            ]
321
+                        );
322
+                        throw $e;
323
+                    } catch (\Exception $e) {
324
+                        $this->logger->error(
325
+                            'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
326
+                            [
327
+                                'app' => 'objectstore',
328
+                                'exception' => $e,
329
+                            ]
330
+                        );
331
+                        return false;
332
+                    }
333
+                } else {
334
+                    return false;
335
+                }
336
+                // no break
337
+            case 'w':
338
+            case 'wb':
339
+            case 'w+':
340
+            case 'wb+':
341
+                $dirName = dirname($path);
342
+                $parentExists = $this->is_dir($dirName);
343
+                if (!$parentExists) {
344
+                    return false;
345
+                }
346
+
347
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
348
+                $handle = fopen($tmpFile, $mode);
349
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
350
+                    $this->writeBack($tmpFile, $path);
351
+                    unlink($tmpFile);
352
+                });
353
+            case 'a':
354
+            case 'ab':
355
+            case 'r+':
356
+            case 'a+':
357
+            case 'x':
358
+            case 'x+':
359
+            case 'c':
360
+            case 'c+':
361
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
362
+                if ($this->file_exists($path)) {
363
+                    $source = $this->fopen($path, 'r');
364
+                    file_put_contents($tmpFile, $source);
365
+                }
366
+                $handle = fopen($tmpFile, $mode);
367
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
368
+                    $this->writeBack($tmpFile, $path);
369
+                    unlink($tmpFile);
370
+                });
371
+        }
372
+        return false;
373
+    }
374
+
375
+    public function file_exists(string $path): bool {
376
+        $path = $this->normalizePath($path);
377
+        return (bool)$this->stat($path);
378
+    }
379
+
380
+    public function rename(string $source, string $target): bool {
381
+        $source = $this->normalizePath($source);
382
+        $target = $this->normalizePath($target);
383
+        $this->remove($target);
384
+        $this->getCache()->move($source, $target);
385
+        $this->touch(dirname($target));
386
+        return true;
387
+    }
388
+
389
+    public function getMimeType(string $path): string|false {
390
+        $path = $this->normalizePath($path);
391
+        return parent::getMimeType($path);
392
+    }
393
+
394
+    public function touch(string $path, ?int $mtime = null): bool {
395
+        if (is_null($mtime)) {
396
+            $mtime = time();
397
+        }
398
+
399
+        $path = $this->normalizePath($path);
400
+        $dirName = dirname($path);
401
+        $parentExists = $this->is_dir($dirName);
402
+        if (!$parentExists) {
403
+            return false;
404
+        }
405
+
406
+        $stat = $this->stat($path);
407
+        if (is_array($stat)) {
408
+            // update existing mtime in db
409
+            $stat['mtime'] = $mtime;
410
+            $this->getCache()->update($stat['fileid'], $stat);
411
+        } else {
412
+            try {
413
+                //create a empty file, need to have at least on char to make it
414
+                // work with all object storage implementations
415
+                $this->file_put_contents($path, ' ');
416
+                $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
417
+                $stat = [
418
+                    'etag' => $this->getETag($path),
419
+                    'mimetype' => $mimeType,
420
+                    'size' => 0,
421
+                    'mtime' => $mtime,
422
+                    'storage_mtime' => $mtime,
423
+                    'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
424
+                ];
425
+                $this->getCache()->put($path, $stat);
426
+            } catch (\Exception $ex) {
427
+                $this->logger->error(
428
+                    'Could not create object for ' . $path,
429
+                    [
430
+                        'app' => 'objectstore',
431
+                        'exception' => $ex,
432
+                    ]
433
+                );
434
+                throw $ex;
435
+            }
436
+        }
437
+        return true;
438
+    }
439
+
440
+    public function writeBack(string $tmpFile, string $path) {
441
+        $size = filesize($tmpFile);
442
+        $this->writeStream($path, fopen($tmpFile, 'r'), $size);
443
+    }
444
+
445
+    public function hasUpdated(string $path, int $time): bool {
446
+        return false;
447
+    }
448
+
449
+    public function needsPartFile(): bool {
450
+        return false;
451
+    }
452
+
453
+    public function file_put_contents(string $path, mixed $data): int {
454
+        $fh = fopen('php://temp', 'w+');
455
+        fwrite($fh, $data);
456
+        rewind($fh);
457
+        return $this->writeStream($path, $fh, strlen($data));
458
+    }
459
+
460
+    public function writeStream(string $path, $stream, ?int $size = null): int {
461
+        if ($size === null) {
462
+            $stats = fstat($stream);
463
+            if (is_array($stats) && isset($stats['size'])) {
464
+                $size = $stats['size'];
465
+            }
466
+        }
467
+
468
+        $stat = $this->stat($path);
469
+        if (empty($stat)) {
470
+            // create new file
471
+            $stat = [
472
+                'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
473
+            ];
474
+        }
475
+        // update stat with new data
476
+        $mTime = time();
477
+        $stat['size'] = (int)$size;
478
+        $stat['mtime'] = $mTime;
479
+        $stat['storage_mtime'] = $mTime;
480
+
481
+        $mimetypeDetector = \OC::$server->getMimeTypeDetector();
482
+        $mimetype = $mimetypeDetector->detectPath($path);
483
+        $metadata = [
484
+            'mimetype' => $mimetype,
485
+            'original-storage' => $this->getId(),
486
+            'original-path' => $path,
487
+        ];
488
+
489
+        $stat['mimetype'] = $mimetype;
490
+        $stat['etag'] = $this->getETag($path);
491
+        $stat['checksum'] = '';
492
+
493
+        $exists = $this->getCache()->inCache($path);
494
+        $uploadPath = $exists ? $path : $path . '.part';
495
+
496
+        if ($exists) {
497
+            $fileId = $stat['fileid'];
498
+        } else {
499
+            $parent = $this->normalizePath(dirname($path));
500
+            if (!$this->is_dir($parent)) {
501
+                throw new \InvalidArgumentException("trying to upload a file ($path) inside a non-directory ($parent)");
502
+            }
503
+            $fileId = $this->getCache()->put($uploadPath, $stat);
504
+        }
505
+
506
+        $urn = $this->getURN($fileId);
507
+        try {
508
+            //upload to object storage
509
+            if ($size === null) {
510
+                $countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
511
+                    $this->getCache()->update($fileId, [
512
+                        'size' => $writtenSize,
513
+                    ]);
514
+                    $size = $writtenSize;
515
+                });
516
+                if ($this->objectStore instanceof IObjectStoreMetaData) {
517
+                    $this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata);
518
+                } else {
519
+                    $this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']);
520
+                }
521
+                if (is_resource($countStream)) {
522
+                    fclose($countStream);
523
+                }
524
+                $stat['size'] = $size;
525
+            } else {
526
+                if ($this->objectStore instanceof IObjectStoreMetaData) {
527
+                    $this->objectStore->writeObjectWithMetaData($urn, $stream, $metadata);
528
+                } else {
529
+                    $this->objectStore->writeObject($urn, $stream, $metadata['mimetype']);
530
+                }
531
+                if (is_resource($stream)) {
532
+                    fclose($stream);
533
+                }
534
+            }
535
+        } catch (\Exception $ex) {
536
+            if (!$exists) {
537
+                /*
538 538
 				 * Only remove the entry if we are dealing with a new file.
539 539
 				 * Else people lose access to existing files
540 540
 				 */
541
-				$this->getCache()->remove($uploadPath);
542
-				$this->logger->error(
543
-					'Could not create object ' . $urn . ' for ' . $path,
544
-					[
545
-						'app' => 'objectstore',
546
-						'exception' => $ex,
547
-					]
548
-				);
549
-			} else {
550
-				$this->logger->error(
551
-					'Could not update object ' . $urn . ' for ' . $path,
552
-					[
553
-						'app' => 'objectstore',
554
-						'exception' => $ex,
555
-					]
556
-				);
557
-			}
558
-			throw $ex; // make this bubble up
559
-		}
560
-
561
-		if ($exists) {
562
-			// Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
563
-			$stat['unencrypted_size'] = $stat['size'];
564
-			$this->getCache()->update($fileId, $stat);
565
-		} else {
566
-			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
567
-				$this->getCache()->move($uploadPath, $path);
568
-			} else {
569
-				$this->getCache()->remove($uploadPath);
570
-				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
571
-			}
572
-		}
573
-
574
-		return $size;
575
-	}
576
-
577
-	public function getObjectStore(): IObjectStore {
578
-		return $this->objectStore;
579
-	}
580
-
581
-	public function copyFromStorage(
582
-		IStorage $sourceStorage,
583
-		string $sourceInternalPath,
584
-		string $targetInternalPath,
585
-		bool $preserveMtime = false,
586
-	): bool {
587
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
588
-			/** @var ObjectStoreStorage $sourceStorage */
589
-			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
590
-				/** @var CacheEntry $sourceEntry */
591
-				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
592
-				$sourceEntryData = $sourceEntry->getData();
593
-				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
594
-				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
595
-				// unjailed storage.
596
-				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
597
-					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
598
-				}
599
-				$this->copyInner($sourceStorage->getCache(), $sourceEntry, $targetInternalPath);
600
-				return true;
601
-			}
602
-		}
603
-
604
-		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
605
-	}
606
-
607
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool {
608
-		$sourceCache = $sourceStorage->getCache();
609
-		if (
610
-			$sourceStorage->instanceOfStorage(ObjectStoreStorage::class) &&
611
-			$sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()
612
-		) {
613
-			if ($this->getCache()->get($targetInternalPath)) {
614
-				$this->unlink($targetInternalPath);
615
-				$this->getCache()->remove($targetInternalPath);
616
-			}
617
-			$this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
618
-			// Do not import any data when source and target bucket are identical.
619
-			return true;
620
-		}
621
-		if (!$sourceCacheEntry) {
622
-			$sourceCacheEntry = $sourceCache->get($sourceInternalPath);
623
-		}
624
-
625
-		$this->copyObjects($sourceStorage, $sourceCache, $sourceCacheEntry);
626
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
627
-			/** @var ObjectStoreStorage $sourceStorage */
628
-			$sourceStorage->setPreserveCacheOnDelete(true);
629
-		}
630
-		if ($sourceCacheEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
631
-			$sourceStorage->rmdir($sourceInternalPath);
632
-		} else {
633
-			$sourceStorage->unlink($sourceInternalPath);
634
-		}
635
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
636
-			/** @var ObjectStoreStorage $sourceStorage */
637
-			$sourceStorage->setPreserveCacheOnDelete(false);
638
-		}
639
-		if ($this->getCache()->get($targetInternalPath)) {
640
-			$this->unlink($targetInternalPath);
641
-			$this->getCache()->remove($targetInternalPath);
642
-		}
643
-		$this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
644
-
645
-		return true;
646
-	}
647
-
648
-	/**
649
-	 * Copy the object(s) of a file or folder into this storage, without touching the cache
650
-	 */
651
-	private function copyObjects(IStorage $sourceStorage, ICache $sourceCache, ICacheEntry $sourceCacheEntry) {
652
-		$copiedFiles = [];
653
-		try {
654
-			foreach ($this->getAllChildObjects($sourceCache, $sourceCacheEntry) as $file) {
655
-				$sourceStream = $sourceStorage->fopen($file->getPath(), 'r');
656
-				if (!$sourceStream) {
657
-					throw new \Exception("Failed to open source file {$file->getPath()} ({$file->getId()})");
658
-				}
659
-				$this->objectStore->writeObject($this->getURN($file->getId()), $sourceStream, $file->getMimeType());
660
-				if (is_resource($sourceStream)) {
661
-					fclose($sourceStream);
662
-				}
663
-				$copiedFiles[] = $file->getId();
664
-			}
665
-		} catch (\Exception $e) {
666
-			foreach ($copiedFiles as $fileId) {
667
-				try {
668
-					$this->objectStore->deleteObject($this->getURN($fileId));
669
-				} catch (\Exception $e) {
670
-					// ignore
671
-				}
672
-			}
673
-			throw $e;
674
-		}
675
-	}
676
-
677
-	/**
678
-	 * @return \Iterator<ICacheEntry>
679
-	 */
680
-	private function getAllChildObjects(ICache $cache, ICacheEntry $entry): \Iterator {
681
-		if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
682
-			foreach ($cache->getFolderContentsById($entry->getId()) as $child) {
683
-				yield from $this->getAllChildObjects($cache, $child);
684
-			}
685
-		} else {
686
-			yield $entry;
687
-		}
688
-	}
689
-
690
-	public function copy(string $source, string $target): bool {
691
-		$source = $this->normalizePath($source);
692
-		$target = $this->normalizePath($target);
693
-
694
-		$cache = $this->getCache();
695
-		$sourceEntry = $cache->get($source);
696
-		if (!$sourceEntry) {
697
-			throw new NotFoundException('Source object not found');
698
-		}
699
-
700
-		$this->copyInner($cache, $sourceEntry, $target);
701
-
702
-		return true;
703
-	}
704
-
705
-	private function copyInner(ICache $sourceCache, ICacheEntry $sourceEntry, string $to) {
706
-		$cache = $this->getCache();
707
-
708
-		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
709
-			if ($cache->inCache($to)) {
710
-				$cache->remove($to);
711
-			}
712
-			$this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
713
-
714
-			foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
715
-				$this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
716
-			}
717
-		} else {
718
-			$this->copyFile($sourceEntry, $to);
719
-		}
720
-	}
721
-
722
-	private function copyFile(ICacheEntry $sourceEntry, string $to) {
723
-		$cache = $this->getCache();
724
-
725
-		$sourceUrn = $this->getURN($sourceEntry->getId());
726
-
727
-		if (!$cache instanceof Cache) {
728
-			throw new \Exception('Invalid source cache for object store copy');
729
-		}
730
-
731
-		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
732
-
733
-		$targetUrn = $this->getURN($targetId);
734
-
735
-		try {
736
-			$this->objectStore->copyObject($sourceUrn, $targetUrn);
737
-			if ($this->handleCopiesAsOwned) {
738
-				// Copied the file thus we gain all permissions as we are the owner now ! warning while this aligns with local storage it should not be used and instead fix local storage !
739
-				$cache->update($targetId, ['permissions' => \OCP\Constants::PERMISSION_ALL]);
740
-			}
741
-		} catch (\Exception $e) {
742
-			$cache->remove($to);
743
-
744
-			throw $e;
745
-		}
746
-	}
747
-
748
-	public function startChunkedWrite(string $targetPath): string {
749
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
750
-			throw new GenericFileException('Object store does not support multipart upload');
751
-		}
752
-		$cacheEntry = $this->getCache()->get($targetPath);
753
-		$urn = $this->getURN($cacheEntry->getId());
754
-		return $this->objectStore->initiateMultipartUpload($urn);
755
-	}
756
-
757
-	/**
758
-	 * @throws GenericFileException
759
-	 */
760
-	public function putChunkedWritePart(
761
-		string $targetPath,
762
-		string $writeToken,
763
-		string $chunkId,
764
-		$data,
765
-		$size = null,
766
-	): ?array {
767
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
768
-			throw new GenericFileException('Object store does not support multipart upload');
769
-		}
770
-		$cacheEntry = $this->getCache()->get($targetPath);
771
-		$urn = $this->getURN($cacheEntry->getId());
772
-
773
-		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
774
-
775
-		$parts[$chunkId] = [
776
-			'PartNumber' => $chunkId,
777
-			'ETag' => trim($result->get('ETag'), '"'),
778
-		];
779
-		return $parts[$chunkId];
780
-	}
781
-
782
-	public function completeChunkedWrite(string $targetPath, string $writeToken): int {
783
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
784
-			throw new GenericFileException('Object store does not support multipart upload');
785
-		}
786
-		$cacheEntry = $this->getCache()->get($targetPath);
787
-		$urn = $this->getURN($cacheEntry->getId());
788
-		$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
789
-		$sortedParts = array_values($parts);
790
-		sort($sortedParts);
791
-		try {
792
-			$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
793
-			$stat = $this->stat($targetPath);
794
-			$mtime = time();
795
-			if (is_array($stat)) {
796
-				$stat['size'] = $size;
797
-				$stat['mtime'] = $mtime;
798
-				$stat['mimetype'] = $this->getMimeType($targetPath);
799
-				$this->getCache()->update($stat['fileid'], $stat);
800
-			}
801
-		} catch (S3MultipartUploadException|S3Exception $e) {
802
-			$this->objectStore->abortMultipartUpload($urn, $writeToken);
803
-			$this->logger->error(
804
-				'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
805
-				[
806
-					'app' => 'objectstore',
807
-					'exception' => $e,
808
-				]
809
-			);
810
-			throw new GenericFileException('Could not write chunked file');
811
-		}
812
-		return $size;
813
-	}
814
-
815
-	public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
816
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
817
-			throw new GenericFileException('Object store does not support multipart upload');
818
-		}
819
-		$cacheEntry = $this->getCache()->get($targetPath);
820
-		$urn = $this->getURN($cacheEntry->getId());
821
-		$this->objectStore->abortMultipartUpload($urn, $writeToken);
822
-	}
823
-
824
-	public function setPreserveCacheOnDelete(bool $preserve) {
825
-		$this->preserveCacheItemsOnDelete = $preserve;
826
-	}
541
+                $this->getCache()->remove($uploadPath);
542
+                $this->logger->error(
543
+                    'Could not create object ' . $urn . ' for ' . $path,
544
+                    [
545
+                        'app' => 'objectstore',
546
+                        'exception' => $ex,
547
+                    ]
548
+                );
549
+            } else {
550
+                $this->logger->error(
551
+                    'Could not update object ' . $urn . ' for ' . $path,
552
+                    [
553
+                        'app' => 'objectstore',
554
+                        'exception' => $ex,
555
+                    ]
556
+                );
557
+            }
558
+            throw $ex; // make this bubble up
559
+        }
560
+
561
+        if ($exists) {
562
+            // Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
563
+            $stat['unencrypted_size'] = $stat['size'];
564
+            $this->getCache()->update($fileId, $stat);
565
+        } else {
566
+            if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
567
+                $this->getCache()->move($uploadPath, $path);
568
+            } else {
569
+                $this->getCache()->remove($uploadPath);
570
+                throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
571
+            }
572
+        }
573
+
574
+        return $size;
575
+    }
576
+
577
+    public function getObjectStore(): IObjectStore {
578
+        return $this->objectStore;
579
+    }
580
+
581
+    public function copyFromStorage(
582
+        IStorage $sourceStorage,
583
+        string $sourceInternalPath,
584
+        string $targetInternalPath,
585
+        bool $preserveMtime = false,
586
+    ): bool {
587
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
588
+            /** @var ObjectStoreStorage $sourceStorage */
589
+            if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
590
+                /** @var CacheEntry $sourceEntry */
591
+                $sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
592
+                $sourceEntryData = $sourceEntry->getData();
593
+                // $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
594
+                // user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
595
+                // unjailed storage.
596
+                if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
597
+                    $sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
598
+                }
599
+                $this->copyInner($sourceStorage->getCache(), $sourceEntry, $targetInternalPath);
600
+                return true;
601
+            }
602
+        }
603
+
604
+        return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
605
+    }
606
+
607
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool {
608
+        $sourceCache = $sourceStorage->getCache();
609
+        if (
610
+            $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) &&
611
+            $sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()
612
+        ) {
613
+            if ($this->getCache()->get($targetInternalPath)) {
614
+                $this->unlink($targetInternalPath);
615
+                $this->getCache()->remove($targetInternalPath);
616
+            }
617
+            $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
618
+            // Do not import any data when source and target bucket are identical.
619
+            return true;
620
+        }
621
+        if (!$sourceCacheEntry) {
622
+            $sourceCacheEntry = $sourceCache->get($sourceInternalPath);
623
+        }
624
+
625
+        $this->copyObjects($sourceStorage, $sourceCache, $sourceCacheEntry);
626
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
627
+            /** @var ObjectStoreStorage $sourceStorage */
628
+            $sourceStorage->setPreserveCacheOnDelete(true);
629
+        }
630
+        if ($sourceCacheEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
631
+            $sourceStorage->rmdir($sourceInternalPath);
632
+        } else {
633
+            $sourceStorage->unlink($sourceInternalPath);
634
+        }
635
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
636
+            /** @var ObjectStoreStorage $sourceStorage */
637
+            $sourceStorage->setPreserveCacheOnDelete(false);
638
+        }
639
+        if ($this->getCache()->get($targetInternalPath)) {
640
+            $this->unlink($targetInternalPath);
641
+            $this->getCache()->remove($targetInternalPath);
642
+        }
643
+        $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
644
+
645
+        return true;
646
+    }
647
+
648
+    /**
649
+     * Copy the object(s) of a file or folder into this storage, without touching the cache
650
+     */
651
+    private function copyObjects(IStorage $sourceStorage, ICache $sourceCache, ICacheEntry $sourceCacheEntry) {
652
+        $copiedFiles = [];
653
+        try {
654
+            foreach ($this->getAllChildObjects($sourceCache, $sourceCacheEntry) as $file) {
655
+                $sourceStream = $sourceStorage->fopen($file->getPath(), 'r');
656
+                if (!$sourceStream) {
657
+                    throw new \Exception("Failed to open source file {$file->getPath()} ({$file->getId()})");
658
+                }
659
+                $this->objectStore->writeObject($this->getURN($file->getId()), $sourceStream, $file->getMimeType());
660
+                if (is_resource($sourceStream)) {
661
+                    fclose($sourceStream);
662
+                }
663
+                $copiedFiles[] = $file->getId();
664
+            }
665
+        } catch (\Exception $e) {
666
+            foreach ($copiedFiles as $fileId) {
667
+                try {
668
+                    $this->objectStore->deleteObject($this->getURN($fileId));
669
+                } catch (\Exception $e) {
670
+                    // ignore
671
+                }
672
+            }
673
+            throw $e;
674
+        }
675
+    }
676
+
677
+    /**
678
+     * @return \Iterator<ICacheEntry>
679
+     */
680
+    private function getAllChildObjects(ICache $cache, ICacheEntry $entry): \Iterator {
681
+        if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
682
+            foreach ($cache->getFolderContentsById($entry->getId()) as $child) {
683
+                yield from $this->getAllChildObjects($cache, $child);
684
+            }
685
+        } else {
686
+            yield $entry;
687
+        }
688
+    }
689
+
690
+    public function copy(string $source, string $target): bool {
691
+        $source = $this->normalizePath($source);
692
+        $target = $this->normalizePath($target);
693
+
694
+        $cache = $this->getCache();
695
+        $sourceEntry = $cache->get($source);
696
+        if (!$sourceEntry) {
697
+            throw new NotFoundException('Source object not found');
698
+        }
699
+
700
+        $this->copyInner($cache, $sourceEntry, $target);
701
+
702
+        return true;
703
+    }
704
+
705
+    private function copyInner(ICache $sourceCache, ICacheEntry $sourceEntry, string $to) {
706
+        $cache = $this->getCache();
707
+
708
+        if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
709
+            if ($cache->inCache($to)) {
710
+                $cache->remove($to);
711
+            }
712
+            $this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
713
+
714
+            foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
715
+                $this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
716
+            }
717
+        } else {
718
+            $this->copyFile($sourceEntry, $to);
719
+        }
720
+    }
721
+
722
+    private function copyFile(ICacheEntry $sourceEntry, string $to) {
723
+        $cache = $this->getCache();
724
+
725
+        $sourceUrn = $this->getURN($sourceEntry->getId());
726
+
727
+        if (!$cache instanceof Cache) {
728
+            throw new \Exception('Invalid source cache for object store copy');
729
+        }
730
+
731
+        $targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
732
+
733
+        $targetUrn = $this->getURN($targetId);
734
+
735
+        try {
736
+            $this->objectStore->copyObject($sourceUrn, $targetUrn);
737
+            if ($this->handleCopiesAsOwned) {
738
+                // Copied the file thus we gain all permissions as we are the owner now ! warning while this aligns with local storage it should not be used and instead fix local storage !
739
+                $cache->update($targetId, ['permissions' => \OCP\Constants::PERMISSION_ALL]);
740
+            }
741
+        } catch (\Exception $e) {
742
+            $cache->remove($to);
743
+
744
+            throw $e;
745
+        }
746
+    }
747
+
748
+    public function startChunkedWrite(string $targetPath): string {
749
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
750
+            throw new GenericFileException('Object store does not support multipart upload');
751
+        }
752
+        $cacheEntry = $this->getCache()->get($targetPath);
753
+        $urn = $this->getURN($cacheEntry->getId());
754
+        return $this->objectStore->initiateMultipartUpload($urn);
755
+    }
756
+
757
+    /**
758
+     * @throws GenericFileException
759
+     */
760
+    public function putChunkedWritePart(
761
+        string $targetPath,
762
+        string $writeToken,
763
+        string $chunkId,
764
+        $data,
765
+        $size = null,
766
+    ): ?array {
767
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
768
+            throw new GenericFileException('Object store does not support multipart upload');
769
+        }
770
+        $cacheEntry = $this->getCache()->get($targetPath);
771
+        $urn = $this->getURN($cacheEntry->getId());
772
+
773
+        $result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
774
+
775
+        $parts[$chunkId] = [
776
+            'PartNumber' => $chunkId,
777
+            'ETag' => trim($result->get('ETag'), '"'),
778
+        ];
779
+        return $parts[$chunkId];
780
+    }
781
+
782
+    public function completeChunkedWrite(string $targetPath, string $writeToken): int {
783
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
784
+            throw new GenericFileException('Object store does not support multipart upload');
785
+        }
786
+        $cacheEntry = $this->getCache()->get($targetPath);
787
+        $urn = $this->getURN($cacheEntry->getId());
788
+        $parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
789
+        $sortedParts = array_values($parts);
790
+        sort($sortedParts);
791
+        try {
792
+            $size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
793
+            $stat = $this->stat($targetPath);
794
+            $mtime = time();
795
+            if (is_array($stat)) {
796
+                $stat['size'] = $size;
797
+                $stat['mtime'] = $mtime;
798
+                $stat['mimetype'] = $this->getMimeType($targetPath);
799
+                $this->getCache()->update($stat['fileid'], $stat);
800
+            }
801
+        } catch (S3MultipartUploadException|S3Exception $e) {
802
+            $this->objectStore->abortMultipartUpload($urn, $writeToken);
803
+            $this->logger->error(
804
+                'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
805
+                [
806
+                    'app' => 'objectstore',
807
+                    'exception' => $e,
808
+                ]
809
+            );
810
+            throw new GenericFileException('Could not write chunked file');
811
+        }
812
+        return $size;
813
+    }
814
+
815
+    public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
816
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
817
+            throw new GenericFileException('Object store does not support multipart upload');
818
+        }
819
+        $cacheEntry = $this->getCache()->get($targetPath);
820
+        $urn = $this->getURN($cacheEntry->getId());
821
+        $this->objectStore->abortMultipartUpload($urn, $writeToken);
822
+    }
823
+
824
+    public function setPreserveCacheOnDelete(bool $preserve) {
825
+        $this->preserveCacheItemsOnDelete = $preserve;
826
+    }
827 827
 }
Please login to merge, or discard this patch.
Spacing   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -52,17 +52,17 @@  discard block
 block discarded – undo
52 52
 			throw new \Exception('missing IObjectStore instance');
53 53
 		}
54 54
 		if (isset($parameters['storageid'])) {
55
-			$this->id = 'object::store:' . $parameters['storageid'];
55
+			$this->id = 'object::store:'.$parameters['storageid'];
56 56
 		} else {
57
-			$this->id = 'object::store:' . $this->objectStore->getStorageId();
57
+			$this->id = 'object::store:'.$this->objectStore->getStorageId();
58 58
 		}
59 59
 		if (isset($parameters['objectPrefix'])) {
60 60
 			$this->objectPrefix = $parameters['objectPrefix'];
61 61
 		}
62 62
 		if (isset($parameters['validateWrites'])) {
63
-			$this->validateWrites = (bool)$parameters['validateWrites'];
63
+			$this->validateWrites = (bool) $parameters['validateWrites'];
64 64
 		}
65
-		$this->handleCopiesAsOwned = (bool)($parameters['handleCopiesAsOwned'] ?? false);
65
+		$this->handleCopiesAsOwned = (bool) ($parameters['handleCopiesAsOwned'] ?? false);
66 66
 
67 67
 		$this->logger = \OCP\Server::get(LoggerInterface::class);
68 68
 	}
@@ -196,7 +196,7 @@  discard block
 block discarded – undo
196 196
 		} catch (\Exception $ex) {
197 197
 			if ($ex->getCode() !== 404) {
198 198
 				$this->logger->error(
199
-					'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
199
+					'Could not delete object '.$this->getURN($entry->getId()).' for '.$entry->getPath(),
200 200
 					[
201 201
 						'app' => 'objectstore',
202 202
 						'exception' => $ex,
@@ -212,7 +212,7 @@  discard block
 block discarded – undo
212 212
 		return true;
213 213
 	}
214 214
 
215
-	public function stat(string $path): array|false {
215
+	public function stat(string $path): array | false {
216 216
 		$path = $this->normalizePath($path);
217 217
 		$cacheEntry = $this->getCache()->get($path);
218 218
 		if ($cacheEntry instanceof CacheEntry) {
@@ -247,7 +247,7 @@  discard block
 block discarded – undo
247 247
 	 * @return string the unified resource name used to identify the object
248 248
 	 */
249 249
 	public function getURN(int $fileId): string {
250
-		return $this->objectPrefix . $fileId;
250
+		return $this->objectPrefix.$fileId;
251 251
 	}
252 252
 
253 253
 	public function opendir(string $path) {
@@ -267,7 +267,7 @@  discard block
 block discarded – undo
267 267
 		}
268 268
 	}
269 269
 
270
-	public function filetype(string $path): string|false {
270
+	public function filetype(string $path): string | false {
271 271
 		$path = $this->normalizePath($path);
272 272
 		$stat = $this->stat($path);
273 273
 		if ($stat) {
@@ -308,12 +308,12 @@  discard block
 block discarded – undo
308 308
 						$streamStat = fstat($handle);
309 309
 						$actualSize = $streamStat['size'] ?? -1;
310 310
 						if ($actualSize > -1 && $actualSize !== $filesize) {
311
-							$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
311
+							$this->getCache()->update((int) $stat['fileid'], ['size' => $actualSize]);
312 312
 						}
313 313
 						return $handle;
314 314
 					} catch (NotFoundException $e) {
315 315
 						$this->logger->error(
316
-							'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
316
+							'Could not get object '.$this->getURN($stat['fileid']).' for file '.$path,
317 317
 							[
318 318
 								'app' => 'objectstore',
319 319
 								'exception' => $e,
@@ -322,7 +322,7 @@  discard block
 block discarded – undo
322 322
 						throw $e;
323 323
 					} catch (\Exception $e) {
324 324
 						$this->logger->error(
325
-							'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
325
+							'Could not get object '.$this->getURN($stat['fileid']).' for file '.$path,
326 326
 							[
327 327
 								'app' => 'objectstore',
328 328
 								'exception' => $e,
@@ -346,7 +346,7 @@  discard block
 block discarded – undo
346 346
 
347 347
 				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
348 348
 				$handle = fopen($tmpFile, $mode);
349
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
349
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
350 350
 					$this->writeBack($tmpFile, $path);
351 351
 					unlink($tmpFile);
352 352
 				});
@@ -364,7 +364,7 @@  discard block
 block discarded – undo
364 364
 					file_put_contents($tmpFile, $source);
365 365
 				}
366 366
 				$handle = fopen($tmpFile, $mode);
367
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
367
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
368 368
 					$this->writeBack($tmpFile, $path);
369 369
 					unlink($tmpFile);
370 370
 				});
@@ -374,7 +374,7 @@  discard block
 block discarded – undo
374 374
 
375 375
 	public function file_exists(string $path): bool {
376 376
 		$path = $this->normalizePath($path);
377
-		return (bool)$this->stat($path);
377
+		return (bool) $this->stat($path);
378 378
 	}
379 379
 
380 380
 	public function rename(string $source, string $target): bool {
@@ -386,7 +386,7 @@  discard block
 block discarded – undo
386 386
 		return true;
387 387
 	}
388 388
 
389
-	public function getMimeType(string $path): string|false {
389
+	public function getMimeType(string $path): string | false {
390 390
 		$path = $this->normalizePath($path);
391 391
 		return parent::getMimeType($path);
392 392
 	}
@@ -425,7 +425,7 @@  discard block
 block discarded – undo
425 425
 				$this->getCache()->put($path, $stat);
426 426
 			} catch (\Exception $ex) {
427 427
 				$this->logger->error(
428
-					'Could not create object for ' . $path,
428
+					'Could not create object for '.$path,
429 429
 					[
430 430
 						'app' => 'objectstore',
431 431
 						'exception' => $ex,
@@ -474,7 +474,7 @@  discard block
 block discarded – undo
474 474
 		}
475 475
 		// update stat with new data
476 476
 		$mTime = time();
477
-		$stat['size'] = (int)$size;
477
+		$stat['size'] = (int) $size;
478 478
 		$stat['mtime'] = $mTime;
479 479
 		$stat['storage_mtime'] = $mTime;
480 480
 
@@ -491,7 +491,7 @@  discard block
 block discarded – undo
491 491
 		$stat['checksum'] = '';
492 492
 
493 493
 		$exists = $this->getCache()->inCache($path);
494
-		$uploadPath = $exists ? $path : $path . '.part';
494
+		$uploadPath = $exists ? $path : $path.'.part';
495 495
 
496 496
 		if ($exists) {
497 497
 			$fileId = $stat['fileid'];
@@ -507,7 +507,7 @@  discard block
 block discarded – undo
507 507
 		try {
508 508
 			//upload to object storage
509 509
 			if ($size === null) {
510
-				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
510
+				$countStream = CountWrapper::wrap($stream, function($writtenSize) use ($fileId, &$size) {
511 511
 					$this->getCache()->update($fileId, [
512 512
 						'size' => $writtenSize,
513 513
 					]);
@@ -540,7 +540,7 @@  discard block
 block discarded – undo
540 540
 				 */
541 541
 				$this->getCache()->remove($uploadPath);
542 542
 				$this->logger->error(
543
-					'Could not create object ' . $urn . ' for ' . $path,
543
+					'Could not create object '.$urn.' for '.$path,
544 544
 					[
545 545
 						'app' => 'objectstore',
546 546
 						'exception' => $ex,
@@ -548,7 +548,7 @@  discard block
 block discarded – undo
548 548
 				);
549 549
 			} else {
550 550
 				$this->logger->error(
551
-					'Could not update object ' . $urn . ' for ' . $path,
551
+					'Could not update object '.$urn.' for '.$path,
552 552
 					[
553 553
 						'app' => 'objectstore',
554 554
 						'exception' => $ex,
@@ -712,7 +712,7 @@  discard block
 block discarded – undo
712 712
 			$this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
713 713
 
714 714
 			foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
715
-				$this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
715
+				$this->copyInner($sourceCache, $child, $to.'/'.$child->getName());
716 716
 			}
717 717
 		} else {
718 718
 			$this->copyFile($sourceEntry, $to);
@@ -770,7 +770,7 @@  discard block
 block discarded – undo
770 770
 		$cacheEntry = $this->getCache()->get($targetPath);
771 771
 		$urn = $this->getURN($cacheEntry->getId());
772 772
 
773
-		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
773
+		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int) $chunkId, $data, $size);
774 774
 
775 775
 		$parts[$chunkId] = [
776 776
 			'PartNumber' => $chunkId,
@@ -798,10 +798,10 @@  discard block
 block discarded – undo
798 798
 				$stat['mimetype'] = $this->getMimeType($targetPath);
799 799
 				$this->getCache()->update($stat['fileid'], $stat);
800 800
 			}
801
-		} catch (S3MultipartUploadException|S3Exception $e) {
801
+		} catch (S3MultipartUploadException | S3Exception $e) {
802 802
 			$this->objectStore->abortMultipartUpload($urn, $writeToken);
803 803
 			$this->logger->error(
804
-				'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
804
+				'Could not compete multipart upload '.$urn.' with uploadId '.$writeToken,
805 805
 				[
806 806
 					'app' => 'objectstore',
807 807
 					'exception' => $e,
Please login to merge, or discard this patch.
tests/lib/Files/ObjectStore/ObjectStoreStorageTest.php 1 patch
Indentation   +243 added lines, -243 removed lines patch added patch discarded remove patch
@@ -17,262 +17,262 @@
 block discarded – undo
17 17
  * @group DB
18 18
  */
19 19
 class ObjectStoreStorageTest extends Storage {
20
-	/** @var ObjectStoreStorageOverwrite */
21
-	protected $instance;
22
-
23
-	/**
24
-	 * @var IObjectStore
25
-	 */
26
-	private $objectStorage;
27
-
28
-	protected function setUp(): void {
29
-		parent::setUp();
30
-
31
-		$baseStorage = new Temporary();
32
-		$this->objectStorage = new StorageObjectStore($baseStorage);
33
-		$config['objectstore'] = $this->objectStorage;
34
-		$this->instance = new ObjectStoreStorageOverwrite($config);
35
-	}
36
-
37
-	protected function tearDown(): void {
38
-		if (is_null($this->instance)) {
39
-			return;
40
-		}
41
-		$this->instance->getCache()->clear();
42
-
43
-		parent::tearDown();
44
-	}
45
-
46
-	public function testStat(): void {
47
-		$textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt';
48
-		$ctimeStart = time();
49
-		$this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile));
50
-		$this->assertTrue($this->instance->isReadable('/lorem.txt'));
51
-		$ctimeEnd = time();
52
-		$mTime = $this->instance->filemtime('/lorem.txt');
53
-
54
-		// check that ($ctimeStart - 5) <= $mTime <= ($ctimeEnd + 1)
55
-		$this->assertGreaterThanOrEqual(($ctimeStart - 5), $mTime);
56
-		$this->assertLessThanOrEqual(($ctimeEnd + 1), $mTime);
57
-		$this->assertEquals(filesize($textFile), $this->instance->filesize('/lorem.txt'));
58
-
59
-		$stat = $this->instance->stat('/lorem.txt');
60
-		//only size and mtime are required in the result
61
-		$this->assertEquals($stat['size'], $this->instance->filesize('/lorem.txt'));
62
-		$this->assertEquals($stat['mtime'], $mTime);
63
-
64
-		if ($this->instance->touch('/lorem.txt', 100) !== false) {
65
-			$mTime = $this->instance->filemtime('/lorem.txt');
66
-			$this->assertEquals($mTime, 100);
67
-		}
68
-	}
69
-
70
-	public function testCheckUpdate(): void {
71
-		$this->markTestSkipped('Detecting external changes is not supported on object storages');
72
-	}
73
-
74
-	/**
75
-	 * @dataProvider copyAndMoveProvider
76
-	 */
77
-	public function testMove($source, $target): void {
78
-		$this->initSourceAndTarget($source);
79
-		$sourceId = $this->instance->getCache()->getId(ltrim($source, '/'));
80
-		$this->assertNotEquals(-1, $sourceId);
81
-
82
-		$this->instance->rename($source, $target);
83
-
84
-		$this->assertTrue($this->instance->file_exists($target), $target . ' was not created');
85
-		$this->assertFalse($this->instance->file_exists($source), $source . ' still exists');
86
-		$this->assertSameAsLorem($target);
87
-
88
-		$targetId = $this->instance->getCache()->getId(ltrim($target, '/'));
89
-		$this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
90
-	}
91
-
92
-	public function testRenameDirectory(): void {
93
-		$this->instance->mkdir('source');
94
-		$this->instance->file_put_contents('source/test1.txt', 'foo');
95
-		$this->instance->file_put_contents('source/test2.txt', 'qwerty');
96
-		$this->instance->mkdir('source/subfolder');
97
-		$this->instance->file_put_contents('source/subfolder/test.txt', 'bar');
98
-		$sourceId = $this->instance->getCache()->getId('source');
99
-		$this->assertNotEquals(-1, $sourceId);
100
-		$this->instance->rename('source', 'target');
101
-
102
-		$this->assertFalse($this->instance->file_exists('source'));
103
-		$this->assertFalse($this->instance->file_exists('source/test1.txt'));
104
-		$this->assertFalse($this->instance->file_exists('source/test2.txt'));
105
-		$this->assertFalse($this->instance->file_exists('source/subfolder'));
106
-		$this->assertFalse($this->instance->file_exists('source/subfolder/test.txt'));
107
-
108
-		$this->assertTrue($this->instance->file_exists('target'));
109
-		$this->assertTrue($this->instance->file_exists('target/test1.txt'));
110
-		$this->assertTrue($this->instance->file_exists('target/test2.txt'));
111
-		$this->assertTrue($this->instance->file_exists('target/subfolder'));
112
-		$this->assertTrue($this->instance->file_exists('target/subfolder/test.txt'));
113
-
114
-		$this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
115
-		$this->assertEquals('qwerty', $this->instance->file_get_contents('target/test2.txt'));
116
-		$this->assertEquals('bar', $this->instance->file_get_contents('target/subfolder/test.txt'));
117
-		$targetId = $this->instance->getCache()->getId('target');
118
-		$this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
119
-	}
120
-
121
-	public function testRenameOverWriteDirectory(): void {
122
-		$this->instance->mkdir('source');
123
-		$this->instance->file_put_contents('source/test1.txt', 'foo');
124
-		$sourceId = $this->instance->getCache()->getId('source');
125
-		$this->assertNotEquals(-1, $sourceId);
126
-
127
-		$this->instance->mkdir('target');
128
-		$this->instance->file_put_contents('target/test1.txt', 'bar');
129
-		$this->instance->file_put_contents('target/test2.txt', 'bar');
130
-
131
-		$this->instance->rename('source', 'target');
132
-
133
-		$this->assertFalse($this->instance->file_exists('source'));
134
-		$this->assertFalse($this->instance->file_exists('source/test1.txt'));
135
-		$this->assertFalse($this->instance->file_exists('target/test2.txt'));
136
-		$this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
137
-		$targetId = $this->instance->getCache()->getId('target');
138
-		$this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
139
-	}
140
-
141
-	public function testRenameOverWriteDirectoryOverFile(): void {
142
-		$this->instance->mkdir('source');
143
-		$this->instance->file_put_contents('source/test1.txt', 'foo');
144
-		$sourceId = $this->instance->getCache()->getId('source');
145
-		$this->assertNotEquals(-1, $sourceId);
146
-
147
-		$this->instance->file_put_contents('target', 'bar');
148
-
149
-		$this->instance->rename('source', 'target');
150
-
151
-		$this->assertFalse($this->instance->file_exists('source'));
152
-		$this->assertFalse($this->instance->file_exists('source/test1.txt'));
153
-		$this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
154
-		$targetId = $this->instance->getCache()->getId('target');
155
-		$this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
156
-	}
157
-
158
-	public function testWriteObjectSilentFailure(): void {
159
-		$objectStore = $this->instance->getObjectStore();
160
-		$this->instance->setObjectStore(new FailWriteObjectStore($objectStore));
161
-
162
-		try {
163
-			$this->instance->file_put_contents('test.txt', 'foo');
164
-			$this->fail('expected exception');
165
-		} catch (\Exception $e) {
166
-			$this->assertStringStartsWith('Object not found after writing', $e->getMessage());
167
-		}
168
-		$this->assertFalse($this->instance->file_exists('test.txt'));
169
-	}
170
-
171
-	public function testWriteObjectSilentFailureNoCheck(): void {
172
-		$objectStore = $this->instance->getObjectStore();
173
-		$this->instance->setObjectStore(new FailWriteObjectStore($objectStore));
174
-		$this->instance->setValidateWrites(false);
175
-
176
-		$this->instance->file_put_contents('test.txt', 'foo');
177
-		$this->assertTrue($this->instance->file_exists('test.txt'));
178
-	}
179
-
180
-	public function testDeleteObjectFailureKeepCache(): void {
181
-		$objectStore = $this->instance->getObjectStore();
182
-		$this->instance->setObjectStore(new FailDeleteObjectStore($objectStore));
183
-		$cache = $this->instance->getCache();
184
-
185
-		$this->instance->file_put_contents('test.txt', 'foo');
186
-
187
-		$this->assertTrue($cache->inCache('test.txt'));
188
-
189
-		$this->assertFalse($this->instance->unlink('test.txt'));
190
-
191
-		$this->assertTrue($cache->inCache('test.txt'));
192
-
193
-		$this->instance->mkdir('foo');
194
-		$this->instance->file_put_contents('foo/test.txt', 'foo');
195
-
196
-		$this->assertTrue($cache->inCache('foo'));
197
-		$this->assertTrue($cache->inCache('foo/test.txt'));
198
-
199
-		$this->instance->rmdir('foo');
200
-
201
-		$this->assertTrue($cache->inCache('foo'));
202
-		$this->assertTrue($cache->inCache('foo/test.txt'));
203
-	}
204
-
205
-	public function testCopyBetweenJails(): void {
206
-		$this->instance->mkdir('a');
207
-		$this->instance->mkdir('b');
208
-		$jailA = new Jail([
209
-			'storage' => $this->instance,
210
-			'root' => 'a'
211
-		]);
212
-		$jailB = new Jail([
213
-			'storage' => $this->instance,
214
-			'root' => 'b'
215
-		]);
216
-		$jailA->mkdir('sub');
217
-		$jailA->file_put_contents('1.txt', '1');
218
-		$jailA->file_put_contents('sub/2.txt', '2');
219
-		$jailA->file_put_contents('sub/3.txt', '3');
20
+    /** @var ObjectStoreStorageOverwrite */
21
+    protected $instance;
22
+
23
+    /**
24
+     * @var IObjectStore
25
+     */
26
+    private $objectStorage;
27
+
28
+    protected function setUp(): void {
29
+        parent::setUp();
30
+
31
+        $baseStorage = new Temporary();
32
+        $this->objectStorage = new StorageObjectStore($baseStorage);
33
+        $config['objectstore'] = $this->objectStorage;
34
+        $this->instance = new ObjectStoreStorageOverwrite($config);
35
+    }
36
+
37
+    protected function tearDown(): void {
38
+        if (is_null($this->instance)) {
39
+            return;
40
+        }
41
+        $this->instance->getCache()->clear();
42
+
43
+        parent::tearDown();
44
+    }
45
+
46
+    public function testStat(): void {
47
+        $textFile = \OC::$SERVERROOT . '/tests/data/lorem.txt';
48
+        $ctimeStart = time();
49
+        $this->instance->file_put_contents('/lorem.txt', file_get_contents($textFile));
50
+        $this->assertTrue($this->instance->isReadable('/lorem.txt'));
51
+        $ctimeEnd = time();
52
+        $mTime = $this->instance->filemtime('/lorem.txt');
53
+
54
+        // check that ($ctimeStart - 5) <= $mTime <= ($ctimeEnd + 1)
55
+        $this->assertGreaterThanOrEqual(($ctimeStart - 5), $mTime);
56
+        $this->assertLessThanOrEqual(($ctimeEnd + 1), $mTime);
57
+        $this->assertEquals(filesize($textFile), $this->instance->filesize('/lorem.txt'));
58
+
59
+        $stat = $this->instance->stat('/lorem.txt');
60
+        //only size and mtime are required in the result
61
+        $this->assertEquals($stat['size'], $this->instance->filesize('/lorem.txt'));
62
+        $this->assertEquals($stat['mtime'], $mTime);
63
+
64
+        if ($this->instance->touch('/lorem.txt', 100) !== false) {
65
+            $mTime = $this->instance->filemtime('/lorem.txt');
66
+            $this->assertEquals($mTime, 100);
67
+        }
68
+    }
69
+
70
+    public function testCheckUpdate(): void {
71
+        $this->markTestSkipped('Detecting external changes is not supported on object storages');
72
+    }
73
+
74
+    /**
75
+     * @dataProvider copyAndMoveProvider
76
+     */
77
+    public function testMove($source, $target): void {
78
+        $this->initSourceAndTarget($source);
79
+        $sourceId = $this->instance->getCache()->getId(ltrim($source, '/'));
80
+        $this->assertNotEquals(-1, $sourceId);
81
+
82
+        $this->instance->rename($source, $target);
83
+
84
+        $this->assertTrue($this->instance->file_exists($target), $target . ' was not created');
85
+        $this->assertFalse($this->instance->file_exists($source), $source . ' still exists');
86
+        $this->assertSameAsLorem($target);
87
+
88
+        $targetId = $this->instance->getCache()->getId(ltrim($target, '/'));
89
+        $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
90
+    }
91
+
92
+    public function testRenameDirectory(): void {
93
+        $this->instance->mkdir('source');
94
+        $this->instance->file_put_contents('source/test1.txt', 'foo');
95
+        $this->instance->file_put_contents('source/test2.txt', 'qwerty');
96
+        $this->instance->mkdir('source/subfolder');
97
+        $this->instance->file_put_contents('source/subfolder/test.txt', 'bar');
98
+        $sourceId = $this->instance->getCache()->getId('source');
99
+        $this->assertNotEquals(-1, $sourceId);
100
+        $this->instance->rename('source', 'target');
101
+
102
+        $this->assertFalse($this->instance->file_exists('source'));
103
+        $this->assertFalse($this->instance->file_exists('source/test1.txt'));
104
+        $this->assertFalse($this->instance->file_exists('source/test2.txt'));
105
+        $this->assertFalse($this->instance->file_exists('source/subfolder'));
106
+        $this->assertFalse($this->instance->file_exists('source/subfolder/test.txt'));
107
+
108
+        $this->assertTrue($this->instance->file_exists('target'));
109
+        $this->assertTrue($this->instance->file_exists('target/test1.txt'));
110
+        $this->assertTrue($this->instance->file_exists('target/test2.txt'));
111
+        $this->assertTrue($this->instance->file_exists('target/subfolder'));
112
+        $this->assertTrue($this->instance->file_exists('target/subfolder/test.txt'));
113
+
114
+        $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
115
+        $this->assertEquals('qwerty', $this->instance->file_get_contents('target/test2.txt'));
116
+        $this->assertEquals('bar', $this->instance->file_get_contents('target/subfolder/test.txt'));
117
+        $targetId = $this->instance->getCache()->getId('target');
118
+        $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
119
+    }
120
+
121
+    public function testRenameOverWriteDirectory(): void {
122
+        $this->instance->mkdir('source');
123
+        $this->instance->file_put_contents('source/test1.txt', 'foo');
124
+        $sourceId = $this->instance->getCache()->getId('source');
125
+        $this->assertNotEquals(-1, $sourceId);
126
+
127
+        $this->instance->mkdir('target');
128
+        $this->instance->file_put_contents('target/test1.txt', 'bar');
129
+        $this->instance->file_put_contents('target/test2.txt', 'bar');
130
+
131
+        $this->instance->rename('source', 'target');
132
+
133
+        $this->assertFalse($this->instance->file_exists('source'));
134
+        $this->assertFalse($this->instance->file_exists('source/test1.txt'));
135
+        $this->assertFalse($this->instance->file_exists('target/test2.txt'));
136
+        $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
137
+        $targetId = $this->instance->getCache()->getId('target');
138
+        $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
139
+    }
140
+
141
+    public function testRenameOverWriteDirectoryOverFile(): void {
142
+        $this->instance->mkdir('source');
143
+        $this->instance->file_put_contents('source/test1.txt', 'foo');
144
+        $sourceId = $this->instance->getCache()->getId('source');
145
+        $this->assertNotEquals(-1, $sourceId);
146
+
147
+        $this->instance->file_put_contents('target', 'bar');
148
+
149
+        $this->instance->rename('source', 'target');
150
+
151
+        $this->assertFalse($this->instance->file_exists('source'));
152
+        $this->assertFalse($this->instance->file_exists('source/test1.txt'));
153
+        $this->assertEquals('foo', $this->instance->file_get_contents('target/test1.txt'));
154
+        $targetId = $this->instance->getCache()->getId('target');
155
+        $this->assertSame($sourceId, $targetId, 'fileid must be stable on move or shares will break');
156
+    }
157
+
158
+    public function testWriteObjectSilentFailure(): void {
159
+        $objectStore = $this->instance->getObjectStore();
160
+        $this->instance->setObjectStore(new FailWriteObjectStore($objectStore));
161
+
162
+        try {
163
+            $this->instance->file_put_contents('test.txt', 'foo');
164
+            $this->fail('expected exception');
165
+        } catch (\Exception $e) {
166
+            $this->assertStringStartsWith('Object not found after writing', $e->getMessage());
167
+        }
168
+        $this->assertFalse($this->instance->file_exists('test.txt'));
169
+    }
170
+
171
+    public function testWriteObjectSilentFailureNoCheck(): void {
172
+        $objectStore = $this->instance->getObjectStore();
173
+        $this->instance->setObjectStore(new FailWriteObjectStore($objectStore));
174
+        $this->instance->setValidateWrites(false);
175
+
176
+        $this->instance->file_put_contents('test.txt', 'foo');
177
+        $this->assertTrue($this->instance->file_exists('test.txt'));
178
+    }
179
+
180
+    public function testDeleteObjectFailureKeepCache(): void {
181
+        $objectStore = $this->instance->getObjectStore();
182
+        $this->instance->setObjectStore(new FailDeleteObjectStore($objectStore));
183
+        $cache = $this->instance->getCache();
184
+
185
+        $this->instance->file_put_contents('test.txt', 'foo');
186
+
187
+        $this->assertTrue($cache->inCache('test.txt'));
188
+
189
+        $this->assertFalse($this->instance->unlink('test.txt'));
190
+
191
+        $this->assertTrue($cache->inCache('test.txt'));
192
+
193
+        $this->instance->mkdir('foo');
194
+        $this->instance->file_put_contents('foo/test.txt', 'foo');
195
+
196
+        $this->assertTrue($cache->inCache('foo'));
197
+        $this->assertTrue($cache->inCache('foo/test.txt'));
198
+
199
+        $this->instance->rmdir('foo');
200
+
201
+        $this->assertTrue($cache->inCache('foo'));
202
+        $this->assertTrue($cache->inCache('foo/test.txt'));
203
+    }
204
+
205
+    public function testCopyBetweenJails(): void {
206
+        $this->instance->mkdir('a');
207
+        $this->instance->mkdir('b');
208
+        $jailA = new Jail([
209
+            'storage' => $this->instance,
210
+            'root' => 'a'
211
+        ]);
212
+        $jailB = new Jail([
213
+            'storage' => $this->instance,
214
+            'root' => 'b'
215
+        ]);
216
+        $jailA->mkdir('sub');
217
+        $jailA->file_put_contents('1.txt', '1');
218
+        $jailA->file_put_contents('sub/2.txt', '2');
219
+        $jailA->file_put_contents('sub/3.txt', '3');
220 220
 
221
-		$jailB->copyFromStorage($jailA, '', 'target');
222
-
223
-		$this->assertEquals('1', $this->instance->file_get_contents('b/target/1.txt'));
224
-		$this->assertEquals('2', $this->instance->file_get_contents('b/target/sub/2.txt'));
225
-		$this->assertEquals('3', $this->instance->file_get_contents('b/target/sub/3.txt'));
226
-	}
221
+        $jailB->copyFromStorage($jailA, '', 'target');
222
+
223
+        $this->assertEquals('1', $this->instance->file_get_contents('b/target/1.txt'));
224
+        $this->assertEquals('2', $this->instance->file_get_contents('b/target/sub/2.txt'));
225
+        $this->assertEquals('3', $this->instance->file_get_contents('b/target/sub/3.txt'));
226
+    }
227 227
 
228
-	public function testCopyPreservesPermissions(): void {
229
-		$cache = $this->instance->getCache();
228
+    public function testCopyPreservesPermissions(): void {
229
+        $cache = $this->instance->getCache();
230 230
 
231
-		$this->instance->file_put_contents('test.txt', 'foo');
232
-		$this->assertTrue($cache->inCache('test.txt'));
233
-
234
-		$cache->update($cache->getId('test.txt'), ['permissions' => \OCP\Constants::PERMISSION_READ]);
235
-		$this->assertEquals(\OCP\Constants::PERMISSION_READ, $this->instance->getPermissions('test.txt'));
231
+        $this->instance->file_put_contents('test.txt', 'foo');
232
+        $this->assertTrue($cache->inCache('test.txt'));
233
+
234
+        $cache->update($cache->getId('test.txt'), ['permissions' => \OCP\Constants::PERMISSION_READ]);
235
+        $this->assertEquals(\OCP\Constants::PERMISSION_READ, $this->instance->getPermissions('test.txt'));
236 236
 
237
-		$this->assertTrue($this->instance->copy('test.txt', 'new.txt'));
237
+        $this->assertTrue($this->instance->copy('test.txt', 'new.txt'));
238 238
 
239
-		$this->assertTrue($cache->inCache('new.txt'));
240
-		$this->assertEquals(\OCP\Constants::PERMISSION_READ, $this->instance->getPermissions('new.txt'));
241
-	}
239
+        $this->assertTrue($cache->inCache('new.txt'));
240
+        $this->assertEquals(\OCP\Constants::PERMISSION_READ, $this->instance->getPermissions('new.txt'));
241
+    }
242 242
 
243
-	/**
244
-	 * Test that copying files will drop permissions like local storage does
245
-	 * TODO: Drop this and fix local storage
246
-	 */
247
-	public function testCopyGrantsPermissions(): void {
248
-		$config['objectstore'] = $this->objectStorage;
249
-		$config['handleCopiesAsOwned'] = true;
250
-		$instance = new ObjectStoreStorageOverwrite($config);
243
+    /**
244
+     * Test that copying files will drop permissions like local storage does
245
+     * TODO: Drop this and fix local storage
246
+     */
247
+    public function testCopyGrantsPermissions(): void {
248
+        $config['objectstore'] = $this->objectStorage;
249
+        $config['handleCopiesAsOwned'] = true;
250
+        $instance = new ObjectStoreStorageOverwrite($config);
251 251
 
252
-		$cache = $instance->getCache();
252
+        $cache = $instance->getCache();
253 253
 
254
-		$instance->file_put_contents('test.txt', 'foo');
255
-		$this->assertTrue($cache->inCache('test.txt'));
254
+        $instance->file_put_contents('test.txt', 'foo');
255
+        $this->assertTrue($cache->inCache('test.txt'));
256 256
 
257
-		$cache->update($cache->getId('test.txt'), ['permissions' => \OCP\Constants::PERMISSION_READ]);
258
-		$this->assertEquals(\OCP\Constants::PERMISSION_READ, $instance->getPermissions('test.txt'));
257
+        $cache->update($cache->getId('test.txt'), ['permissions' => \OCP\Constants::PERMISSION_READ]);
258
+        $this->assertEquals(\OCP\Constants::PERMISSION_READ, $instance->getPermissions('test.txt'));
259 259
 
260
-		$this->assertTrue($instance->copy('test.txt', 'new.txt'));
260
+        $this->assertTrue($instance->copy('test.txt', 'new.txt'));
261 261
 
262
-		$this->assertTrue($cache->inCache('new.txt'));
263
-		$this->assertEquals(\OCP\Constants::PERMISSION_ALL, $instance->getPermissions('new.txt'));
264
-	}
262
+        $this->assertTrue($cache->inCache('new.txt'));
263
+        $this->assertEquals(\OCP\Constants::PERMISSION_ALL, $instance->getPermissions('new.txt'));
264
+    }
265 265
 
266
-	public function testCopyFolderSize(): void {
267
-		$cache = $this->instance->getCache();
266
+    public function testCopyFolderSize(): void {
267
+        $cache = $this->instance->getCache();
268 268
 
269
-		$this->instance->mkdir('source');
270
-		$this->instance->file_put_contents('source/test.txt', 'foo');
271
-		$this->instance->getUpdater()->update('source/test.txt');
272
-		$this->assertEquals(3, $cache->get('source')->getSize());
269
+        $this->instance->mkdir('source');
270
+        $this->instance->file_put_contents('source/test.txt', 'foo');
271
+        $this->instance->getUpdater()->update('source/test.txt');
272
+        $this->assertEquals(3, $cache->get('source')->getSize());
273 273
 
274
-		$this->assertTrue($this->instance->copy('source', 'target'));
274
+        $this->assertTrue($this->instance->copy('source', 'target'));
275 275
 
276
-		$this->assertEquals(3, $cache->get('target')->getSize());
277
-	}
276
+        $this->assertEquals(3, $cache->get('target')->getSize());
277
+    }
278 278
 }
Please login to merge, or discard this patch.