Passed
Push — master ( b3f59a...eecd46 )
by Julius
16:28 queued 12s
created

ObjectStoreStorage::rmObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 15
rs 9.9332
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Marcel Klehr <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Tigran Mkrtchyan <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\Files\ObjectStore;
32
33
use Aws\S3\Exception\S3Exception;
34
use Aws\S3\Exception\S3MultipartUploadException;
35
use Icewind\Streams\CallbackWrapper;
36
use Icewind\Streams\CountWrapper;
37
use Icewind\Streams\IteratorDirectory;
38
use OC\Files\Cache\Cache;
39
use OC\Files\Cache\CacheEntry;
40
use OC\Files\Storage\PolyFill\CopyDirectory;
41
use OCP\Files\Cache\ICacheEntry;
42
use OCP\Files\FileInfo;
43
use OCP\Files\GenericFileException;
44
use OCP\Files\NotFoundException;
45
use OCP\Files\ObjectStore\IObjectStore;
46
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
47
use OCP\Files\Storage\IChunkedFileWrite;
48
use OCP\Files\Storage\IStorage;
49
50
class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
51
	use CopyDirectory;
52
53
	/**
54
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
55
	 */
56
	protected $objectStore;
57
	/**
58
	 * @var string $id
59
	 */
60
	protected $id;
61
	/**
62
	 * @var \OC\User\User $user
63
	 */
64
	protected $user;
65
66
	private $objectPrefix = 'urn:oid:';
67
68
	private $logger;
69
70
	/** @var bool */
71
	protected $validateWrites = true;
72
73
	public function __construct($params) {
74
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
75
			$this->objectStore = $params['objectstore'];
76
		} else {
77
			throw new \Exception('missing IObjectStore instance');
78
		}
79
		if (isset($params['storageid'])) {
80
			$this->id = 'object::store:' . $params['storageid'];
81
		} else {
82
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
83
		}
84
		if (isset($params['objectPrefix'])) {
85
			$this->objectPrefix = $params['objectPrefix'];
86
		}
87
		if (isset($params['validateWrites'])) {
88
			$this->validateWrites = (bool)$params['validateWrites'];
89
		}
90
		//initialize cache with root directory in cache
91
		if (!$this->is_dir('/')) {
92
			$this->mkdir('/');
93
		}
94
95
		$this->logger = \OC::$server->getLogger();
96
	}
97
98
	public function mkdir($path) {
99
		$path = $this->normalizePath($path);
100
		if ($this->file_exists($path)) {
101
			return false;
102
		}
103
104
		$mTime = time();
105
		$data = [
106
			'mimetype' => 'httpd/unix-directory',
107
			'size' => 0,
108
			'mtime' => $mTime,
109
			'storage_mtime' => $mTime,
110
			'permissions' => \OCP\Constants::PERMISSION_ALL,
111
		];
112
		if ($path === '') {
113
			//create root on the fly
114
			$data['etag'] = $this->getETag('');
115
			$this->getCache()->put('', $data);
116
			return true;
117
		} else {
118
			// if parent does not exist, create it
119
			$parent = $this->normalizePath(dirname($path));
120
			$parentType = $this->filetype($parent);
121
			if ($parentType === false) {
122
				if (!$this->mkdir($parent)) {
123
					// something went wrong
124
					return false;
125
				}
126
			} elseif ($parentType === 'file') {
127
				// parent is a file
128
				return false;
129
			}
130
			// finally create the new dir
131
			$mTime = time(); // update mtime
132
			$data['mtime'] = $mTime;
133
			$data['storage_mtime'] = $mTime;
134
			$data['etag'] = $this->getETag($path);
135
			$this->getCache()->put($path, $data);
136
			return true;
137
		}
138
	}
139
140
	/**
141
	 * @param string $path
142
	 * @return string
143
	 */
144
	private function normalizePath($path) {
145
		$path = trim($path, '/');
146
		//FIXME why do we sometimes get a path like 'files//username'?
147
		$path = str_replace('//', '/', $path);
148
149
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
150
		if (!$path || $path === '.') {
151
			$path = '';
152
		}
153
154
		return $path;
155
	}
