Passed
Push — master ( 691aa8...315510 )
by Blizzz
17:05 queued 13s
created

ObjectStoreStorage::completeChunkedWrite()   A

Complexity

Conditions 4
Paths 11

Size

Total Lines 28
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 23
c 0
b 0
f 0
nc 11
nop 2
dl 0
loc 28
rs 9.552
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
namespace OC\Files\ObjectStore;
31
32
use Aws\S3\Exception\S3Exception;
33
use Aws\S3\Exception\S3MultipartUploadException;
34
use Icewind\Streams\CallbackWrapper;
35
use Icewind\Streams\CountWrapper;
36
use Icewind\Streams\IteratorDirectory;
37
use OC\Files\Cache\Cache;
38
use OC\Files\Cache\CacheEntry;
39
use OC\Files\Storage\PolyFill\CopyDirectory;
40
use OCP\Files\Cache\ICacheEntry;
41
use OCP\Files\FileInfo;
42
use OCP\Files\GenericFileException;
43
use OCP\Files\NotFoundException;
44
use OCP\Files\ObjectStore\IObjectStore;
45
use OCP\Files\ObjectStore\IObjectStoreMultiPartUpload;
46
use OCP\Files\Storage\IChunkedFileWrite;
47
use OCP\Files\Storage\IStorage;
48
49
class ObjectStoreStorage extends \OC\Files\Storage\Common implements IChunkedFileWrite {
50
	use CopyDirectory;
51
52
	/**
53
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
54
	 */
55
	protected $objectStore;
56
	/**
57
	 * @var string $id
58
	 */
59
	protected $id;
60
	/**
61
	 * @var \OC\User\User $user
62
	 */
63
	protected $user;
64
65
	private $objectPrefix = 'urn:oid:';
66
67
	private $logger;
68
69
	/** @var bool */
70
	protected $validateWrites = true;
71
72
	public function __construct($params) {
73
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
74
			$this->objectStore = $params['objectstore'];
75
		} else {
76
			throw new \Exception('missing IObjectStore instance');
77
		}
78
		if (isset($params['storageid'])) {
79
			$this->id = 'object::store:' . $params['storageid'];
80
		} else {
81
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
82
		}
83
		if (isset($params['objectPrefix'])) {
84
			$this->objectPrefix = $params['objectPrefix'];
85
		}
86
		if (isset($params['validateWrites'])) {
87
			$this->validateWrites = (bool)$params['validateWrites'];
88
		}
89
		//initialize cache with root directory in cache
90
		if (!$this->is_dir('/')) {
91
			$this->mkdir('/');
92
		}
93
94
		$this->logger = \OC::$server->getLogger();
95
	}
96
97
	public function mkdir($path) {
98
		$path = $this->normalizePath($path);
99
		if ($this->file_exists($path)) {
100
			return false;
101
		}
102
103
		$mTime = time();
104
		$data = [
105
			'mimetype' => 'httpd/unix-directory',
106
			'size' => 0,
107
			'mtime' => $mTime,
108
			'storage_mtime' => $mTime,
109
			'permissions' => \OCP\Constants::PERMISSION_ALL,
110
		];
111
		if ($path === '') {
112
			//create root on the fly
113
			$data['etag'] = $this->getETag('');
114
			$this->getCache()->put('', $data);
115
			return true;
116
		} else {
117
			// if parent does not exist, create it
118
			$parent = $this->normalizePath(dirname($path));
119
			$parentType = $this->filetype($parent);
120
			if ($parentType === false) {
121
				if (!$this->mkdir($parent)) {
122
					// something went wrong
123
					return false;
124
				}
125
			} elseif ($parentType === 'file') {
126
				// parent is a file
127
				return false;
128
			}
129
			// finally create the new dir
130
			$mTime = time(); // update mtime
131
			$data['mtime'] = $mTime;
132
			$data['storage_mtime'] = $mTime;
133
			$data['etag'] = $this->getETag($path);
134
			$this->getCache()->put($path, $data);
135
			return true;
136
		}
137
	}
