Passed
Push — master ( d4341a...140c25 )
by Julius
26:30 queued 09:59
created
apps/files_external/lib/Lib/Storage/AmazonS3.php 2 patches
Indentation   +733 added lines, -733 removed lines patch added patch discarded remove patch
@@ -58,737 +58,737 @@
 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
-					'StorageClass' => $this->storageClass,
588
-				]);
589
-				$this->testTimeout();
590
-			} catch (S3Exception $e) {
591
-				$this->logger->error($e->getMessage(), [
592
-					'app' => 'files_external',
593
-					'exception' => $e,
594
-				]);
595
-				return false;
596
-			}
597
-		} else {
598
-			$this->remove($target);
599
-
600
-			try {
601
-				$this->mkdir($target);
602
-				$this->testTimeout();
603
-			} catch (S3Exception $e) {
604
-				$this->logger->error($e->getMessage(), [
605
-					'app' => 'files_external',
606
-					'exception' => $e,
607
-				]);
608
-				return false;
609
-			}
610
-
611
-			foreach ($this->getDirectoryContent($source) as $item) {
612
-				$childSource = $source . '/' . $item['name'];
613
-				$childTarget = $target . '/' . $item['name'];
614
-				$this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
615
-			}
616
-		}
617
-
618
-		$this->invalidateCache($target);
619
-
620
-		return true;
621
-	}
622
-
623
-	public function rename($source, $target) {
624
-		$source = $this->normalizePath($source);
625
-		$target = $this->normalizePath($target);
626
-
627
-		if ($this->is_file($source)) {
628
-			if ($this->copy($source, $target) === false) {
629
-				return false;
630
-			}
631
-
632
-			if ($this->unlink($source) === false) {
633
-				$this->unlink($target);
634
-				return false;
635
-			}
636
-		} else {
637
-			if ($this->copy($source, $target) === false) {
638
-				return false;
639
-			}
640
-
641
-			if ($this->rmdir($source) === false) {
642
-				$this->rmdir($target);
643
-				return false;
644
-			}
645
-		}
646
-
647
-		return true;
648
-	}
649
-
650
-	public function test() {
651
-		$this->getConnection()->headBucket([
652
-			'Bucket' => $this->bucket
653
-		]);
654
-		return true;
655
-	}
656
-
657
-	public function getId() {
658
-		return $this->id;
659
-	}
660
-
661
-	public function writeBack($tmpFile, $path) {
662
-		try {
663
-			$source = fopen($tmpFile, 'r');
664
-			$this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
665
-			$this->invalidateCache($path);
666
-
667
-			unlink($tmpFile);
668
-			return true;
669
-		} catch (S3Exception $e) {
670
-			$this->logger->error($e->getMessage(), [
671
-				'app' => 'files_external',
672
-				'exception' => $e,
673
-			]);
674
-			return false;
675
-		}
676
-	}
677
-
678
-	/**
679
-	 * check if curl is installed
680
-	 */
681
-	public static function checkDependencies() {
682
-		return true;
683
-	}
684
-
685
-	public function getDirectoryContent($directory): \Traversable {
686
-		$path = $this->normalizePath($directory);
687
-
688
-		if ($this->isRoot($path)) {
689
-			$path = '';
690
-		} else {
691
-			$path .= '/';
692
-		}
693
-
694
-		$results = $this->getConnection()->getPaginator('ListObjectsV2', [
695
-			'Bucket' => $this->bucket,
696
-			'Delimiter' => '/',
697
-			'Prefix' => $path,
698
-		]);
699
-
700
-		foreach ($results as $result) {
701
-			// sub folders
702
-			if (is_array($result['CommonPrefixes'])) {
703
-				foreach ($result['CommonPrefixes'] as $prefix) {
704
-					$dir = $this->getDirectoryMetaData($prefix['Prefix']);
705
-					if ($dir) {
706
-						yield $dir;
707
-					}
708
-				}
709
-			}
710
-			if (is_array($result['Contents'])) {
711
-				foreach ($result['Contents'] as $object) {
712
-					$this->objectCache[$object['Key']] = $object;
713
-					if ($object['Key'] !== $path) {
714
-						yield $this->objectToMetaData($object);
715
-					}
716
-				}
717
-			}
718
-		}
719
-	}
720
-
721
-	private function objectToMetaData(array $object): array {
722
-		return [
723
-			'name' => basename($object['Key']),
724
-			'mimetype' => $this->mimeDetector->detectPath($object['Key']),
725
-			'mtime' => strtotime($object['LastModified']),
726
-			'storage_mtime' => strtotime($object['LastModified']),
727
-			'etag' => trim($object['ETag'], '"'),
728
-			'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
729
-			'size' => (int)($object['Size'] ?? $object['ContentLength']),
730
-		];
731
-	}
732
-
733
-	private function getDirectoryMetaData(string $path): ?array {
734
-		$path = trim($path, '/');
735
-		// when versioning is enabled, delete markers are returned as part of CommonPrefixes
736
-		// resulting in "ghost" folders, verify that each folder actually exists
737
-		if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
738
-			return null;
739
-		}
740
-		$cacheEntry = $this->getCache()->get($path);
741
-		if ($cacheEntry instanceof CacheEntry) {
742
-			return $cacheEntry->getData();
743
-		} else {
744
-			return [
745
-				'name' => basename($path),
746
-				'mimetype' => FileInfo::MIMETYPE_FOLDER,
747
-				'mtime' => time(),
748
-				'storage_mtime' => time(),
749
-				'etag' => uniqid(),
750
-				'permissions' => Constants::PERMISSION_ALL,
751
-				'size' => -1,
752
-			];
753
-		}
754
-	}
755
-
756
-	public function versioningEnabled(): bool {
757
-		if ($this->versioningEnabled === null) {
758
-			$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
759
-			if ($cached === null) {
760
-				$this->versioningEnabled = $this->getVersioningStatusFromBucket();
761
-				$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
762
-			} else {
763
-				$this->versioningEnabled = $cached;
764
-			}
765
-		}
766
-		return $this->versioningEnabled;
767
-	}
768
-
769
-	protected function getVersioningStatusFromBucket(): bool {
770
-		try {
771
-			$result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
772
-			return $result->get('Status') === 'Enabled';
773
-		} catch (S3Exception $s3Exception) {
774
-			// This is needed for compatibility with Storj gateway which does not support versioning yet
775
-			if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
776
-				return false;
777
-			}
778
-			throw $s3Exception;
779
-		}
780
-	}
781
-
782
-	public function hasUpdated($path, $time) {
783
-		// for files we can get the proper mtime
784
-		if ($path !== '' && $object = $this->headObject($path)) {
785
-			$stat = $this->objectToMetaData($object);
786
-			return $stat['mtime'] > $time;
787
-		} else {
788
-			// for directories, the only real option we have is to do a prefix listing and iterate over all objects
789
-			// however, since this is just as expensive as just re-scanning the directory, we can simply return true
790
-			// and have the scanner figure out if anything has actually changed
791
-			return true;
792
-		}
793
-	}
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
+                    'StorageClass' => $this->storageClass,
588
+                ]);
589
+                $this->testTimeout();
590
+            } catch (S3Exception $e) {
591
+                $this->logger->error($e->getMessage(), [
592
+                    'app' => 'files_external',
593
+                    'exception' => $e,
594
+                ]);
595
+                return false;
596
+            }
597
+        } else {
598
+            $this->remove($target);
599
+
600
+            try {
601
+                $this->mkdir($target);
602
+                $this->testTimeout();
603
+            } catch (S3Exception $e) {
604
+                $this->logger->error($e->getMessage(), [
605
+                    'app' => 'files_external',
606
+                    'exception' => $e,
607
+                ]);
608
+                return false;
609
+            }
610
+
611
+            foreach ($this->getDirectoryContent($source) as $item) {
612
+                $childSource = $source . '/' . $item['name'];
613
+                $childTarget = $target . '/' . $item['name'];
614
+                $this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
615
+            }
616
+        }
617
+
618
+        $this->invalidateCache($target);
619
+
620
+        return true;
621
+    }
622
+
623
+    public function rename($source, $target) {
624
+        $source = $this->normalizePath($source);
625
+        $target = $this->normalizePath($target);
626
+
627
+        if ($this->is_file($source)) {
628
+            if ($this->copy($source, $target) === false) {
629
+                return false;
630
+            }
631
+
632
+            if ($this->unlink($source) === false) {
633
+                $this->unlink($target);
634
+                return false;
635
+            }
636
+        } else {
637
+            if ($this->copy($source, $target) === false) {
638
+                return false;
639
+            }
640
+
641
+            if ($this->rmdir($source) === false) {
642
+                $this->rmdir($target);
643
+                return false;
644
+            }
645
+        }
646
+
647
+        return true;
648
+    }
649
+
650
+    public function test() {
651
+        $this->getConnection()->headBucket([
652
+            'Bucket' => $this->bucket
653
+        ]);
654
+        return true;
655
+    }
656
+
657
+    public function getId() {
658
+        return $this->id;
659
+    }
660
+
661
+    public function writeBack($tmpFile, $path) {
662
+        try {
663
+            $source = fopen($tmpFile, 'r');
664
+            $this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
665
+            $this->invalidateCache($path);
666
+
667
+            unlink($tmpFile);
668
+            return true;
669
+        } catch (S3Exception $e) {
670
+            $this->logger->error($e->getMessage(), [
671
+                'app' => 'files_external',
672
+                'exception' => $e,
673
+            ]);
674
+            return false;
675
+        }
676
+    }
677
+
678
+    /**
679
+     * check if curl is installed
680
+     */
681
+    public static function checkDependencies() {
682
+        return true;
683
+    }
684
+
685
+    public function getDirectoryContent($directory): \Traversable {
686
+        $path = $this->normalizePath($directory);
687
+
688
+        if ($this->isRoot($path)) {
689
+            $path = '';
690
+        } else {
691
+            $path .= '/';
692
+        }
693
+
694
+        $results = $this->getConnection()->getPaginator('ListObjectsV2', [
695
+            'Bucket' => $this->bucket,
696
+            'Delimiter' => '/',
697
+            'Prefix' => $path,
698
+        ]);
699
+
700
+        foreach ($results as $result) {
701
+            // sub folders
702
+            if (is_array($result['CommonPrefixes'])) {
703
+                foreach ($result['CommonPrefixes'] as $prefix) {
704
+                    $dir = $this->getDirectoryMetaData($prefix['Prefix']);
705
+                    if ($dir) {
706
+                        yield $dir;
707
+                    }
708
+                }
709
+            }
710
+            if (is_array($result['Contents'])) {
711
+                foreach ($result['Contents'] as $object) {
712
+                    $this->objectCache[$object['Key']] = $object;
713
+                    if ($object['Key'] !== $path) {
714
+                        yield $this->objectToMetaData($object);
715
+                    }
716
+                }
717
+            }
718
+        }
719
+    }
720
+
721
+    private function objectToMetaData(array $object): array {
722
+        return [
723
+            'name' => basename($object['Key']),
724
+            'mimetype' => $this->mimeDetector->detectPath($object['Key']),
725
+            'mtime' => strtotime($object['LastModified']),
726
+            'storage_mtime' => strtotime($object['LastModified']),
727
+            'etag' => trim($object['ETag'], '"'),
728
+            'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
729
+            'size' => (int)($object['Size'] ?? $object['ContentLength']),
730
+        ];
731
+    }
732
+
733
+    private function getDirectoryMetaData(string $path): ?array {
734
+        $path = trim($path, '/');
735
+        // when versioning is enabled, delete markers are returned as part of CommonPrefixes
736
+        // resulting in "ghost" folders, verify that each folder actually exists
737
+        if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
738
+            return null;
739
+        }
740
+        $cacheEntry = $this->getCache()->get($path);
741
+        if ($cacheEntry instanceof CacheEntry) {
742
+            return $cacheEntry->getData();
743
+        } else {
744
+            return [
745
+                'name' => basename($path),
746
+                'mimetype' => FileInfo::MIMETYPE_FOLDER,
747
+                'mtime' => time(),
748
+                'storage_mtime' => time(),
749
+                'etag' => uniqid(),
750
+                'permissions' => Constants::PERMISSION_ALL,
751
+                'size' => -1,
752
+            ];
753
+        }
754
+    }
755
+
756
+    public function versioningEnabled(): bool {
757
+        if ($this->versioningEnabled === null) {
758
+            $cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
759
+            if ($cached === null) {
760
+                $this->versioningEnabled = $this->getVersioningStatusFromBucket();
761
+                $this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
762
+            } else {
763
+                $this->versioningEnabled = $cached;
764
+            }
765
+        }
766
+        return $this->versioningEnabled;
767
+    }
768
+
769
+    protected function getVersioningStatusFromBucket(): bool {
770
+        try {
771
+            $result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
772
+            return $result->get('Status') === 'Enabled';
773
+        } catch (S3Exception $s3Exception) {
774
+            // This is needed for compatibility with Storj gateway which does not support versioning yet
775
+            if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
776
+                return false;
777
+            }
778
+            throw $s3Exception;
779
+        }
780
+    }
781
+
782
+    public function hasUpdated($path, $time) {
783
+        // for files we can get the proper mtime
784
+        if ($path !== '' && $object = $this->headObject($path)) {
785
+            $stat = $this->objectToMetaData($object);
786
+            return $stat['mtime'] > $time;
787
+        } else {
788
+            // for directories, the only real option we have is to do a prefix listing and iterate over all objects
789
+            // however, since this is just as expensive as just re-scanning the directory, we can simply return true
790
+            // and have the scanner figure out if anything has actually changed
791
+            return true;
792
+        }
793
+    }
794 794
 }
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
 					'StorageClass' => $this->storageClass,