156
157
	/**
158
	 * Object Stores use a NoopScanner because metadata is directly stored in
159
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
160
	 *
161
	 * @param string $path
162
	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
0 ignored issues
show
Bug introduced by
The type OC\Files\ObjectStore\optional was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
163
	 * @return \OC\Files\ObjectStore\NoopScanner
164
	 */
165
	public function getScanner($path = '', $storage = null) {
166
		if (!$storage) {
167
			$storage = $this;
168
		}
169
		if (!isset($this->scanner)) {
170
			$this->scanner = new NoopScanner($storage);
171
		}
172
		return $this->scanner;
173
	}
174
175
	public function getId() {
176
		return $this->id;
177
	}
178
179
	public function rmdir($path) {
180
		$path = $this->normalizePath($path);
181
		$entry = $this->getCache()->get($path);
182
183
		if (!$entry || $entry->getMimeType() !== ICacheEntry::DIRECTORY_MIMETYPE) {
184
			return false;
185
		}
186
187
		return $this->rmObjects($entry);
188
	}
189
190
	private function rmObjects(ICacheEntry $entry): bool {
191
		$children = $this->getCache()->getFolderContentsById($entry->getId());
192
		foreach ($children as $child) {
193
			if ($child->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
194
				if (!$this->rmObjects($child)) {
195
					return false;
196
				}
197
			} else {
198
				if (!$this->rmObject($child)) {
199
					return false;
200
				}
201
			}
202
		}
203
204
		$this->getCache()->remove($entry->getPath());
205
206
		return true;
207
	}
208
209
	public function unlink($path) {
210
		$path = $this->normalizePath($path);
211
		$entry = $this->getCache()->get($path);
212
213
		if ($entry instanceof ICacheEntry) {
214
			if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
215
				return $this->rmObjects($entry);
216
			} else {
217
				return $this->rmObject($entry);
218
			}
219
		}
220
		return false;
221
	}
222
223
	public function rmObject(ICacheEntry $entry): bool {
224
		try {
225
			$this->objectStore->deleteObject($this->getURN($entry->getId()));
226
		} catch (\Exception $ex) {
227
			if ($ex->getCode() !== 404) {
228
				$this->logger->logException($ex, [
229
					'app' => 'objectstore',
230
					'message' => 'Could not delete object ' . $this->getURN($entry->getId()) . ' for ' . $entry->getPath(),
231
				]);
232
				return false;
233
			}
234
			//removing from cache is ok as it does not exist in the objectstore anyway
235
		}
236
		$this->getCache()->remove($entry->getPath());
237
		return true;
238
	}
239
240
	public function stat($path) {
241
		$path = $this->normalizePath($path);
242
		$cacheEntry = $this->getCache()->get($path);
243
		if ($cacheEntry instanceof CacheEntry) {
244
			return $cacheEntry->getData();
245
		} else {
246
			return false;
247
		}
248
	}
249
250
	public function getPermissions($path) {
251
		$stat = $this->stat($path);
252
253
		if (is_array($stat) && isset($stat['permissions'])) {
254
			return $stat['permissions'];
255
		}
256
257
		return parent::getPermissions($path);
258
	}
259
260
	/**
261
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
262
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
263
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
264
	 *
265
	 * @param int $fileId the fileid
266
	 * @return null|string the unified resource name used to identify the object
267
	 */
268
	public function getURN($fileId) {
269
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
270
			return $this->objectPrefix . $fileId;
271
		}
272
		return null;
273
	}
274
275
	public function opendir($path) {
276
		$path = $this->normalizePath($path);
277
278
		try {
279
			$files = [];
280
			$folderContents = $this->getCache()->getFolderContents($path);
281
			foreach ($folderContents as $file) {
282
				$files[] = $file['name'];
283
			}
284
285
			return IteratorDirectory::wrap($files);
286
		} catch (\Exception $e) {
287
			$this->logger->logException($e);
288
			return false;
289
		}
290
	}
291
292
	public function filetype($path) {
293
		$path = $this->normalizePath($path);
294
		$stat = $this->stat($path);
295
		if ($stat) {
296
			if ($stat['mimetype'] === 'httpd/unix-directory') {
297
				return 'dir';
298
			}
299
			return 'file';
300
		} else {
301
			return false;
302
		}
303
	}
