Completed
Push — master ( f05f79...419e0a )
by
unknown
25:14 queued 14s
created
lib/private/Files/ObjectStore/ObjectStoreStorage.php 2 patches
Indentation   +782 added lines, -782 removed lines patch added patch discarded remove patch
@@ -29,789 +29,789 @@
 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
-			} catch (\Exception $ex) {
417
-				$this->logger->error(
418
-					'Could not create object for ' . $path,
419
-					[
420
-						'app' => 'objectstore',
421
-						'exception' => $ex,
422
-					]
423
-				);
424
-				throw $ex;
425
-			}
426
-		}
427
-		return true;
428
-	}
429
-
430
-	public function writeBack(string $tmpFile, string $path) {
431
-		$size = filesize($tmpFile);
432
-		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
433
-	}
434
-
435
-	public function hasUpdated(string $path, int $time): bool {
436
-		return false;
437
-	}
438
-
439
-	public function needsPartFile(): bool {
440
-		return false;
441
-	}
442
-
443
-	public function file_put_contents(string $path, mixed $data): int {
444
-		$fh = fopen('php://temp', 'w+');
445
-		fwrite($fh, $data);
446
-		rewind($fh);
447
-		return $this->writeStream($path, $fh, strlen($data));
448
-	}
449
-
450
-	public function writeStream(string $path, $stream, ?int $size = null): int {
451
-		if ($size === null) {
452
-			$stats = fstat($stream);
453
-			if (is_array($stats) && isset($stats['size'])) {
454
-				$size = $stats['size'];
455
-			}
456
-		}
457
-
458
-		$stat = $this->stat($path);
459
-		if (empty($stat)) {
460
-			// create new file
461
-			$stat = [
462
-				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
463
-			];
464
-		}
465
-		// update stat with new data
466
-		$mTime = time();
467
-		$stat['size'] = (int)$size;
468
-		$stat['mtime'] = $mTime;
469
-		$stat['storage_mtime'] = $mTime;
470
-
471
-		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
472
-		$mimetype = $mimetypeDetector->detectPath($path);
473
-		$metadata = [
474
-			'mimetype' => $mimetype,
475
-			'original-storage' => $this->getId(),
476
-			'original-path' => $path,
477
-		];
478
-
479
-		$stat['mimetype'] = $mimetype;
480
-		$stat['etag'] = $this->getETag($path);
481
-		$stat['checksum'] = '';
482
-
483
-		$exists = $this->getCache()->inCache($path);
484
-		$uploadPath = $exists ? $path : $path . '.part';
485
-
486
-		if ($exists) {
487
-			$fileId = $stat['fileid'];
488
-		} else {
489
-			$parent = $this->normalizePath(dirname($path));
490
-			if (!$this->is_dir($parent)) {
491
-				throw new \InvalidArgumentException("trying to upload a file ($path) inside a non-directory ($parent)");
492
-			}
493
-			$fileId = $this->getCache()->put($uploadPath, $stat);
494
-		}
495
-
496
-		$urn = $this->getURN($fileId);
497
-		try {
498
-			//upload to object storage
499
-			if ($size === null) {
500
-				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
501
-					$this->getCache()->update($fileId, [
502
-						'size' => $writtenSize,
503
-					]);
504
-					$size = $writtenSize;
505
-				});
506
-				if ($this->objectStore instanceof IObjectStoreMetaData) {
507
-					$this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata);
508
-				} else {
509
-					$this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']);
510
-				}
511
-				if (is_resource($countStream)) {
512
-					fclose($countStream);
513
-				}
514
-				$stat['size'] = $size;
515
-			} else {
516
-				if ($this->objectStore instanceof IObjectStoreMetaData) {
517
-					$this->objectStore->writeObjectWithMetaData($urn, $stream, $metadata);
518
-				} else {
519
-					$this->objectStore->writeObject($urn, $stream, $metadata['mimetype']);
520
-				}
521
-				if (is_resource($stream)) {
522
-					fclose($stream);
523
-				}
524
-			}
525
-		} catch (\Exception $ex) {
526
-			if (!$exists) {
527
-				/*
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
+            } catch (\Exception $ex) {
417
+                $this->logger->error(
418
+                    'Could not create object for ' . $path,
419
+                    [
420
+                        'app' => 'objectstore',
421
+                        'exception' => $ex,
422
+                    ]
423
+                );
424
+                throw $ex;
425
+            }
426
+        }
427
+        return true;
428
+    }
429
+
430
+    public function writeBack(string $tmpFile, string $path) {
431
+        $size = filesize($tmpFile);
432
+        $this->writeStream($path, fopen($tmpFile, 'r'), $size);
433
+    }
434
+
435
+    public function hasUpdated(string $path, int $time): bool {
436
+        return false;
437
+    }
438
+
439
+    public function needsPartFile(): bool {
440
+        return false;
441
+    }
442
+
443
+    public function file_put_contents(string $path, mixed $data): int {
444
+        $fh = fopen('php://temp', 'w+');
445
+        fwrite($fh, $data);
446
+        rewind($fh);
447
+        return $this->writeStream($path, $fh, strlen($data));
448
+    }
449
+
450
+    public function writeStream(string $path, $stream, ?int $size = null): int {
451
+        if ($size === null) {
452
+            $stats = fstat($stream);
453
+            if (is_array($stats) && isset($stats['size'])) {
454
+                $size = $stats['size'];
455
+            }
456
+        }
457
+
458
+        $stat = $this->stat($path);
459
+        if (empty($stat)) {
460
+            // create new file
461
+            $stat = [
462
+                'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
463
+            ];
464
+        }
465
+        // update stat with new data
466
+        $mTime = time();
467
+        $stat['size'] = (int)$size;
468
+        $stat['mtime'] = $mTime;
469
+        $stat['storage_mtime'] = $mTime;
470
+
471
+        $mimetypeDetector = \OC::$server->getMimeTypeDetector();
472
+        $mimetype = $mimetypeDetector->detectPath($path);
473
+        $metadata = [
474
+            'mimetype' => $mimetype,
475
+            'original-storage' => $this->getId(),
476
+            'original-path' => $path,
477
+        ];
478
+
479
+        $stat['mimetype'] = $mimetype;
480
+        $stat['etag'] = $this->getETag($path);
481
+        $stat['checksum'] = '';
482
+
483
+        $exists = $this->getCache()->inCache($path);
484
+        $uploadPath = $exists ? $path : $path . '.part';
485
+
486
+        if ($exists) {
487
+            $fileId = $stat['fileid'];
488
+        } else {
489
+            $parent = $this->normalizePath(dirname($path));
490
+            if (!$this->is_dir($parent)) {
491
+                throw new \InvalidArgumentException("trying to upload a file ($path) inside a non-directory ($parent)");
492
+            }
493
+            $fileId = $this->getCache()->put($uploadPath, $stat);
494
+        }
495
+
496
+        $urn = $this->getURN($fileId);
497
+        try {
498
+            //upload to object storage
499
+            if ($size === null) {
500
+                $countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
501
+                    $this->getCache()->update($fileId, [
502
+                        'size' => $writtenSize,
503
+                    ]);
504
+                    $size = $writtenSize;
505
+                });
506
+                if ($this->objectStore instanceof IObjectStoreMetaData) {
507
+                    $this->objectStore->writeObjectWithMetaData($urn, $countStream, $metadata);
508
+                } else {
509
+                    $this->objectStore->writeObject($urn, $countStream, $metadata['mimetype']);
510
+                }
511
+                if (is_resource($countStream)) {
512
+                    fclose($countStream);
513
+                }
514
+                $stat['size'] = $size;
515
+            } else {
516
+                if ($this->objectStore instanceof IObjectStoreMetaData) {
517
+                    $this->objectStore->writeObjectWithMetaData($urn, $stream, $metadata);
518
+                } else {
519
+                    $this->objectStore->writeObject($urn, $stream, $metadata['mimetype']);
520
+                }
521
+                if (is_resource($stream)) {
522
+                    fclose($stream);
523
+                }
524
+            }
525
+        } catch (\Exception $ex) {
526
+            if (!$exists) {
527
+                /*
528 528
 				 * Only remove the entry if we are dealing with a new file.
529 529
 				 * Else people lose access to existing files
530 530
 				 */
531
-				$this->getCache()->remove($uploadPath);
532
-				$this->logger->error(
533
-					'Could not create object ' . $urn . ' for ' . $path,
534
-					[
535
-						'app' => 'objectstore',
536
-						'exception' => $ex,
537
-					]
538
-				);
539
-			} else {
540
-				$this->logger->error(
541
-					'Could not update object ' . $urn . ' for ' . $path,
542
-					[
543
-						'app' => 'objectstore',
544
-						'exception' => $ex,
545
-					]
546
-				);
547
-			}
548
-			throw $ex; // make this bubble up
549
-		}
550
-
551
-		if ($exists) {
552
-			// Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
553
-			$stat['unencrypted_size'] = $stat['size'];
554
-			$this->getCache()->update($fileId, $stat);
555
-		} else {
556
-			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
557
-				$this->getCache()->move($uploadPath, $path);
558
-			} else {
559
-				$this->getCache()->remove($uploadPath);
560
-				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
561
-			}
562
-		}
563
-
564
-		return $size;
565
-	}
566
-
567
-	public function getObjectStore(): IObjectStore {
568
-		return $this->objectStore;
569
-	}
570
-
571
-	public function copyFromStorage(
572
-		IStorage $sourceStorage,
573
-		string $sourceInternalPath,
574
-		string $targetInternalPath,
575
-		bool $preserveMtime = false,
576
-	): bool {
577
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
578
-			/** @var ObjectStoreStorage $sourceStorage */
579
-			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
580
-				/** @var CacheEntry $sourceEntry */
581
-				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
582
-				$sourceEntryData = $sourceEntry->getData();
583
-				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
584
-				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
585
-				// unjailed storage.
586
-				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
587
-					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
588
-				}
589
-				$this->copyInner($sourceStorage->getCache(), $sourceEntry, $targetInternalPath);
590
-				return true;
591
-			}
592
-		}
593
-
594
-		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
595
-	}
596
-
597
-	public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool {
598
-		$sourceCache = $sourceStorage->getCache();
599
-		if (
600
-			$sourceStorage->instanceOfStorage(ObjectStoreStorage::class) &&
601
-			$sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()
602
-		) {
603
-			if ($this->getCache()->get($targetInternalPath)) {
604
-				$this->unlink($targetInternalPath);
605
-				$this->getCache()->remove($targetInternalPath);
606
-			}
607
-			$this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
608
-			// Do not import any data when source and target bucket are identical.
609
-			return true;
610
-		}
611
-		if (!$sourceCacheEntry) {
612
-			$sourceCacheEntry = $sourceCache->get($sourceInternalPath);
613
-		}
614
-
615
-		$this->copyObjects($sourceStorage, $sourceCache, $sourceCacheEntry);
616
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
617
-			/** @var ObjectStoreStorage $sourceStorage */
618
-			$sourceStorage->setPreserveCacheOnDelete(true);
619
-		}
620
-		if ($sourceCacheEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
621
-			$sourceStorage->rmdir($sourceInternalPath);
622
-		} else {
623
-			$sourceStorage->unlink($sourceInternalPath);
624
-		}
625
-		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
626
-			/** @var ObjectStoreStorage $sourceStorage */
627
-			$sourceStorage->setPreserveCacheOnDelete(false);
628
-		}
629
-		if ($this->getCache()->get($targetInternalPath)) {
630
-			$this->unlink($targetInternalPath);
631
-			$this->getCache()->remove($targetInternalPath);
632
-		}
633
-		$this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
634
-
635
-		return true;
636
-	}
637
-
638
-	/**
639
-	 * Copy the object(s) of a file or folder into this storage, without touching the cache
640
-	 */
641
-	private function copyObjects(IStorage $sourceStorage, ICache $sourceCache, ICacheEntry $sourceCacheEntry) {
642
-		$copiedFiles = [];
643
-		try {
644
-			foreach ($this->getAllChildObjects($sourceCache, $sourceCacheEntry) as $file) {
645
-				$sourceStream = $sourceStorage->fopen($file->getPath(), 'r');
646
-				if (!$sourceStream) {
647
-					throw new \Exception("Failed to open source file {$file->getPath()} ({$file->getId()})");
648
-				}
649
-				$this->objectStore->writeObject($this->getURN($file->getId()), $sourceStream, $file->getMimeType());
650
-				if (is_resource($sourceStream)) {
651
-					fclose($sourceStream);
652
-				}
653
-				$copiedFiles[] = $file->getId();
654
-			}
655
-		} catch (\Exception $e) {
656
-			foreach ($copiedFiles as $fileId) {
657
-				try {
658
-					$this->objectStore->deleteObject($this->getURN($fileId));
659
-				} catch (\Exception $e) {
660
-					// ignore
661
-				}
662
-			}
663
-			throw $e;
664
-		}
665
-	}
666
-
667
-	/**
668
-	 * @return \Iterator<ICacheEntry>
669
-	 */
670
-	private function getAllChildObjects(ICache $cache, ICacheEntry $entry): \Iterator {
671
-		if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
672
-			foreach ($cache->getFolderContentsById($entry->getId()) as $child) {
673
-				yield from $this->getAllChildObjects($cache, $child);
674
-			}
675
-		} else {
676
-			yield $entry;
677
-		}
678
-	}
679
-
680
-	public function copy(string $source, string $target): bool {
681
-		$source = $this->normalizePath($source);
682
-		$target = $this->normalizePath($target);
683
-
684
-		$cache = $this->getCache();
685
-		$sourceEntry = $cache->get($source);
686
-		if (!$sourceEntry) {
687
-			throw new NotFoundException('Source object not found');
688
-		}
689
-
690
-		$this->copyInner($cache, $sourceEntry, $target);
691
-
692
-		return true;
693
-	}
694
-
695
-	private function copyInner(ICache $sourceCache, ICacheEntry $sourceEntry, string $to) {
696
-		$cache = $this->getCache();
697
-
698
-		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
699
-			if ($cache->inCache($to)) {
700
-				$cache->remove($to);
701
-			}
702
-			$this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
703
-
704
-			foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
705
-				$this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
706
-			}
707
-		} else {
708
-			$this->copyFile($sourceEntry, $to);
709
-		}
710
-	}
711
-
712
-	private function copyFile(ICacheEntry $sourceEntry, string $to) {
713
-		$cache = $this->getCache();
714
-
715
-		$sourceUrn = $this->getURN($sourceEntry->getId());
716
-
717
-		if (!$cache instanceof Cache) {
718
-			throw new \Exception('Invalid source cache for object store copy');
719
-		}
720
-
721
-		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
722
-
723
-		$targetUrn = $this->getURN($targetId);
724
-
725
-		try {
726
-			$this->objectStore->copyObject($sourceUrn, $targetUrn);
727
-			if ($this->handleCopiesAsOwned) {
728
-				// 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 !
729
-				$cache->update($targetId, ['permissions' => \OCP\Constants::PERMISSION_ALL]);
730
-			}
731
-		} catch (\Exception $e) {
732
-			$cache->remove($to);
733
-
734
-			throw $e;
735
-		}
736
-	}
737
-
738
-	public function startChunkedWrite(string $targetPath): string {
739
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
740
-			throw new GenericFileException('Object store does not support multipart upload');
741
-		}
742
-		$cacheEntry = $this->getCache()->get($targetPath);
743
-		$urn = $this->getURN($cacheEntry->getId());
744
-		return $this->objectStore->initiateMultipartUpload($urn);
745
-	}
746
-
747
-	/**
748
-	 * @throws GenericFileException
749
-	 */
750
-	public function putChunkedWritePart(
751
-		string $targetPath,
752
-		string $writeToken,
753
-		string $chunkId,
754
-		$data,
755
-		$size = null,
756
-	): ?array {
757
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
758
-			throw new GenericFileException('Object store does not support multipart upload');
759
-		}
760
-		$cacheEntry = $this->getCache()->get($targetPath);
761
-		$urn = $this->getURN($cacheEntry->getId());
762
-
763
-		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
764
-
765
-		$parts[$chunkId] = [
766
-			'PartNumber' => $chunkId,
767
-			'ETag' => trim($result->get('ETag'), '"'),
768
-		];
769
-		return $parts[$chunkId];
770
-	}
771
-
772
-	public function completeChunkedWrite(string $targetPath, string $writeToken): int {
773
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
774
-			throw new GenericFileException('Object store does not support multipart upload');
775
-		}
776
-		$cacheEntry = $this->getCache()->get($targetPath);
777
-		$urn = $this->getURN($cacheEntry->getId());
778
-		$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
779
-		$sortedParts = array_values($parts);
780
-		sort($sortedParts);
781
-		try {
782
-			$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
783
-			$stat = $this->stat($targetPath);
784
-			$mtime = time();
785
-			if (is_array($stat)) {
786
-				$stat['size'] = $size;
787
-				$stat['mtime'] = $mtime;
788
-				$stat['mimetype'] = $this->getMimeType($targetPath);
789
-				$this->getCache()->update($stat['fileid'], $stat);
790
-			}
791
-		} catch (S3MultipartUploadException|S3Exception $e) {
792
-			$this->objectStore->abortMultipartUpload($urn, $writeToken);
793
-			$this->logger->error(
794
-				'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
795
-				[
796
-					'app' => 'objectstore',
797
-					'exception' => $e,
798
-				]
799
-			);
800
-			throw new GenericFileException('Could not write chunked file');
801
-		}
802
-		return $size;
803
-	}
804
-
805
-	public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
806
-		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
807
-			throw new GenericFileException('Object store does not support multipart upload');
808
-		}
809
-		$cacheEntry = $this->getCache()->get($targetPath);
810
-		$urn = $this->getURN($cacheEntry->getId());
811
-		$this->objectStore->abortMultipartUpload($urn, $writeToken);
812
-	}
813
-
814
-	public function setPreserveCacheOnDelete(bool $preserve) {
815
-		$this->preserveCacheItemsOnDelete = $preserve;
816
-	}
531
+                $this->getCache()->remove($uploadPath);
532
+                $this->logger->error(
533
+                    'Could not create object ' . $urn . ' for ' . $path,
534
+                    [
535
+                        'app' => 'objectstore',
536
+                        'exception' => $ex,
537
+                    ]
538
+                );
539
+            } else {
540
+                $this->logger->error(
541
+                    'Could not update object ' . $urn . ' for ' . $path,
542
+                    [
543
+                        'app' => 'objectstore',
544
+                        'exception' => $ex,
545
+                    ]
546
+                );
547
+            }
548
+            throw $ex; // make this bubble up
549
+        }
550
+
551
+        if ($exists) {
552
+            // Always update the unencrypted size, for encryption the Encryption wrapper will update this afterwards anyways
553
+            $stat['unencrypted_size'] = $stat['size'];
554
+            $this->getCache()->update($fileId, $stat);
555
+        } else {
556
+            if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
557
+                $this->getCache()->move($uploadPath, $path);
558
+            } else {
559
+                $this->getCache()->remove($uploadPath);
560
+                throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
561
+            }
562
+        }
563
+
564
+        return $size;
565
+    }
566
+
567
+    public function getObjectStore(): IObjectStore {
568
+        return $this->objectStore;
569
+    }
570
+
571
+    public function copyFromStorage(
572
+        IStorage $sourceStorage,
573
+        string $sourceInternalPath,
574
+        string $targetInternalPath,
575
+        bool $preserveMtime = false,
576
+    ): bool {
577
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
578
+            /** @var ObjectStoreStorage $sourceStorage */
579
+            if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
580
+                /** @var CacheEntry $sourceEntry */
581
+                $sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
582
+                $sourceEntryData = $sourceEntry->getData();
583
+                // $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
584
+                // user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
585
+                // unjailed storage.
586
+                if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
587
+                    $sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
588
+                }
589
+                $this->copyInner($sourceStorage->getCache(), $sourceEntry, $targetInternalPath);
590
+                return true;
591
+            }
592
+        }
593
+
594
+        return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
595
+    }
596
+
597
+    public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, ?ICacheEntry $sourceCacheEntry = null): bool {
598
+        $sourceCache = $sourceStorage->getCache();
599
+        if (
600
+            $sourceStorage->instanceOfStorage(ObjectStoreStorage::class) &&
601
+            $sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()
602
+        ) {
603
+            if ($this->getCache()->get($targetInternalPath)) {
604
+                $this->unlink($targetInternalPath);
605
+                $this->getCache()->remove($targetInternalPath);
606
+            }
607
+            $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
608
+            // Do not import any data when source and target bucket are identical.
609
+            return true;
610
+        }
611
+        if (!$sourceCacheEntry) {
612
+            $sourceCacheEntry = $sourceCache->get($sourceInternalPath);
613
+        }
614
+
615
+        $this->copyObjects($sourceStorage, $sourceCache, $sourceCacheEntry);
616
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
617
+            /** @var ObjectStoreStorage $sourceStorage */
618
+            $sourceStorage->setPreserveCacheOnDelete(true);
619
+        }
620
+        if ($sourceCacheEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
621
+            $sourceStorage->rmdir($sourceInternalPath);
622
+        } else {
623
+            $sourceStorage->unlink($sourceInternalPath);
624
+        }
625
+        if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
626
+            /** @var ObjectStoreStorage $sourceStorage */
627
+            $sourceStorage->setPreserveCacheOnDelete(false);
628
+        }
629
+        if ($this->getCache()->get($targetInternalPath)) {
630
+            $this->unlink($targetInternalPath);
631
+            $this->getCache()->remove($targetInternalPath);
632
+        }
633
+        $this->getCache()->moveFromCache($sourceCache, $sourceInternalPath, $targetInternalPath);
634
+
635
+        return true;
636
+    }
637
+
638
+    /**
639
+     * Copy the object(s) of a file or folder into this storage, without touching the cache
640
+     */
641
+    private function copyObjects(IStorage $sourceStorage, ICache $sourceCache, ICacheEntry $sourceCacheEntry) {
642
+        $copiedFiles = [];
643
+        try {
644
+            foreach ($this->getAllChildObjects($sourceCache, $sourceCacheEntry) as $file) {
645
+                $sourceStream = $sourceStorage->fopen($file->getPath(), 'r');
646
+                if (!$sourceStream) {
647
+                    throw new \Exception("Failed to open source file {$file->getPath()} ({$file->getId()})");
648
+                }
649
+                $this->objectStore->writeObject($this->getURN($file->getId()), $sourceStream, $file->getMimeType());
650
+                if (is_resource($sourceStream)) {
651
+                    fclose($sourceStream);
652
+                }
653
+                $copiedFiles[] = $file->getId();
654
+            }
655
+        } catch (\Exception $e) {
656
+            foreach ($copiedFiles as $fileId) {
657
+                try {
658
+                    $this->objectStore->deleteObject($this->getURN($fileId));
659
+                } catch (\Exception $e) {
660
+                    // ignore
661
+                }
662
+            }
663
+            throw $e;
664
+        }
665
+    }
666
+
667
+    /**
668
+     * @return \Iterator<ICacheEntry>
669
+     */
670
+    private function getAllChildObjects(ICache $cache, ICacheEntry $entry): \Iterator {
671
+        if ($entry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
672
+            foreach ($cache->getFolderContentsById($entry->getId()) as $child) {
673
+                yield from $this->getAllChildObjects($cache, $child);
674
+            }
675
+        } else {
676
+            yield $entry;
677
+        }
678
+    }
679
+
680
+    public function copy(string $source, string $target): bool {
681
+        $source = $this->normalizePath($source);
682
+        $target = $this->normalizePath($target);
683
+
684
+        $cache = $this->getCache();
685
+        $sourceEntry = $cache->get($source);
686
+        if (!$sourceEntry) {
687
+            throw new NotFoundException('Source object not found');
688
+        }
689
+
690
+        $this->copyInner($cache, $sourceEntry, $target);
691
+
692
+        return true;
693
+    }
694
+
695
+    private function copyInner(ICache $sourceCache, ICacheEntry $sourceEntry, string $to) {
696
+        $cache = $this->getCache();
697
+
698
+        if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
699
+            if ($cache->inCache($to)) {
700
+                $cache->remove($to);
701
+            }
702
+            $this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
703
+
704
+            foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
705
+                $this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
706
+            }
707
+        } else {
708
+            $this->copyFile($sourceEntry, $to);
709
+        }
710
+    }
711
+
712
+    private function copyFile(ICacheEntry $sourceEntry, string $to) {
713
+        $cache = $this->getCache();
714
+
715
+        $sourceUrn = $this->getURN($sourceEntry->getId());
716
+
717
+        if (!$cache instanceof Cache) {
718
+            throw new \Exception('Invalid source cache for object store copy');
719
+        }
720
+
721
+        $targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
722
+
723
+        $targetUrn = $this->getURN($targetId);
724
+
725
+        try {
726
+            $this->objectStore->copyObject($sourceUrn, $targetUrn);
727
+            if ($this->handleCopiesAsOwned) {
728
+                // 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 !
729
+                $cache->update($targetId, ['permissions' => \OCP\Constants::PERMISSION_ALL]);
730
+            }
731
+        } catch (\Exception $e) {
732
+            $cache->remove($to);
733
+
734
+            throw $e;
735
+        }
736
+    }
737
+
738
+    public function startChunkedWrite(string $targetPath): string {
739
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
740
+            throw new GenericFileException('Object store does not support multipart upload');
741
+        }
742
+        $cacheEntry = $this->getCache()->get($targetPath);
743
+        $urn = $this->getURN($cacheEntry->getId());
744
+        return $this->objectStore->initiateMultipartUpload($urn);
745
+    }
746
+
747
+    /**
748
+     * @throws GenericFileException
749
+     */
750
+    public function putChunkedWritePart(
751
+        string $targetPath,
752
+        string $writeToken,
753
+        string $chunkId,
754
+        $data,
755
+        $size = null,
756
+    ): ?array {
757
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
758
+            throw new GenericFileException('Object store does not support multipart upload');
759
+        }
760
+        $cacheEntry = $this->getCache()->get($targetPath);
761
+        $urn = $this->getURN($cacheEntry->getId());
762
+
763
+        $result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
764
+
765
+        $parts[$chunkId] = [
766
+            'PartNumber' => $chunkId,
767
+            'ETag' => trim($result->get('ETag'), '"'),
768
+        ];
769
+        return $parts[$chunkId];
770
+    }
771
+
772
+    public function completeChunkedWrite(string $targetPath, string $writeToken): int {
773
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
774
+            throw new GenericFileException('Object store does not support multipart upload');
775
+        }
776
+        $cacheEntry = $this->getCache()->get($targetPath);
777
+        $urn = $this->getURN($cacheEntry->getId());
778
+        $parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
779
+        $sortedParts = array_values($parts);
780
+        sort($sortedParts);
781
+        try {
782
+            $size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
783
+            $stat = $this->stat($targetPath);
784
+            $mtime = time();
785
+            if (is_array($stat)) {
786
+                $stat['size'] = $size;
787
+                $stat['mtime'] = $mtime;
788
+                $stat['mimetype'] = $this->getMimeType($targetPath);
789
+                $this->getCache()->update($stat['fileid'], $stat);
790
+            }
791
+        } catch (S3MultipartUploadException|S3Exception $e) {
792
+            $this->objectStore->abortMultipartUpload($urn, $writeToken);
793
+            $this->logger->error(
794
+                'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
795
+                [
796
+                    'app' => 'objectstore',
797
+                    'exception' => $e,
798
+                ]
799
+            );
800
+            throw new GenericFileException('Could not write chunked file');
801
+        }
802
+        return $size;
803
+    }
804
+
805
+    public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
806
+        if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
807
+            throw new GenericFileException('Object store does not support multipart upload');
808
+        }
809
+        $cacheEntry = $this->getCache()->get($targetPath);
810
+        $urn = $this->getURN($cacheEntry->getId());
811
+        $this->objectStore->abortMultipartUpload($urn, $writeToken);
812
+    }
813
+
814
+    public function setPreserveCacheOnDelete(bool $preserve) {
815
+        $this->preserveCacheItemsOnDelete = $preserve;
816
+    }
817 817
 }
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
 	}