138
139
	/**
140
	 * @param string $path
141
	 * @return string
142
	 */
143
	private function normalizePath($path) {
144
		$path = trim($path, '/');
145
		//FIXME why do we sometimes get a path like 'files//username'?
146
		$path = str_replace('//', '/', $path);
147
148
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
149
		if (!$path || $path === '.') {
150
			$path = '';
151
		}
152
153
		return $path;
154
	}
155
156
	/**
157
	 * Object Stores use a NoopScanner because metadata is directly stored in
158
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
159
	 *
160
	 * @param string $path
161
	 * @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...
162
	 * @return \OC\Files\ObjectStore\NoopScanner
163
	 */
164
	public function getScanner($path = '', $storage = null) {
165
		if (!$storage) {
166
			$storage = $this;
167
		}
168
		if (!isset($this->scanner)) {
169
			$this->scanner = new NoopScanner($storage);
170
		}
171
		return $this->scanner;
172
	}
173
174
	public function getId() {
175
		return $this->id;
176
	}
177
178
	public function rmdir($path) {
179
		$path = $this->normalizePath($path);
180
181
		if (!$this->is_dir($path)) {
182
			return false;
183
		}
184
185
		if (!$this->rmObjects($path)) {
186
			return false;
187
		}
188
189
		$this->getCache()->remove($path);
190
191
		return true;
192
	}
193
194
	private function rmObjects($path) {
195
		$children = $this->getCache()->getFolderContents($path);
196
		foreach ($children as $child) {
197
			if ($child['mimetype'] === 'httpd/unix-directory') {
198
				if (!$this->rmObjects($child['path'])) {
199
					return false;
200
				}
201
			} else {
202
				if (!$this->unlink($child['path'])) {
203
					return false;
204
				}
205
			}
206
		}
207
208
		return true;
209
	}
210
211
	public function unlink($path) {
212
		$path = $this->normalizePath($path);
213
		$stat = $this->stat($path);
214
215
		if ($stat && isset($stat['fileid'])) {
216
			if ($stat['mimetype'] === 'httpd/unix-directory') {
217
				return $this->rmdir($path);
218
			}
219
			try {
220
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
221
			} catch (\Exception $ex) {
222
				if ($ex->getCode() !== 404) {
223
					$this->logger->logException($ex, [
224
						'app' => 'objectstore',
225
						'message' => 'Could not delete object ' . $this->getURN($stat['fileid']) . ' for ' . $path,
226
					]);
227
					return false;
228
				}
229
				//removing from cache is ok as it does not exist in the objectstore anyway
230
			}
231
			$this->getCache()->remove($path);
232
			return true;
233
		}
234
		return false;
235
	}
236
237
	public function stat($path) {
238
		$path = $this->normalizePath($path);
239
		$cacheEntry = $this->getCache()->get($path);
240
		if ($cacheEntry instanceof CacheEntry) {
241
			return $cacheEntry->getData();
242
		} else {
243
			return false;
244
		}
245
	}
246
247
	public function getPermissions($path) {
248
		$stat = $this->stat($path);
249
250
		if (is_array($stat) && isset($stat['permissions'])) {
251
			return $stat['permissions'];
252
		}
253
254
		return parent::getPermissions($path);
255
	}
256
257
	/**
258
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
259
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
260
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
261
	 *
262
	 * @param int $fileId the fileid
263
	 * @return null|string the unified resource name used to identify the object
264
	 */
265
	public function getURN($fileId) {
266
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
267
			return $this->objectPrefix . $fileId;
268
		}
269
		return null;
270
	}
271
272
	public function opendir($path) {
273
		$path = $this->normalizePath($path);
274
275
		try {
276
			$files = [];
277
			$folderContents = $this->getCache()->getFolderContents($path);
278
			foreach ($folderContents as $file) {
279
				$files[] = $file['name'];
280
			}
281
282
			return IteratorDirectory::wrap($files);
283
		} catch (\Exception $e) {
284
			$this->logger->logException($e);
285
			return false;
286
		}
287
	}