304
305
	public function fopen($path, $mode) {
306
		$path = $this->normalizePath($path);
307
308
		if (strrpos($path, '.') !== false) {
309
			$ext = substr($path, strrpos($path, '.'));
310
		} else {
311
			$ext = '';
312
		}
313
314
		switch ($mode) {
315
			case 'r':
316
			case 'rb':
317
				$stat = $this->stat($path);
318
				if (is_array($stat)) {
319
					$filesize = $stat['size'] ?? 0;
320
					// Reading 0 sized files is a waste of time
321
					if ($filesize === 0) {
322
						return fopen('php://memory', $mode);
323
					}
324
325
					try {
326
						$handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
327
						if ($handle === false) {
0 ignored issues
show
introduced by
The condition $handle === false is always false.
Loading history...
328
							return false; // keep backward compatibility
329
						}
330
						$streamStat = fstat($handle);
331
						$actualSize = $streamStat['size'] ?? -1;
332
						if ($actualSize > -1 && $actualSize !== $filesize) {
333
							$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
334
						}
335
						return $handle;
336
					} catch (NotFoundException $e) {
337
						$this->logger->logException($e, [
338
							'app' => 'objectstore',
339
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
340
						]);
341
						throw $e;
342
					} catch (\Exception $ex) {
343
						$this->logger->logException($ex, [
344
							'app' => 'objectstore',
345
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
346
						]);
347
						return false;
348
					}
349
				} else {
350
					return false;
351
				}
352
				// no break
353
			case 'w':
354
			case 'wb':
355
			case 'w+':
356
			case 'wb+':
357
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
358
				$handle = fopen($tmpFile, $mode);
359
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
360
					$this->writeBack($tmpFile, $path);
361
					unlink($tmpFile);
362
				});
363
			case 'a':
364
			case 'ab':
365
			case 'r+':
366
			case 'a+':
367
			case 'x':
368
			case 'x+':
369
			case 'c':
370
			case 'c+':
371
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
372
				if ($this->file_exists($path)) {
373
					$source = $this->fopen($path, 'r');
374
					file_put_contents($tmpFile, $source);
375
				}
376
				$handle = fopen($tmpFile, $mode);
377
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
378
					$this->writeBack($tmpFile, $path);
379
					unlink($tmpFile);
380
				});
381
		}
382
		return false;
383
	}
384
385
	public function file_exists($path) {
386
		$path = $this->normalizePath($path);
387
		return (bool)$this->stat($path);
388
	}
389
390
	public function rename($source, $target) {
391
		$source = $this->normalizePath($source);
392
		$target = $this->normalizePath($target);
393
		$this->remove($target);
394
		$this->getCache()->move($source, $target);
395
		$this->touch(dirname($target));
396
		return true;
397
	}
398
399
	public function getMimeType($path) {
400
		$path = $this->normalizePath($path);
401
		return parent::getMimeType($path);
402
	}
403
404
	public function touch($path, $mtime = null) {
405
		if (is_null($mtime)) {
406
			$mtime = time();
407
		}
408
409
		$path = $this->normalizePath($path);
410
		$dirName = dirname($path);
411
		$parentExists = $this->is_dir($dirName);
412
		if (!$parentExists) {
413
			return false;
414
		}
415
416
		$stat = $this->stat($path);
417
		if (is_array($stat)) {
418
			// update existing mtime in db
419
			$stat['mtime'] = $mtime;
420
			$this->getCache()->update($stat['fileid'], $stat);
421
		} else {
422
			try {
423
				//create a empty file, need to have at least on char to make it
424
				// work with all object storage implementations
425
				$this->file_put_contents($path, ' ');
426
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
427
				$stat = [
428
					'etag' => $this->getETag($path),
429
					'mimetype' => $mimeType,
430
					'size' => 0,
431
					'mtime' => $mtime,
432
					'storage_mtime' => $mtime,
433
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
434
				];
435
				$this->getCache()->put($path, $stat);
436
			} catch (\Exception $ex) {
437
				$this->logger->logException($ex, [
438
					'app' => 'objectstore',
439
					'message' => 'Could not create object for ' . $path,
440
				]);
441
				throw $ex;
442
			}
443
		}
444
		return true;
445
	}