588 588
 				]);
589 589
 				$this->testTimeout();
@@ -609,8 +609,8 @@  discard block
 block discarded – undo
609 609
 			}
610 610
 
611 611
 			foreach ($this->getDirectoryContent($source) as $item) {
612
-				$childSource = $source . '/' . $item['name'];
613
-				$childTarget = $target . '/' . $item['name'];
612
+				$childSource = $source.'/'.$item['name'];
613
+				$childTarget = $target.'/'.$item['name'];
614 614
 				$this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
615 615
 			}
616 616
 		}
@@ -726,7 +726,7 @@  discard block
 block discarded – undo
726 726
 			'storage_mtime' => strtotime($object['LastModified']),
727 727
 			'etag' => trim($object['ETag'], '"'),
728 728
 			'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
729
-			'size' => (int)($object['Size'] ?? $object['ContentLength']),
729
+			'size' => (int) ($object['Size'] ?? $object['ContentLength']),
730 730
 		];
731 731
 	}
732 732
 
@@ -755,10 +755,10 @@  discard block
 block discarded – undo
755 755
 
756 756
 	public function versioningEnabled(): bool {
757 757
 		if ($this->versioningEnabled === null) {
758
-			$cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
758
+			$cached = $this->memCache->get('versioning-enabled::'.$this->getBucket());
759 759
 			if ($cached === null) {
760 760
 				$this->versioningEnabled = $this->getVersioningStatusFromBucket();
761
-				$this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
761
+				$this->memCache->set('versioning-enabled::'.$this->getBucket(), $this->versioningEnabled, 60);
762 762
 			} else {
763 763
 				$this->versioningEnabled = $cached;
764 764
 			}
Please login to merge, or discard this patch.