Passed
Push — master ( 7e3600...e3822e )
by Blizzz
36:19 queued 20:27
created
apps/files_external/lib/Lib/Storage/AmazonS3.php 2 patches
Indentation   +732 added lines, -732 removed lines patch added patch discarded remove patch
@@ -58,736 +58,736 @@
 block discarded – undo
58 58
 use Psr\Log\LoggerInterface;
59 59
 
60 60
 class AmazonS3 extends \OC\Files\Storage\Common {
61
-	use S3ConnectionTrait;
62
-	use S3ObjectTrait;
63
-
64
-	private LoggerInterface $logger;
65
-
66
-	public function needsPartFile() {
67
-		return false;
68
-	}
69
-
70
-	/** @var CappedMemoryCache<array|false> */
71
-	private CappedMemoryCache $objectCache;
72
-
73
-	/** @var CappedMemoryCache<bool> */
74
-	private CappedMemoryCache $directoryCache;
75
-
76
-	/** @var CappedMemoryCache<array> */
77
-	private CappedMemoryCache $filesCache;
78
-
79
-	private IMimeTypeDetector $mimeDetector;
80
-	private ?bool $versioningEnabled = null;
81
-	private ICache $memCache;
82
-
83
-	public function __construct($parameters) {
84
-		parent::__construct($parameters);
85
-		$this->parseParams($parameters);
86
-		$this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
87
-		$this->objectCache = new CappedMemoryCache();
88
-		$this->directoryCache = new CappedMemoryCache();
89
-		$this->filesCache = new CappedMemoryCache();
90
-		$this->mimeDetector = Server::get(IMimeTypeDetector::class);
91
-		/** @var ICacheFactory $cacheFactory */
92
-		$cacheFactory = Server::get(ICacheFactory::class);
93
-		$this->memCache = $cacheFactory->createLocal('s3-external');
94
-		$this->logger = Server::get(LoggerInterface::class);
95
-	}
96
-
97
-	/**
98
-	 * @param string $path
99
-	 * @return string correctly encoded path
100
-	 */
101
-	private function normalizePath($path) {
102
-		$path = trim($path, '/');
103
-
104
-		if (!$path) {
105
-			$path = '.';
106
-		}
107
-
108
-		return $path;
109
-	}
110
-
111
-	private function isRoot($path) {
112
-		return $path === '.';
113
-	}
114
-
115
-	private function cleanKey($path) {
116
-		if ($this->isRoot($path)) {
117
-			return '/';
118
-		}
119
-		return $path;
120
-	}
121
-
122
-	private function clearCache() {
123
-		$this->objectCache = new CappedMemoryCache();
124
-		$this->directoryCache = new CappedMemoryCache();
125
-		$this->filesCache = new CappedMemoryCache();
126
-	}
127
-
128
-	private function invalidateCache($key) {
129
-		unset($this->objectCache[$key]);
130
-		$keys = array_keys($this->objectCache->getData());
131
-		$keyLength = strlen($key);
132
-		foreach ($keys as $existingKey) {
133
-			if (substr($existingKey, 0, $keyLength) === $key) {
134
-				unset($this->objectCache[$existingKey]);
135
-			}
136
-		}
137
-		unset($this->filesCache[$key]);
138
-		$keys = array_keys($this->directoryCache->getData());
139
-		$keyLength = strlen($key);
140
-		foreach ($keys as $existingKey) {
141
-			if (substr($existingKey, 0, $keyLength) === $key) {
142
-				unset($this->directoryCache[$existingKey]);
143
-			}
144
-		}
145
-		unset($this->directoryCache[$key]);
146
-	}
147
-
148
-	/**
149
-	 * @return array|false
150
-	 */
151
-	private function headObject(string $key) {
152
-		if (!isset($this->objectCache[$key])) {
153
-			try {
154
-				$this->objectCache[$key] = $this->getConnection()->headObject([
155
-					'Bucket' => $this->bucket,
156
-					'Key' => $key
157
-				])->toArray();
158
-			} catch (S3Exception $e) {
159
-				if ($e->getStatusCode() >= 500) {
160
-					throw $e;
161
-				}
162
-				$this->objectCache[$key] = false;
163
-			}
164
-		}
165
-
166
-		if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) {
167
-			/** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
168
-			$this->objectCache[$key]["Key"] = $key;
169
-		}
170
-		return $this->objectCache[$key];
171
-	}
172
-
173
-	/**
174
-	 * Return true if directory exists
175
-	 *
176
-	 * There are no folders in s3. A folder like structure could be archived
177
-	 * by prefixing files with the folder name.
178
-	 *
179
-	 * Implementation from flysystem-aws-s3-v3:
180
-	 * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
181
-	 *
182
-	 * @param $path
183
-	 * @return bool
184
-	 * @throws \Exception
185
-	 */
186
-	private function doesDirectoryExist($path) {
187
-		if ($path === '.' || $path === '') {
188
-			return true;
189
-		}
190
-		$path = rtrim($path, '/') . '/';
191
-
192
-		if (isset($this->directoryCache[$path])) {
193
-			return $this->directoryCache[$path];
194
-		}
195
-		try {
196
-			// Maybe this isn't an actual key, but a prefix.
197
-			// Do a prefix listing of objects to determine.
198
-			$result = $this->getConnection()->listObjectsV2([
199
-				'Bucket' => $this->bucket,
200
-				'Prefix' => $path,
201
-				'MaxKeys' => 1,
202
-			]);
203
-
204
-			if (isset($result['Contents'])) {
205
-				$this->directoryCache[$path] = true;
206
-				return true;
207
-			}
208
-
209
-			// empty directories have their own object
210
-			$object = $this->headObject($path);
211
-
212
-			if ($object) {
213
-				$this->directoryCache[$path] = true;
214
-				return true;
215
-			}
216
-		} catch (S3Exception $e) {
217
-			if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
218
-				$this->directoryCache[$path] = false;
219
-			}
220
-			throw $e;
221
-		}
222
-
223
-
224
-		$this->directoryCache[$path] = false;
225
-		return false;
226
-	}
227
-
228
-	/**
229
-	 * Remove a file or folder
230
-	 *
231
-	 * @param string $path
232
-	 * @return bool
233
-	 */
234
-	protected function remove($path) {
235
-		// remember fileType to reduce http calls
236
-		$fileType = $this->filetype($path);
237
-		if ($fileType === 'dir') {
238
-			return $this->rmdir($path);
239
-		} elseif ($fileType === 'file') {
240
-			return $this->unlink($path);
241
-		} else {
242
-			return false;
243
-		}
244
-	}
245
-
246
-	public function mkdir($path) {
247
-		$path = $this->normalizePath($path);
248
-
249
-		if ($this->is_dir($path)) {
250
-			return false;
251
-		}
252
-
253
-		try {
254
-			$this->getConnection()->putObject([
255
-				'Bucket' => $this->bucket,
256
-				'Key' => $path . '/',
257
-				'Body' => '',
258
-				'ContentType' => FileInfo::MIMETYPE_FOLDER
259
-			]);
260
-			$this->testTimeout();
261
-		} catch (S3Exception $e) {
262
-			$this->logger->error($e->getMessage(), [
263
-				'app' => 'files_external',
264
-				'exception' => $e,
265
-			]);
266
-			return false;
267
-		}
268
-
269
-		$this->invalidateCache($path);
270
-
271
-		return true;
272
-	}
273
-
274
-	public function file_exists($path) {
275
-		return $this->filetype($path) !== false;
276
-	}
277
-
278
-
279
-	public function rmdir($path) {
280
-		$path = $this->normalizePath($path);
281
-
282
-		if ($this->isRoot($path)) {
283
-			return $this->clearBucket();
284
-		}
285
-
286
-		if (!$this->file_exists($path)) {
287
-			return false;
288
-		}
289
-
290
-		$this->invalidateCache($path);
291
-		return $this->batchDelete($path);
292
-	}
293
-
294
-	protected function clearBucket() {
295
-		$this->clearCache();
296
-		return $this->batchDelete();
297
-	}
298
-
299
-	private function batchDelete($path = null) {
300
-		// TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html
301
-		$params = [
302
-			'Bucket' => $this->bucket
303
-		];
304
-		if ($path !== null) {
305
-			$params['Prefix'] = $path . '/';
306
-		}
307
-		try {
308
-			$connection = $this->getConnection();
309
-			// Since there are no real directories on S3, we need
310
-			// to delete all objects prefixed with the path.
311
-			do {
312
-				// instead of the iterator, manually loop over the list ...
313
-				$objects = $connection->listObjects($params);
314
-				// ... so we can delete the files in batches
315
-				if (isset($objects['Contents'])) {
316
-					$connection->deleteObjects([
317
-						'Bucket' => $this->bucket,
318
-						'Delete' => [
319
-							'Objects' => $objects['Contents']
320
-						]
321
-					]);
322
-					$this->testTimeout();
323
-				}
324
-				// we reached the end when the list is no longer truncated
325
-			} while ($objects['IsTruncated']);
326
-			if ($path !== '' && $path !== null) {
327
-				$this->deleteObject($path);
328
-			}
329
-		} catch (S3Exception $e) {
330
-			$this->logger->error($e->getMessage(), [
331
-				'app' => 'files_external',
332
-				'exception' => $e,
333
-			]);
334
-			return false;
335
-		}
336
-		return true;
337
-	}
338
-
339
-	public function opendir($path) {
340
-		try {
341
-			$content = iterator_to_array($this->getDirectoryContent($path));
342
-			return IteratorDirectory::wrap(array_map(function (array $item) {
343
-				return $item['name'];
344
-			}, $content));
345
-		} catch (S3Exception $e) {
346
-			return false;
347
-		}
348
-	}
349
-
350
-	public function stat($path) {
351
-		$path = $this->normalizePath($path);
352
-
353
-		if ($this->is_dir($path)) {
354
-			$stat = $this->getDirectoryMetaData($path);
355
-		} else {
356
-			$object = $this->headObject($path);
357
-			if ($object === false) {
358
-				return false;
359
-			}
360
-			$stat = $this->objectToMetaData($object);
361
-		}
362
-		$stat['atime'] = time();
363
-
364
-		return $stat;
365
-	}
366
-
367
-	/**
368
-	 * Return content length for object
369
-	 *
370
-	 * When the information is already present (e.g. opendir has been called before)
371
-	 * this value is return. Otherwise a headObject is emitted.
372
-	 *
373
-	 * @param $path
374
-	 * @return int|mixed
375
-	 */
376
-	private function getContentLength($path) {
377
-		if (isset($this->filesCache[$path])) {
378
-			return (int)$this->filesCache[$path]['ContentLength'];
379
-		}
380
-
381
-		$result = $this->headObject($path);
382
-		if (isset($result['ContentLength'])) {
383
-			return (int)$result['ContentLength'];
384
-		}
385
-
386
-		return 0;
387
-	}
388
-
389
-	/**
390
-	 * Return last modified for object
391
-	 *
392
-	 * When the information is already present (e.g. opendir has been called before)
393
-	 * this value is return. Otherwise a headObject is emitted.
394
-	 *
395
-	 * @param $path
396
-	 * @return mixed|string
397
-	 */
398
-	private function getLastModified($path) {
399
-		if (isset($this->filesCache[$path])) {
400
-			return $this->filesCache[$path]['LastModified'];
401
-		}
402
-
403
-		$result = $this->headObject($path);
404
-		if (isset($result['LastModified'])) {
405
-			return $result['LastModified'];
406
-		}
407
-
408
-		return 'now';
409
-	}
410
-
411
-	public function is_dir($path) {
412
-		$path = $this->normalizePath($path);
413
-
414
-		if (isset($this->filesCache[$path])) {
415
-			return false;
416
-		}
417
-
418
-		try {
419
-			return $this->doesDirectoryExist($path);
420
-		} catch (S3Exception $e) {
421
-			$this->logger->error($e->getMessage(), [
422
-				'app' => 'files_external',
423
-				'exception' => $e,
424
-			]);
425
-			return false;
426
-		}
427
-	}
428
-
429
-	public function filetype($path) {
430
-		$path = $this->normalizePath($path);
431
-
432
-		if ($this->isRoot($path)) {
433
-			return 'dir';
434
-		}
435
-
436
-		try {
437
-			if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
438
-				return 'dir';
439
-			}
440
-			if (isset($this->filesCache[$path]) || $this->headObject($path)) {
441
-				return 'file';
442
-			}
443
-			if ($this->doesDirectoryExist($path)) {
444
-				return 'dir';
445
-			}
446
-		} catch (S3Exception $e) {
447
-			$this->logger->error($e->getMessage(), [
448
-				'app' => 'files_external',
449
-				'exception' => $e,
450
-			]);
451
-			return false;
452
-		}
453
-
454
-		return false;
455
-	}
456
-
457
-	public function getPermissions($path) {
458
-		$type = $this->filetype($path);
459
-		if (!$type) {
460
-			return 0;
461
-		}
462
-		return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
463
-	}
464
-
465
-	public function unlink($path) {
466
-		$path = $this->normalizePath($path);
467
-
468
-		if ($this->is_dir($path)) {
469
-			return $this->rmdir($path);
470
-		}
471
-
472
-		try {
473
-			$this->deleteObject($path);
474
-			$this->invalidateCache($path);
475
-		} catch (S3Exception $e) {
476
-			$this->logger->error($e->getMessage(), [
477
-				'app' => 'files_external',
478
-				'exception' => $e,
479
-			]);
480
-			return false;
481
-		}
482
-
483
-		return true;
484
-	}
485
-
486
-	public function fopen($path, $mode) {
487
-		$path = $this->normalizePath($path);
488
-
489
-		switch ($mode) {
490
-			case 'r':
491
-			case 'rb':
492
-				// Don't try to fetch empty files
493
-				$stat = $this->stat($path);
494
-				if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
495
-					return fopen('php://memory', $mode);
496
-				}
497
-
498
-				try {
499
-					return $this->readObject($path);
500
-				} catch (S3Exception $e) {
501
-					$this->logger->error($e->getMessage(), [
502
-						'app' => 'files_external',
503
-						'exception' => $e,
504
-					]);
505
-					return false;
506
-				}
507
-			case 'w':
508
-			case 'wb':
509
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
510
-
511
-				$handle = fopen($tmpFile, 'w');
512
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
513
-					$this->writeBack($tmpFile, $path);
514
-				});
515
-			case 'a':
516
-			case 'ab':
517
-			case 'r+':
518
-			case 'w+':
519
-			case 'wb+':
520
-			case 'a+':
521
-			case 'x':
522
-			case 'x+':
523
-			case 'c':
524
-			case 'c+':
525
-				if (strrpos($path, '.') !== false) {
526
-					$ext = substr($path, strrpos($path, '.'));
527
-				} else {
528
-					$ext = '';
529
-				}
530
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
531
-				if ($this->file_exists($path)) {
532
-					$source = $this->readObject($path);
533
-					file_put_contents($tmpFile, $source);
534
-				}
535
-
536
-				$handle = fopen($tmpFile, $mode);
537
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
538
-					$this->writeBack($tmpFile, $path);
539
-				});
540
-		}
541
-		return false;
542
-	}
543
-
544
-	public function touch($path, $mtime = null) {
545
-		if (is_null($mtime)) {
546
-			$mtime = time();
547
-		}
548
-		$metadata = [
549
-			'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
550
-		];
551
-
552
-		try {
553
-			if (!$this->file_exists($path)) {
554
-				$mimeType = $this->mimeDetector->detectPath($path);
555
-				$this->getConnection()->putObject([
556
-					'Bucket' => $this->bucket,
557
-					'Key' => $this->cleanKey($path),
558
-					'Metadata' => $metadata,
559
-					'Body' => '',
560
-					'ContentType' => $mimeType,
561
-					'MetadataDirective' => 'REPLACE',
562
-				]);
563
-				$this->testTimeout();
564
-			}
565
-		} catch (S3Exception $e) {
566
-			$this->logger->error($e->getMessage(), [
567
-				'app' => 'files_external',
568
-				'exception' => $e,
569
-			]);
570
-			return false;
571
-		}
572
-
573
-		$this->invalidateCache($path);
574
-		return true;
575
-	}
576
-
577
-	public function copy($source, $target, $isFile = null) {
578
-		$source = $this->normalizePath($source);
579
-		$target = $this->normalizePath($target);
580
-
581
-		if ($isFile === true || $this->is_file($source)) {
582
-			try {
583
-				$this->getConnection()->copyObject([
584
-					'Bucket' => $this->bucket,
585
-					'Key' => $this->cleanKey($target),
586
-					'CopySource' => S3Client::encodeKey($this->bucket . '/' . $source)
587
-				]);
588
-				$this->testTimeout();
589
-			} catch (S3Exception $e) {
590
-				$this->logger->error($e->getMessage(), [
591
-					'app' => 'files_external',
592
-					'exception' => $e,
593
-				]);
594
-				return false;
595
-			}
596
-		} else {
597
-			$this->remove($target);
598
-
599
-			try {
600
-				$this->mkdir($target);
601
-				$this->testTimeout();
602
-			} catch (S3Exception $e) {
603
-				$this->logger->error($e->getMessage(), [
604
-					'app' => 'files_external',
605
-					'exception' => $e,
606
-				]);
607
-				return false;
608
-			}
609
-
610
-			foreach ($this->getDirectoryContent($source) as $item) {
611
-				$childSource = $source . '/' . $item['name'];
612
-				$childTarget = $target . '/' . $item['name'];
613
-				$this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
614
-			}
615
-		}
616
-
617
-		$this->invalidateCache($target);
618
-
619
-		return true;
620
-	}
621
-
622
-	public function rename($source, $target) {
623
-		$source = $this->normalizePath($source);
624
-		$target = $this->normalizePath($target);
625
-
626
-		if ($this->is_file($source)) {
627
-			if ($this->copy($source, $target) === false) {
628
-				return false;
629
-			}
630
-
631
-			if ($this->unlink($source) === false) {
632
-				$this->unlink($target);
633
-				return false;
634
-			}
635
-		} else {
636
-			if ($this->copy($source, $target) === false) {
637
-				return false;
638
-			}
639
-
640
-			if ($this->rmdir($source) === false) {
641
-				$this->rmdir($target);
642
-				return false;
643
-			}
644
-		}
645
-
646
-		return true;
647
-	}
648
-
649
-	public function test() {
650
-		$this->getConnection()->headBucket([
651
-			'Bucket' => $this->bucket
652
-		]);
653
-		return true;
654
-	}
655
-
656
-	public function getId() {
657
-		return $this->id;
658
-	}
659
-
660
-	public function writeBack($tmpFile, $path) {
661
-		try {
662
-			$source = fopen($tmpFile, 'r');
663
-			$this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
664
-			$this->invalidateCache($path);
665
-
666
-			unlink($tmpFile);
667
-			return true;
668
-		} catch (S3Exception $e) {
669
-			$this->logger->error($e->getMessage(), [
670
-				'app' => 'files_external',
671
-				'exception' => $e,
672
-			]);
673
-			return false;
674
-		}
675
-	}
676
-
677
-	/**
678
-	 * check if curl is installed
679
-	 */
680
-	public static function checkDependencies() {
681
-		return true;
682
-	}
683
-
684
-	public function getDirectoryContent($directory): \Traversable {
685
-		$path = $this->normalizePath($directory);
686
-
687
-		if ($this->isRoot($path)) {
688
-			$path = '';
689
-		} else {
690
-			$path .= '/';
691
-		}
692
-
693
-		$results = $this->getConnection()->getPaginator('ListObjectsV2', [
694
-			'Bucket' => $this->bucket,
695
-			'Delimiter' => '/',
696
-			'Prefix' => $path,
697
-		]);
698
-
699
-		foreach ($results as $result) {
700
-			// sub folders
701
-			if (is_array($result['CommonPrefixes'])) {
702
-				foreach ($result['CommonPrefixes'] as $prefix) {
703
-					$dir = $this->getDirectoryMetaData($prefix['Prefix']);
704
-					if ($dir) {
705
-						yield $dir;
706
-					}
707
-				}
708
-			}
709
-			if (is_array($result['Contents'])) {
710
-				foreach ($result['Contents'] as $object) {
711
-					$this->objectCache[$object['Key']] = $object;
712
-					if ($object['Key'] !== $path) {
713
-						yield $this->objectToMetaData($object);
714
-					}
715
-				}
716
-			}
717
-		}
718
-	}
719
-
720
-	private function objectToMetaData(array $object): array {
721
-		return [
722
-			'name' => basename($object['Key']),
723
-			'mimetype' => $this->mimeDetector->detectPath($object['Key']),
724
-			'mtime' => strtotime($object['LastModified']),
725
-			'storage_mtime' => strtotime($object['LastModified']),
726
-			'etag' => $object['ETag'],
727
-			'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
728
-			'size' => (int)($object['Size'] ?? $object['ContentLength']),
729
-		];
730
-	}
731
-
732
-	private function getDirectoryMetaData(string $path): ?array {
733
-		$path = trim($path, '/');
734
-		// when versioning is enabled, delete markers are returned as part of CommonPrefixes
735
-		// resulting in "ghost" folders, verify that each folder actually exists
736
-		if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
737
-			return null;
738
-		}
739
-		$cacheEntry = $this->getCache()->get($path);
740
-		if ($cacheEntry instanceof CacheEntry) {
741
-			return $cacheEntry->getData();
742
-		} else {
743
-			return [
744
-				'name' => basename($path),
745
-				'mimetype' => FileInfo::MIMETYPE_FOLDER,
746
-				'mtime' => time(),
747
-				'storage_mtime' => time(),
748
-				'etag' => uniqid(),
749
-				'permissions' => Constants::PERMISSION_ALL,
750
-				'size' => -1,
751
-			];
752
-		}
753
-	}
754
-
755
-	public function versioningEnabled(): bool {
756
-		if ($this->versioningEnabled === null) {
757
-			$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
758
-			if ($cached === null) {
759
-				$this->versioningEnabled = $this->getVersioningStatusFromBucket();
760
-				$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
761
-			} else {
762
-				$this->versioningEnabled = $cached;
763
-			}
764
-		}
765
-		return $this->versioningEnabled;
766
-	}
767
-
768
-	protected function getVersioningStatusFromBucket(): bool {
769
-		try {
770
-			$result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
771
-			return $result->get('Status') === 'Enabled';
772
-		} catch (S3Exception $s3Exception) {
773
-			// This is needed for compatibility with Storj gateway which does not support versioning yet
774
-			if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
775
-				return false;
776
-			}
777
-			throw $s3Exception;
778
-		}
779
-	}
780
-
781
-	public function hasUpdated($path, $time) {
782
-		// for files we can get the proper mtime
783
-		if ($path !== '' && $object = $this->headObject($path)) {
784
-			$stat = $this->objectToMetaData($object);
785
-			return $stat['mtime'] > $time;
786
-		} else {
787
-			// for directories, the only real option we have is to do a prefix listing and iterate over all objects
788
-			// however, since this is just as expensive as just re-scanning the directory, we can simply return true
789
-			// and have the scanner figure out if anything has actually changed
790
-			return true;
791
-		}
792
-	}
61
+    use S3ConnectionTrait;
62
+    use S3ObjectTrait;
63
+
64
+    private LoggerInterface $logger;
65
+
66
+    public function needsPartFile() {
67
+        return false;
68
+    }
69
+
70
+    /** @var CappedMemoryCache<array|false> */
71
+    private CappedMemoryCache $objectCache;
72
+
73
+    /** @var CappedMemoryCache<bool> */
74
+    private CappedMemoryCache $directoryCache;
75
+
76
+    /** @var CappedMemoryCache<array> */
77
+    private CappedMemoryCache $filesCache;
78
+
79
+    private IMimeTypeDetector $mimeDetector;
80
+    private ?bool $versioningEnabled = null;
81
+    private ICache $memCache;
82
+
83
+    public function __construct($parameters) {
84
+        parent::__construct($parameters);
85
+        $this->parseParams($parameters);
86
+        $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
87
+        $this->objectCache = new CappedMemoryCache();
88
+        $this->directoryCache = new CappedMemoryCache();
89
+        $this->filesCache = new CappedMemoryCache();
90
+        $this->mimeDetector = Server::get(IMimeTypeDetector::class);
91
+        /** @var ICacheFactory $cacheFactory */
92
+        $cacheFactory = Server::get(ICacheFactory::class);
93
+        $this->memCache = $cacheFactory->createLocal('s3-external');
94
+        $this->logger = Server::get(LoggerInterface::class);
95
+    }
96
+
97
+    /**
98
+     * @param string $path
99
+     * @return string correctly encoded path
100
+     */
101
+    private function normalizePath($path) {
102
+        $path = trim($path, '/');
103
+
104
+        if (!$path) {
105
+            $path = '.';
106
+        }
107
+
108
+        return $path;
109
+    }
110
+
111
+    private function isRoot($path) {
112
+        return $path === '.';
113
+    }
114
+
115
+    private function cleanKey($path) {
116
+        if ($this->isRoot($path)) {
117
+            return '/';
118
+        }
119
+        return $path;
120
+    }
121
+
122
+    private function clearCache() {
123
+        $this->objectCache = new CappedMemoryCache();
124
+        $this->directoryCache = new CappedMemoryCache();
125
+        $this->filesCache = new CappedMemoryCache();
126
+    }
127
+
128
+    private function invalidateCache($key) {
129
+        unset($this->objectCache[$key]);
130
+        $keys = array_keys($this->objectCache->getData());
131
+        $keyLength = strlen($key);
132
+        foreach ($keys as $existingKey) {
133
+            if (substr($existingKey, 0, $keyLength) === $key) {
134
+                unset($this->objectCache[$existingKey]);
135
+            }
136
+        }
137
+        unset($this->filesCache[$key]);
138
+        $keys = array_keys($this->directoryCache->getData());
139
+        $keyLength = strlen($key);
140
+        foreach ($keys as $existingKey) {
141
+            if (substr($existingKey, 0, $keyLength) === $key) {
142
+                unset($this->directoryCache[$existingKey]);
143
+            }
144
+        }
145
+        unset($this->directoryCache[$key]);
146
+    }
147
+
148
+    /**
149
+     * @return array|false
150
+     */
151
+    private function headObject(string $key) {
152
+        if (!isset($this->objectCache[$key])) {
153
+            try {
154
+                $this->objectCache[$key] = $this->getConnection()->headObject([
155
+                    'Bucket' => $this->bucket,
156
+                    'Key' => $key
157
+                ])->toArray();
158
+            } catch (S3Exception $e) {
159
+                if ($e->getStatusCode() >= 500) {
160
+                    throw $e;
161
+                }
162
+                $this->objectCache[$key] = false;
163
+            }
164
+        }
165
+
166
+        if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]["Key"])) {
167
+            /** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
168
+            $this->objectCache[$key]["Key"] = $key;
169
+        }
170
+        return $this->objectCache[$key];
171
+    }
172
+
173
+    /**
174
+     * Return true if directory exists
175
+     *
176
+     * There are no folders in s3. A folder like structure could be archived
177
+     * by prefixing files with the folder name.
178
+     *
179
+     * Implementation from flysystem-aws-s3-v3:
180
+     * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
181
+     *
182
+     * @param $path
183
+     * @return bool
184
+     * @throws \Exception
185
+     */
186
+    private function doesDirectoryExist($path) {
187
+        if ($path === '.' || $path === '') {
188
+            return true;
189
+        }
190
+        $path = rtrim($path, '/') . '/';
191
+
192
+        if (isset($this->directoryCache[$path])) {
193
+            return $this->directoryCache[$path];
194
+        }
195
+        try {
196
+            // Maybe this isn't an actual key, but a prefix.
197
+            // Do a prefix listing of objects to determine.
198
+            $result = $this->getConnection()->listObjectsV2([
199
+                'Bucket' => $this->bucket,
200
+                'Prefix' => $path,
201
+                'MaxKeys' => 1,
202
+            ]);
203
+
204
+            if (isset($result['Contents'])) {
205
+                $this->directoryCache[$path] = true;
206
+                return true;
207
+            }
208
+
209
+            // empty directories have their own object
210
+            $object = $this->headObject($path);
211
+
212
+            if ($object) {
213
+                $this->directoryCache[$path] = true;
214
+                return true;
215
+            }
216
+        } catch (S3Exception $e) {
217
+            if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
218
+                $this->directoryCache[$path] = false;
219
+            }
220
+            throw $e;
221
+        }
222
+
223
+
224
+        $this->directoryCache[$path] = false;
225
+        return false;
226
+    }
227
+
228
+    /**
229
+     * Remove a file or folder
230
+     *
231
+     * @param string $path
232
+     * @return bool
233
+     */
234
+    protected function remove($path) {
235
+        // remember fileType to reduce http calls
236
+        $fileType = $this->filetype($path);
237
+        if ($fileType === 'dir') {
238
+            return $this->rmdir($path);
239
+        } elseif ($fileType === 'file') {
240
+            return $this->unlink($path);
241
+        } else {
242
+            return false;
243
+        }
244
+    }
245
+
246
+    public function mkdir($path) {
247
+        $path = $this->normalizePath($path);
248
+
249
+        if ($this->is_dir($path)) {
250
+            return false;
251
+        }
252
+
253
+        try {
254
+            $this->getConnection()->putObject([
255
+                'Bucket' => $this->bucket,
256
+                'Key' => $path . '/',
257
+                'Body' => '',
258
+                'ContentType' => FileInfo::MIMETYPE_FOLDER
259
+            ]);
260
+            $this->testTimeout();
261
+        } catch (S3Exception $e) {
262
+            $this->logger->error($e->getMessage(), [
263
+                'app' => 'files_external',
264
+                'exception' => $e,
265
+            ]);
266
+            return false;
267
+        }
268
+
269
+        $this->invalidateCache($path);
270
+
271
+        return true;
272
+    }
273
+
274
+    public function file_exists($path) {
275
+        return $this->filetype($path) !== false;
276
+    }
277
+
278
+
279
+    public function rmdir($path) {
280
+        $path = $this->normalizePath($path);
281
+
282
+        if ($this->isRoot($path)) {
283
+            return $this->clearBucket();
284
+        }
285
+
286
+        if (!$this->file_exists($path)) {
287
+            return false;
288
+        }
289
+
290
+        $this->invalidateCache($path);
291
+        return $this->batchDelete($path);
292
+    }
293
+
294
+    protected function clearBucket() {
295
+        $this->clearCache();
296
+        return $this->batchDelete();
297
+    }
298
+
299
+    private function batchDelete($path = null) {
300
+        // TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html
301
+        $params = [
302
+            'Bucket' => $this->bucket
303
+        ];
304
+        if ($path !== null) {
305
+            $params['Prefix'] = $path . '/';
306
+        }
307
+        try {
308
+            $connection = $this->getConnection();
309
+            // Since there are no real directories on S3, we need
310
+            // to delete all objects prefixed with the path.
311
+            do {
312
+                // instead of the iterator, manually loop over the list ...
313
+                $objects = $connection->listObjects($params);
314
+                // ... so we can delete the files in batches
315
+                if (isset($objects['Contents'])) {
316
+                    $connection->deleteObjects([
317
+                        'Bucket' => $this->bucket,
318
+                        'Delete' => [
319
+                            'Objects' => $objects['Contents']
320
+                        ]
321
+                    ]);
322
+                    $this->testTimeout();
323
+                }
324
+                // we reached the end when the list is no longer truncated
325
+            } while ($objects['IsTruncated']);
326
+            if ($path !== '' && $path !== null) {
327
+                $this->deleteObject($path);
328
+            }
329
+        } catch (S3Exception $e) {
330
+            $this->logger->error($e->getMessage(), [
331
+                'app' => 'files_external',
332
+                'exception' => $e,
333
+            ]);
334
+            return false;
335
+        }
336
+        return true;
337
+    }
338
+
339
+    public function opendir($path) {
340
+        try {
341
+            $content = iterator_to_array($this->getDirectoryContent($path));
342
+            return IteratorDirectory::wrap(array_map(function (array $item) {
343
+                return $item['name'];
344
+            }, $content));
345
+        } catch (S3Exception $e) {
346
+            return false;
347
+        }
348
+    }
349
+
350
+    public function stat($path) {
351
+        $path = $this->normalizePath($path);
352
+
353
+        if ($this->is_dir($path)) {
354
+            $stat = $this->getDirectoryMetaData($path);
355
+        } else {
356
+            $object = $this->headObject($path);
357
+            if ($object === false) {
358
+                return false;
359
+            }
360
+            $stat = $this->objectToMetaData($object);
361
+        }
362
+        $stat['atime'] = time();
363
+
364
+        return $stat;
365
+    }
366
+
367
+    /**
368
+     * Return content length for object
369
+     *
370
+     * When the information is already present (e.g. opendir has been called before)
371
+     * this value is return. Otherwise a headObject is emitted.
372
+     *
373
+     * @param $path
374
+     * @return int|mixed
375
+     */
376
+    private function getContentLength($path) {
377
+        if (isset($this->filesCache[$path])) {
378
+            return (int)$this->filesCache[$path]['ContentLength'];
379
+        }
380
+
381
+        $result = $this->headObject($path);
382
+        if (isset($result['ContentLength'])) {
383
+            return (int)$result['ContentLength'];
384
+        }
385
+
386
+        return 0;
387
+    }
388
+
389
+    /**
390
+     * Return last modified for object
391
+     *
392
+     * When the information is already present (e.g. opendir has been called before)
393
+     * this value is return. Otherwise a headObject is emitted.
394
+     *
395
+     * @param $path
396
+     * @return mixed|string
397
+     */
398
+    private function getLastModified($path) {
399
+        if (isset($this->filesCache[$path])) {
400
+            return $this->filesCache[$path]['LastModified'];
401
+        }
402
+
403
+        $result = $this->headObject($path);
404
+        if (isset($result['LastModified'])) {
405
+            return $result['LastModified'];
406
+        }
407
+
408
+        return 'now';
409
+    }
410
+
411
+    public function is_dir($path) {
412
+        $path = $this->normalizePath($path);
413
+
414
+        if (isset($this->filesCache[$path])) {
415
+            return false;
416
+        }
417
+
418
+        try {
419
+            return $this->doesDirectoryExist($path);
420
+        } catch (S3Exception $e) {
421
+            $this->logger->error($e->getMessage(), [
422
+                'app' => 'files_external',
423
+                'exception' => $e,
424
+            ]);
425
+            return false;
426
+        }
427
+    }
428
+
429
+    public function filetype($path) {
430
+        $path = $this->normalizePath($path);
431
+
432
+        if ($this->isRoot($path)) {
433
+            return 'dir';
434
+        }
435
+
436
+        try {
437
+            if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
438
+                return 'dir';
439
+            }
440
+            if (isset($this->filesCache[$path]) || $this->headObject($path)) {
441
+                return 'file';
442
+            }
443
+            if ($this->doesDirectoryExist($path)) {
444
+                return 'dir';
445
+            }
446
+        } catch (S3Exception $e) {
447
+            $this->logger->error($e->getMessage(), [
448
+                'app' => 'files_external',
449
+                'exception' => $e,
450
+            ]);
451
+            return false;
452
+        }
453
+
454
+        return false;
455
+    }
456
+
457
+    public function getPermissions($path) {
458
+        $type = $this->filetype($path);
459
+        if (!$type) {
460
+            return 0;
461
+        }
462
+        return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
463
+    }
464
+
465
+    public function unlink($path) {
466
+        $path = $this->normalizePath($path);
467
+
468
+        if ($this->is_dir($path)) {
469
+            return $this->rmdir($path);
470
+        }
471
+
472
+        try {
473
+            $this->deleteObject($path);
474
+            $this->invalidateCache($path);
475
+        } catch (S3Exception $e) {
476
+            $this->logger->error($e->getMessage(), [
477
+                'app' => 'files_external',
478
+                'exception' => $e,
479
+            ]);
480
+            return false;
481
+        }
482
+
483
+        return true;
484
+    }
485
+
486
+    public function fopen($path, $mode) {
487
+        $path = $this->normalizePath($path);
488
+
489
+        switch ($mode) {
490
+            case 'r':
491
+            case 'rb':
492
+                // Don't try to fetch empty files
493
+                $stat = $this->stat($path);
494
+                if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
495
+                    return fopen('php://memory', $mode);
496
+                }
497
+
498
+                try {
499
+                    return $this->readObject($path);
500
+                } catch (S3Exception $e) {
501
+                    $this->logger->error($e->getMessage(), [
502
+                        'app' => 'files_external',
503
+                        'exception' => $e,
504
+                    ]);
505
+                    return false;
506
+                }
507
+            case 'w':
508
+            case 'wb':
509
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
510
+
511
+                $handle = fopen($tmpFile, 'w');
512
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
513
+                    $this->writeBack($tmpFile, $path);
514
+                });
515
+            case 'a':
516
+            case 'ab':
517
+            case 'r+':
518
+            case 'w+':
519
+            case 'wb+':
520
+            case 'a+':
521
+            case 'x':
522
+            case 'x+':
523
+            case 'c':
524
+            case 'c+':
525
+                if (strrpos($path, '.') !== false) {
526
+                    $ext = substr($path, strrpos($path, '.'));
527
+                } else {
528
+                    $ext = '';
529
+                }
530
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
531
+                if ($this->file_exists($path)) {
532
+                    $source = $this->readObject($path);
533
+                    file_put_contents($tmpFile, $source);
534
+                }
535
+
536
+                $handle = fopen($tmpFile, $mode);
537
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
538
+                    $this->writeBack($tmpFile, $path);
539
+                });
540
+        }
541
+        return false;
542
+    }
543
+
544
+    public function touch($path, $mtime = null) {
545
+        if (is_null($mtime)) {
546
+            $mtime = time();
547
+        }
548
+        $metadata = [
549
+            'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
550
+        ];
551
+
552
+        try {
553
+            if (!$this->file_exists($path)) {
554
+                $mimeType = $this->mimeDetector->detectPath($path);
555
+                $this->getConnection()->putObject([
556
+                    'Bucket' => $this->bucket,
557
+                    'Key' => $this->cleanKey($path),
558
+                    'Metadata' => $metadata,
559
+                    'Body' => '',
560
+                    'ContentType' => $mimeType,
561
+                    'MetadataDirective' => 'REPLACE',
562
+                ]);
563
+                $this->testTimeout();
564
+            }
565
+        } catch (S3Exception $e) {
566
+            $this->logger->error($e->getMessage(), [
567
+                'app' => 'files_external',
568
+                'exception' => $e,
569
+            ]);
570
+            return false;
571
+        }
572
+
573
+        $this->invalidateCache($path);
574
+        return true;
575
+    }
576
+
577
+    public function copy($source, $target, $isFile = null) {
578
+        $source = $this->normalizePath($source);
579
+        $target = $this->normalizePath($target);
580
+
581
+        if ($isFile === true || $this->is_file($source)) {
582
+            try {
583
+                $this->getConnection()->copyObject([
584
+                    'Bucket' => $this->bucket,
585
+                    'Key' => $this->cleanKey($target),
586
+                    'CopySource' => S3Client::encodeKey($this->bucket . '/' . $source)
587
+                ]);
588
+                $this->testTimeout();
589
+            } catch (S3Exception $e) {
590
+                $this->logger->error($e->getMessage(), [
591
+                    'app' => 'files_external',
592
+                    'exception' => $e,
593
+                ]);
594
+                return false;
595
+            }
596
+        } else {
597
+            $this->remove($target);
598
+
599
+            try {
600
+                $this->mkdir($target);
601
+                $this->testTimeout();
602
+            } catch (S3Exception $e) {
603
+                $this->logger->error($e->getMessage(), [
604
+                    'app' => 'files_external',
605
+                    'exception' => $e,
606
+                ]);
607
+                return false;
608
+            }
609
+
610
+            foreach ($this->getDirectoryContent($source) as $item) {
611
+                $childSource = $source . '/' . $item['name'];
612
+                $childTarget = $target . '/' . $item['name'];
613
+                $this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
614
+            }
615
+        }
616
+
617
+        $this->invalidateCache($target);
618
+
619
+        return true;
620
+    }
621
+
622
+    public function rename($source, $target) {
623
+        $source = $this->normalizePath($source);
624
+        $target = $this->normalizePath($target);
625
+
626
+        if ($this->is_file($source)) {
627
+            if ($this->copy($source, $target) === false) {
628
+                return false;
629
+            }
630
+
631
+            if ($this->unlink($source) === false) {
632
+                $this->unlink($target);
633
+                return false;
634
+            }
635
+        } else {
636
+            if ($this->copy($source, $target) === false) {
637
+                return false;
638
+            }
639
+
640
+            if ($this->rmdir($source) === false) {
641
+                $this->rmdir($target);
642
+                return false;
643
+            }
644
+        }
645
+
646
+        return true;
647
+    }
648
+
649
+    public function test() {
650
+        $this->getConnection()->headBucket([
651
+            'Bucket' => $this->bucket
652
+        ]);
653
+        return true;
654
+    }
655
+
656
+    public function getId() {
657
+        return $this->id;
658
+    }
659
+
660
+    public function writeBack($tmpFile, $path) {
661
+        try {
662
+            $source = fopen($tmpFile, 'r');
663
+            $this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
664
+            $this->invalidateCache($path);
665
+
666
+            unlink($tmpFile);
667
+            return true;
668
+        } catch (S3Exception $e) {
669
+            $this->logger->error($e->getMessage(), [
670
+                'app' => 'files_external',
671
+                'exception' => $e,
672
+            ]);
673
+            return false;
674
+        }
675
+    }
676
+
677
+    /**
678
+     * check if curl is installed
679
+     */
680
+    public static function checkDependencies() {
681
+        return true;
682
+    }
683
+
684
+    public function getDirectoryContent($directory): \Traversable {
685
+        $path = $this->normalizePath($directory);
686
+
687
+        if ($this->isRoot($path)) {
688
+            $path = '';
689
+        } else {
690
+            $path .= '/';
691
+        }
692
+
693
+        $results = $this->getConnection()->getPaginator('ListObjectsV2', [
694
+            'Bucket' => $this->bucket,
695
+            'Delimiter' => '/',
696
+            'Prefix' => $path,
697
+        ]);
698
+
699
+        foreach ($results as $result) {
700
+            // sub folders
701
+            if (is_array($result['CommonPrefixes'])) {
702
+                foreach ($result['CommonPrefixes'] as $prefix) {
703
+                    $dir = $this->getDirectoryMetaData($prefix['Prefix']);
704
+                    if ($dir) {
705
+                        yield $dir;
706
+                    }
707
+                }
708
+            }
709
+            if (is_array($result['Contents'])) {
710
+                foreach ($result['Contents'] as $object) {
711
+                    $this->objectCache[$object['Key']] = $object;
712
+                    if ($object['Key'] !== $path) {
713
+                        yield $this->objectToMetaData($object);
714
+                    }
715
+                }
716
+            }
717
+        }
718
+    }
719
+
720
+    private function objectToMetaData(array $object): array {
721
+        return [
722
+            'name' => basename($object['Key']),
723
+            'mimetype' => $this->mimeDetector->detectPath($object['Key']),
724
+            'mtime' => strtotime($object['LastModified']),
725
+            'storage_mtime' => strtotime($object['LastModified']),
726
+            'etag' => $object['ETag'],
727
+            'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
728
+            'size' => (int)($object['Size'] ?? $object['ContentLength']),
729
+        ];
730
+    }
731
+
732
+    private function getDirectoryMetaData(string $path): ?array {
733
+        $path = trim($path, '/');
734
+        // when versioning is enabled, delete markers are returned as part of CommonPrefixes
735
+        // resulting in "ghost" folders, verify that each folder actually exists
736
+        if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
737
+            return null;
738
+        }
739
+        $cacheEntry = $this->getCache()->get($path);
740
+        if ($cacheEntry instanceof CacheEntry) {
741
+            return $cacheEntry->getData();
742
+        } else {
743
+            return [
744
+                'name' => basename($path),
745
+                'mimetype' => FileInfo::MIMETYPE_FOLDER,
746
+                'mtime' => time(),
747
+                'storage_mtime' => time(),
748
+                'etag' => uniqid(),
749
+                'permissions' => Constants::PERMISSION_ALL,
750
+                'size' => -1,
751
+            ];
752
+        }
753
+    }
754
+
755
+    public function versioningEnabled(): bool {
756
+        if ($this->versioningEnabled === null) {
757
+            $cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
758
+            if ($cached === null) {
759
+                $this->versioningEnabled = $this->getVersioningStatusFromBucket();
760
+                $this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
761
+            } else {
762
+                $this->versioningEnabled = $cached;
763
+            }
764
+        }
765
+        return $this->versioningEnabled;
766
+    }
767
+
768
+    protected function getVersioningStatusFromBucket(): bool {
769
+        try {
770
+            $result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
771
+            return $result->get('Status') === 'Enabled';
772
+        } catch (S3Exception $s3Exception) {
773
+            // This is needed for compatibility with Storj gateway which does not support versioning yet
774
+            if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
775
+                return false;
776
+            }
777
+            throw $s3Exception;
778
+        }
779
+    }
780
+
781
+    public function hasUpdated($path, $time) {
782
+        // for files we can get the proper mtime
783
+        if ($path !== '' && $object = $this->headObject($path)) {
784
+            $stat = $this->objectToMetaData($object);
785
+            return $stat['mtime'] > $time;
786
+        } else {
787
+            // for directories, the only real option we have is to do a prefix listing and iterate over all objects
788
+            // however, since this is just as expensive as just re-scanning the directory, we can simply return true
789
+            // and have the scanner figure out if anything has actually changed
790
+            return true;
791
+        }
792
+    }
793 793
 }