@@ -415,7 +415,7 @@  discard block
 block discarded – undo
415 415
 				$this->file_put_contents($path, ' ');
416 416
 			} catch (\Exception $ex) {
417 417
 				$this->logger->error(
418
-					'Could not create object for ' . $path,
418
+					'Could not create object for '.$path,
419 419
 					[
420 420
 						'app' => 'objectstore',
421 421
 						'exception' => $ex,
@@ -464,7 +464,7 @@  discard block
 block discarded – undo
464 464
 		}
465 465
 		// update stat with new data
466 466
 		$mTime = time();
467
-		$stat['size'] = (int)$size;
467
+		$stat['size'] = (int) $size;
468 468
 		$stat['mtime'] = $mTime;
469 469
 		$stat['storage_mtime'] = $mTime;
470 470
 
@@ -481,7 +481,7 @@  discard block
 block discarded – undo
481 481
 		$stat['checksum'] = '';
482 482
 
483 483
 		$exists = $this->getCache()->inCache($path);
484
-		$uploadPath = $exists ? $path : $path . '.part';
484
+		$uploadPath = $exists ? $path : $path.'.part';
485 485
 
486 486
 		if ($exists) {
487 487
 			$fileId = $stat['fileid'];
@@ -497,7 +497,7 @@  discard block
 block discarded – undo
497 497
 		try {
498 498
 			//upload to object storage
499 499
 			if ($size === null) {
500
-				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
500
+				$countStream = CountWrapper::wrap($stream, function($writtenSize) use ($fileId, &$size) {
501 501
 					$this->getCache()->update($fileId, [
502 502
 						'size' => $writtenSize,
503 503
 					]);
@@ -530,7 +530,7 @@  discard block
 block discarded – undo
530 530
 				 */
531 531
 				$this->getCache()->remove($uploadPath);
532 532
 				$this->logger->error(
533
-					'Could not create object ' . $urn . ' for ' . $path,
533
+					'Could not create object '.$urn.' for '.$path,
534 534
 					[
535 535
 						'app' => 'objectstore',
536 536
 						'exception' => $ex,
@@ -538,7 +538,7 @@  discard block
 block discarded – undo
538 538
 				);
539 539
 			} else {
540 540
 				$this->logger->error(
541
-					'Could not update object ' . $urn . ' for ' . $path,
541
+					'Could not update object '.$urn.' for '.$path,
542 542
 					[
543 543
 						'app' => 'objectstore',
544 544
 						'exception' => $ex,
@@ -702,7 +702,7 @@  discard block
 block discarded – undo
702 702
 			$this->mkdir($to, false, ['size' => $sourceEntry->getSize()]);
703 703
 
704 704
 			foreach ($sourceCache->getFolderContentsById($sourceEntry->getId()) as $child) {
705
-				$this->copyInner($sourceCache, $child, $to . '/' . $child->getName());
705
+				$this->copyInner($sourceCache, $child, $to.'/'.$child->getName());
706 706
 			}
707 707
 		} else {
708 708
 			$this->copyFile($sourceEntry, $to);
@@ -760,7 +760,7 @@  discard block
 block discarded – undo
760 760
 		$cacheEntry = $this->getCache()->get($targetPath);
761 761
 		$urn = $this->getURN($cacheEntry->getId());
762 762
 
763
-		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
763
+		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int) $chunkId, $data, $size);
764 764
 
765 765
 		$parts[$chunkId] = [
766 766
 			'PartNumber' => $chunkId,
@@ -788,10 +788,10 @@  discard block
 block discarded – undo
788 788
 				$stat['mimetype'] = $this->getMimeType($targetPath);
789 789
 				$this->getCache()->update($stat['fileid'], $stat);
790 790
 			}
791
-		} catch (S3MultipartUploadException|S3Exception $e) {
791
+		} catch (S3MultipartUploadException | S3Exception $e) {
792 792
 			$this->objectStore->abortMultipartUpload($urn, $writeToken);
793 793
 			$this->logger->error(
794
-				'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
794
+				'Could not compete multipart upload '.$urn.' with uploadId '.$writeToken,
795 795
 				[
796 796
 					'app' => 'objectstore',
797 797
 					'exception' => $e,
Please login to merge, or discard this patch.