288
289
	public function filetype($path) {
290
		$path = $this->normalizePath($path);
291
		$stat = $this->stat($path);
292
		if ($stat) {
293
			if ($stat['mimetype'] === 'httpd/unix-directory') {
294
				return 'dir';
295
			}
296
			return 'file';
297
		} else {
298
			return false;
299
		}
300
	}
301
302
	public function fopen($path, $mode) {
303
		$path = $this->normalizePath($path);
304
305
		if (strrpos($path, '.') !== false) {
306
			$ext = substr($path, strrpos($path, '.'));
307
		} else {
308
			$ext = '';
309
		}
310
311
		switch ($mode) {
312
			case 'r':
313
			case 'rb':
314
				$stat = $this->stat($path);
315
				if (is_array($stat)) {
316
					$filesize = $stat['size'] ?? 0;
317
					// Reading 0 sized files is a waste of time
318
					if ($filesize === 0) {
319
						return fopen('php://memory', $mode);
320
					}
321
322
					try {
323
						$handle = $this->objectStore->readObject($this->getURN($stat['fileid']));
324
						if ($handle === false) {
0 ignored issues
show
introduced by
The condition $handle === false is always false.
Loading history...
325
							return false; // keep backward compatibility
326
						}
327
						$streamStat = fstat($handle);
328
						$actualSize = $streamStat['size'] ?? -1;
329
						if ($actualSize > -1 && $actualSize !== $filesize) {
330
							$this->getCache()->update((int)$stat['fileid'], ['size' => $actualSize]);
331
						}
332
						return $handle;
333
					} catch (NotFoundException $e) {
334
						$this->logger->logException($e, [
335
							'app' => 'objectstore',
336
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
337
						]);
338
						throw $e;
339
					} catch (\Exception $ex) {
340
						$this->logger->logException($ex, [
341
							'app' => 'objectstore',
342
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
343
						]);
344
						return false;
345
					}
346
				} else {
347
					return false;
348
				}
349
				// no break
350
			case 'w':
351
			case 'wb':
352
			case 'w+':
353
			case 'wb+':
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($path) {
383
		$path = $this->normalizePath($path);
384
		return (bool)$this->stat($path);
385
	}
386
387
	public function rename($source, $target) {
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($path) {
397
		$path = $this->normalizePath($path);
398
		return parent::getMimeType($path);
399
	}
400
401
	public function touch($path, $mtime = null) {
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
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
424
				$stat = [
425
					'etag' => $this->getETag($path),
426
					'mimetype' => $mimeType,
427
					'size' => 0,
428
					'mtime' => $mtime,
429
					'storage_mtime' => $mtime,
430
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
431
				];
432
				$this->getCache()->put($path, $stat);
433
			} catch (\Exception $ex) {
434
				$this->logger->logException($ex, [
435
					'app' => 'objectstore',
436
					'message' => 'Could not create object for ' . $path,
437
				]);
438
				throw $ex;
439
			}
440
		}
441
		return true;
442
	}
443
444
	public function writeBack($tmpFile, $path) {
445
		$size = filesize($tmpFile);
446
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
447
	}
448
449
	/**
450
	 * external changes are not supported, exclusive access to the object storage is assumed
451
	 *
452
	 * @param string $path
453
	 * @param int $time
454
	 * @return false
455
	 */
456
	public function hasUpdated($path, $time) {
457
		return false;
458
	}
459
460
	public function needsPartFile() {
461
		return false;
462
	}
463
464
	public function file_put_contents($path, $data) {
465
		$handle = $this->fopen($path, 'w+');
466
		$result = fwrite($handle, $data);
467
		fclose($handle);
468
		return $result;
469
	}
470
471
	public function writeStream(string $path, $stream, int $size = null): int {
472
		$stat = $this->stat($path);
473
		if (empty($stat)) {
474
			// create new file
475
			$stat = [
476
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
477
			];
478
		}
479
		// update stat with new data
480
		$mTime = time();
481
		$stat['size'] = (int)$size;
482
		$stat['mtime'] = $mTime;
483
		$stat['storage_mtime'] = $mTime;
484
485
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
486
		$mimetype = $mimetypeDetector->detectPath($path);
487
488
		$stat['mimetype'] = $mimetype;
489
		$stat['etag'] = $this->getETag($path);
490
		$stat['checksum'] = '';
491
492
		$exists = $this->getCache()->inCache($path);
493
		$uploadPath = $exists ? $path : $path . '.part';
494
495
		if ($exists) {
496
			$fileId = $stat['fileid'];
497
		} else {
498
			$fileId = $this->getCache()->put($uploadPath, $stat);
499
		}
500
501
		$urn = $this->getURN($fileId);
502
		try {
503
			//upload to object storage
504
			if ($size === null) {
505
				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
506
					$this->getCache()->update($fileId, [
507
						'size' => $writtenSize,
508
					]);
509
					$size = $writtenSize;
510
				});
511
				$this->objectStore->writeObject($urn, $countStream, $mimetype);
512
				if (is_resource($countStream)) {
513
					fclose($countStream);
514
				}
515
				$stat['size'] = $size;
516
			} else {
517
				$this->objectStore->writeObject($urn, $stream, $mimetype);
518
				if (is_resource($stream)) {
519
					fclose($stream);
520
				}
521
			}
522
		} catch (\Exception $ex) {
523
			if (!$exists) {
524
				/*
525
				 * Only remove the entry if we are dealing with a new file.
526
				 * Else people lose access to existing files
527
				 */
528
				$this->getCache()->remove($uploadPath);
529
				$this->logger->logException($ex, [
530
					'app' => 'objectstore',
531
					'message' => 'Could not create object ' . $urn . ' for ' . $path,
532
				]);
533
			} else {
534
				$this->logger->logException($ex, [
535
					'app' => 'objectstore',
536
					'message' => 'Could not update object ' . $urn . ' for ' . $path,
537
				]);
538
			}
539
			throw $ex; // make this bubble up
540
		}
