Passed
Push — master ( 422044...b2e342 )
by Blizzz
18:52 queued 13s
created

ObjectStoreStorage::__construct()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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