446
447
	public function writeBack($tmpFile, $path) {
448
		$size = filesize($tmpFile);
449
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
450
	}
451
452
	/**
453
	 * external changes are not supported, exclusive access to the object storage is assumed
454
	 *
455
	 * @param string $path
456
	 * @param int $time
457
	 * @return false
458
	 */
459
	public function hasUpdated($path, $time) {
460
		return false;
461
	}
462
463
	public function needsPartFile() {
464
		return false;
465
	}
466
467
	public function file_put_contents($path, $data) {
468
		$handle = $this->fopen($path, 'w+');
469
		$result = fwrite($handle, $data);
470
		fclose($handle);
471
		return $result;
472
	}
473
474
	public function writeStream(string $path, $stream, int $size = null): int {
475
		$stat = $this->stat($path);
476
		if (empty($stat)) {
477
			// create new file
478
			$stat = [
479
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
480
			];
481
		}
482
		// update stat with new data
483
		$mTime = time();
484
		$stat['size'] = (int)$size;
485
		$stat['mtime'] = $mTime;
486
		$stat['storage_mtime'] = $mTime;
487
488
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
489
		$mimetype = $mimetypeDetector->detectPath($path);
490
491
		$stat['mimetype'] = $mimetype;
492
		$stat['etag'] = $this->getETag($path);
493
		$stat['checksum'] = '';
494
495
		$exists = $this->getCache()->inCache($path);
496
		$uploadPath = $exists ? $path : $path . '.part';
497
498
		if ($exists) {
499
			$fileId = $stat['fileid'];
500
		} else {
501
			$fileId = $this->getCache()->put($uploadPath, $stat);
502
		}
503
504
		$urn = $this->getURN($fileId);
505
		try {
506
			//upload to object storage
507
			if ($size === null) {
508
				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
509
					$this->getCache()->update($fileId, [
510
						'size' => $writtenSize,
511
					]);
512
					$size = $writtenSize;
513
				});
514
				$this->objectStore->writeObject($urn, $countStream, $mimetype);
515
				if (is_resource($countStream)) {
516
					fclose($countStream);
517
				}
518
				$stat['size'] = $size;
519
			} else {
520
				$this->objectStore->writeObject($urn, $stream, $mimetype);
521
				if (is_resource($stream)) {
522
					fclose($stream);
523
				}
524
			}
525
		} catch (\Exception $ex) {
526
			if (!$exists) {
527
				/*
528
				 * Only remove the entry if we are dealing with a new file.
529
				 * Else people lose access to existing files
530
				 */
531
				$this->getCache()->remove($uploadPath);
532
				$this->logger->logException($ex, [
533
					'app' => 'objectstore',
534
					'message' => 'Could not create object ' . $urn . ' for ' . $path,
535
				]);
536
			} else {
537
				$this->logger->logException($ex, [
538
					'app' => 'objectstore',
539
					'message' => 'Could not update object ' . $urn . ' for ' . $path,
540
				]);
541
			}
542
			throw $ex; // make this bubble up
543
		}
544
545
		if ($exists) {
546
			$this->getCache()->update($fileId, $stat);
547
		} else {
548
			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
549
				$this->getCache()->move($uploadPath, $path);
550
			} else {
551
				$this->getCache()->remove($uploadPath);
552
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
553
			}
554
		}
555
556
		return $size;
557
	}
558
559
	public function getObjectStore(): IObjectStore {
560
		return $this->objectStore;
561
	}
562
563
	public function copyFromStorage(
564
		IStorage $sourceStorage,
565
		$sourceInternalPath,
566
		$targetInternalPath,
567
		$preserveMtime = false
568
	) {
569
		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
570
			/** @var ObjectStoreStorage $sourceStorage */
571
			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
572
				/** @var CacheEntry $sourceEntry */
573
				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
574
				$sourceEntryData = $sourceEntry->getData();
575
				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
576
				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
577
				// unjailed storage.
578
				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
579
					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
580
				}
581
				$this->copyInner($sourceEntry, $targetInternalPath);
582
				return true;
583
			}
584
		}
585
586
		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
587
	}