541
542
		if ($exists) {
543
			$this->getCache()->update($fileId, $stat);
544
		} else {
545
			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
546
				$this->getCache()->move($uploadPath, $path);
547
			} else {
548
				$this->getCache()->remove($uploadPath);
549
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
550
			}
551
		}
552
553
		return $size;
554
	}
555
556
	public function getObjectStore(): IObjectStore {
557
		return $this->objectStore;
558
	}
559
560
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
561
		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
562
			/** @var ObjectStoreStorage $sourceStorage */
563
			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
564
				/** @var CacheEntry $sourceEntry */
565
				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
566
				$sourceEntryData = $sourceEntry->getData();
567
				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
568
				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
569
				// unjailed storage.
570
				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
571
					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
572
				}
573
				$this->copyInner($sourceEntry, $targetInternalPath);
574
				return true;
575
			}
576
		}
577
578
		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
579
	}
580
581
	public function copy($source, $target) {
582
		$source = $this->normalizePath($source);
583
		$target = $this->normalizePath($target);
584
585
		$cache = $this->getCache();
586
		$sourceEntry = $cache->get($source);
587
		if (!$sourceEntry) {
588
			throw new NotFoundException('Source object not found');
589
		}
590
591
		$this->copyInner($sourceEntry, $target);
592
593
		return true;
594
	}
595
596
	private function copyInner(ICacheEntry $sourceEntry, string $to) {
597
		$cache = $this->getCache();
598
599
		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
600
			if ($cache->inCache($to)) {
601
				$cache->remove($to);
602
			}
603
			$this->mkdir($to);
604
605
			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
606
				$this->copyInner($child, $to . '/' . $child->getName());
607
			}
