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