588
589
	public function copy($source, $target) {
590
		$source = $this->normalizePath($source);
591
		$target = $this->normalizePath($target);
592
593
		$cache = $this->getCache();
594
		$sourceEntry = $cache->get($source);
595
		if (!$sourceEntry) {
596
			throw new NotFoundException('Source object not found');
597
		}
598
599
		$this->copyInner($sourceEntry, $target);
600
601
		return true;
602
	}
603
604
	private function copyInner(ICacheEntry $sourceEntry, string $to) {
605
		$cache = $this->getCache();
606
607
		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
608
			if ($cache->inCache($to)) {
609
				$cache->remove($to);
610
			}
611
			$this->mkdir($to);
612
613
			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
614
				$this->copyInner($child, $to . '/' . $child->getName());
615
			}
616
		} else {
617
			$this->copyFile($sourceEntry, $to);
618
		}
619
	}
620
621
	private function copyFile(ICacheEntry $sourceEntry, string $to) {
622
		$cache = $this->getCache();
623
624
		$sourceUrn = $this->getURN($sourceEntry->getId());
625
626
		if (!$cache instanceof Cache) {
0 ignored issues
show
introduced by
$cache is always a sub-type of OC\Files\Cache\Cache.
Loading history...
627
			throw new \Exception("Invalid source cache for object store copy");
628
		}
629
630
		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
631
632
		$targetUrn = $this->getURN($targetId);
633
634
		try {
635
			$this->objectStore->copyObject($sourceUrn, $targetUrn);
636
		} catch (\Exception $e) {
637
			$cache->remove($to);
638
639
			throw $e;
640
		}
641
	}
642
643
	public function startChunkedWrite(string $targetPath): string {
644
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
645
			throw new GenericFileException('Object store does not support multipart upload');
646
		}
647
		$cacheEntry = $this->getCache()->get($targetPath);
648
		$urn = $this->getURN($cacheEntry->getId());
649
		return $this->objectStore->initiateMultipartUpload($urn);
650
	}
651
652
	/**
653
	 *
654
	 * @throws GenericFileException
655
	 */
656
	public function putChunkedWritePart(
657
		string $targetPath,
658
		string $writeToken,
659
		string $chunkId,
660
		$data,
661
		$size = null
662
	): ?array {
663
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
664
			throw new GenericFileException('Object store does not support multipart upload');
665
		}
666
		$cacheEntry = $this->getCache()->get($targetPath);
667
		$urn = $this->getURN($cacheEntry->getId());
668
669
		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
670
671
		$parts[$chunkId] = [
0 ignored issues
show
Comprehensibility Best Practice introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = array(); before regardless.
Loading history...
672
			'PartNumber' => $chunkId,
673
			'ETag' => trim($result->get('ETag'), '"'),
674
		];
675
		return $parts[$chunkId];
676
	}
677
678
	public function completeChunkedWrite(string $targetPath, string $writeToken): int {
679
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
680
			throw new GenericFileException('Object store does not support multipart upload');
681
		}
682
		$cacheEntry = $this->getCache()->get($targetPath);
683
		$urn = $this->getURN($cacheEntry->getId());
684
		$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
685
		$sortedParts = array_values($parts);
686
		sort($sortedParts);
687
		try {
688
			$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
689
			$stat = $this->stat($targetPath);
690
			$mtime = time();
691
			if (is_array($stat)) {
692
				$stat['size'] = $size;
693
				$stat['mtime'] = $mtime;
694
				$stat['mimetype'] = $this->getMimeType($targetPath);
695
				$this->getCache()->update($stat['fileid'], $stat);
696
			}
697
		} catch (S3MultipartUploadException|S3Exception $e) {
698
			$this->objectStore->abortMultipartUpload($urn, $writeToken);
699
			$this->logger->logException($e, [
700
				'app' => 'objectstore',
701
				'message' => 'Could not compete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
702
			]);
703
			throw new GenericFileException('Could not write chunked file');
704
		}
705
		return $size;
706
	}
707
708
	public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
709
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
710
			throw new GenericFileException('Object store does not support multipart upload');
711
		}
712
		$cacheEntry = $this->getCache()->get($targetPath);
713
		$urn = $this->getURN($cacheEntry->getId());
714
		$this->objectStore->abortMultipartUpload($urn, $writeToken);
715
	}
716
}
717