608
		} else {
609
			$this->copyFile($sourceEntry, $to);
610
		}
611
	}
612
613
	private function copyFile(ICacheEntry $sourceEntry, string $to) {
614
		$cache = $this->getCache();
615
616
		$sourceUrn = $this->getURN($sourceEntry->getId());
617
618
		if (!$cache instanceof Cache) {
0 ignored issues
show
introduced by
$cache is always a sub-type of OC\Files\Cache\Cache.
Loading history...
619
			throw new \Exception("Invalid source cache for object store copy");
620
		}
621
622
		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
623
624
		$targetUrn = $this->getURN($targetId);
625
626
		try {
627
			$this->objectStore->copyObject($sourceUrn, $targetUrn);
628
		} catch (\Exception $e) {
629
			$cache->remove($to);
630
631
			throw $e;
632
		}
633
	}
634
635
	public function startChunkedWrite(string $targetPath): string {
636
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
637
			throw new GenericFileException('Object store does not support multipart upload');
638
		}
639
		$cacheEntry = $this->getCache()->get($targetPath);
640
		$urn = $this->getURN($cacheEntry->getId());
641
		return $this->objectStore->initiateMultipartUpload($urn);
642
	}
643
644
	/**
645
	 *
646
	 * @throws GenericFileException
647
	 */
648
	public function putChunkedWritePart(string $targetPath, string $writeToken, string $chunkId, $data, $size = null): ?array {
649
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
650
			throw new GenericFileException('Object store does not support multipart upload');
651
		}
652
		$cacheEntry = $this->getCache()->get($targetPath);
653
		$urn = $this->getURN($cacheEntry->getId());
654
655
		$result = $this->objectStore->uploadMultipartPart($urn, $writeToken, (int)$chunkId, $data, $size);
656
657
		$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...
658
			'PartNumber' => $chunkId,
659
			'ETag' => trim($result->get('ETag'), '"')
660
		];
661
		return $parts[$chunkId];
662
	}
663
664
	public function completeChunkedWrite(string $targetPath, string $writeToken): int {
665
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
666
			throw new GenericFileException('Object store does not support multipart upload');
667
		}
668
		$cacheEntry = $this->getCache()->get($targetPath);
669
		$urn = $this->getURN($cacheEntry->getId());
670
		$parts = $this->objectStore->getMultipartUploads($urn, $writeToken);
671
		$sortedParts = array_values($parts);
672
		sort($sortedParts);
673
		try {
674
			$size = $this->objectStore->completeMultipartUpload($urn, $writeToken, $sortedParts);
675
			$stat = $this->stat($targetPath);
676
			$mtime = time();
677
			if (is_array($stat)) {
678
				$stat['size'] = $size;
679
				$stat['mtime'] = $mtime;
680
				$stat['mimetype'] = $this->getMimeType($targetPath);
681
				$this->getCache()->update($stat['fileid'], $stat);
682
			}
683
		} catch (S3MultipartUploadException | S3Exception $e) {
684
			$this->objectStore->abortMultipartUpload($urn, $writeToken);
685
			$this->logger->logException($e, [
686
				'app' => 'objectstore',
687
				'message' => 'Could not compete multipart upload ' . $urn. ' with uploadId ' . $writeToken
688
			]);
689
			throw new GenericFileException('Could not write chunked file');
690
		}
691
		return $size;
692
	}
693
694
	public function cancelChunkedWrite(string $targetPath, string $writeToken): void {
695
		if (!$this->objectStore instanceof IObjectStoreMultiPartUpload) {
696
			throw new GenericFileException('Object store does not support multipart upload');
697
		}
698
		$cacheEntry = $this->getCache()->get($targetPath);
699
		$urn = $this->getURN($cacheEntry->getId());
700
		$this->objectStore->abortMultipartUpload($urn, $writeToken);
701
	}
702
}
703