Please login to merge, or discard this patch.
Spacing   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -83,7 +83,7 @@  discard block
 block discarded – undo
83 83
 	public function __construct($parameters) {
84 84
 		parent::__construct($parameters);
85 85
 		$this->parseParams($parameters);
86
-		$this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
86
+		$this->id = 'amazon::external::'.md5($this->params['hostname'].':'.$this->params['bucket'].':'.$this->params['key']);
87 87
 		$this->objectCache = new CappedMemoryCache();
88 88
 		$this->directoryCache = new CappedMemoryCache();
89 89
 		$this->filesCache = new CappedMemoryCache();
@@ -187,7 +187,7 @@  discard block
 block discarded – undo
187 187
 		if ($path === '.' || $path === '') {
188 188
 			return true;
189 189
 		}
190
-		$path = rtrim($path, '/') . '/';
190
+		$path = rtrim($path, '/').'/';
191 191
 
192 192
 		if (isset($this->directoryCache[$path])) {
193 193
 			return $this->directoryCache[$path];
@@ -253,7 +253,7 @@  discard block
 block discarded – undo
253 253
 		try {
254 254
 			$this->getConnection()->putObject([
255 255
 				'Bucket' => $this->bucket,
256
-				'Key' => $path . '/',
256
+				'Key' => $path.'/',
257 257
 				'Body' => '',
258 258
 				'ContentType' => FileInfo::MIMETYPE_FOLDER
259 259
 			]);
@@ -302,7 +302,7 @@  discard block
 block discarded – undo
302 302
 			'Bucket' => $this->bucket
303 303
 		];
304 304
 		if ($path !== null) {
305
-			$params['Prefix'] = $path . '/';
305
+			$params['Prefix'] = $path.'/';
306 306
 		}
307 307
 		try {
308 308
 			$connection = $this->getConnection();
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 	public function opendir($path) {
340 340
 		try {
341 341
 			$content = iterator_to_array($this->getDirectoryContent($path));
342
-			return IteratorDirectory::wrap(array_map(function (array $item) {
342
+			return IteratorDirectory::wrap(array_map(function(array $item) {
343 343
 				return $item['name'];
344 344
 			}, $content));
345 345
 		} catch (S3Exception $e) {
@@ -375,12 +375,12 @@  discard block
 block discarded – undo
375 375
 	 */
376 376
 	private function getContentLength($path) {
377 377
 		if (isset($this->filesCache[$path])) {
378
-			return (int)$this->filesCache[$path]['ContentLength'];
378
+			return (int) $this->filesCache[$path]['ContentLength'];
379 379
 		}
380 380
 
381 381
 		$result = $this->headObject($path);
382 382
 		if (isset($result['ContentLength'])) {
383
-			return (int)$result['ContentLength'];
383
+			return (int) $result['ContentLength'];
384 384
 		}
385 385
 
386 386
 		return 0;
@@ -509,7 +509,7 @@  discard block
 block discarded – undo
509 509
 				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
510 510
 
511 511
 				$handle = fopen($tmpFile, 'w');
512
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
512
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
513 513
 					$this->writeBack($tmpFile, $path);
514 514
 				});
515 515
 			case 'a':
@@ -534,7 +534,7 @@  discard block
 block discarded – undo
534 534
 				}
535 535
 
536 536
 				$handle = fopen($tmpFile, $mode);
537
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
537
+				return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
538 538
 					$this->writeBack($tmpFile, $path);
539 539
 				});
540 540
 		}
@@ -583,7 +583,7 @@  discard block
 block discarded – undo
583 583
 				$this->getConnection()->copyObject([
584 584
 					'Bucket' => $this->bucket,
585 585
 					'Key' => $this->cleanKey($target),
586
-					'CopySource' => S3Client::encodeKey($this->bucket . '/' . $source)
586
+					'CopySource' => S3Client::encodeKey($this->bucket.'/'.$source)
587 587
 				]);
588 588
 				$this->testTimeout();
589 589
 			} catch (S3Exception $e) {
@@ -608,8 +608,8 @@  discard block
 block discarded – undo
608 608
 			}
609 609
 
610 610
 			foreach ($this->getDirectoryContent($source) as $item) {
611
-				$childSource = $source . '/' . $item['name'];
612
-				$childTarget = $target . '/' . $item['name'];
611
+				$childSource = $source.'/'.$item['name'];
612
+				$childTarget = $target.'/'.$item['name'];
613 613
 				$this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
614 614
 			}
615 615
 		}
@@ -725,7 +725,7 @@  discard block
 block discarded – undo
725 725
 			'storage_mtime' => strtotime($object['LastModified']),
726 726
 			'etag' => $object['ETag'],
727 727
 			'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
728
-			'size' => (int)($object['Size'] ?? $object['ContentLength']),
728
+			'size' => (int) ($object['Size'] ?? $object['ContentLength']),
729 729
 		];
730 730
 	}
731 731
 
@@ -754,10 +754,10 @@  discard block
 block discarded – undo
754 754
 
755 755
 	public function versioningEnabled(): bool {
756 756
 		if ($this->versioningEnabled === null) {
757
-			$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
757
+			$cached = $this->memCache->get('versioning-enabled::'.$this->getBucket());
758 758
 			if ($cached === null) {
759 759
 				$this->versioningEnabled = $this->getVersioningStatusFromBucket();
760
-				$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
760
+				$this->memCache->set('versioning-enabled::'.$this->getBucket(), $this->versioningEnabled, 60);
761 761
 			} else {
762 762
 				$this->versioningEnabled = $cached;
763 763
 			}
Please login to merge, or discard this patch.