Passed
Push — master ( 50b156...e367a3 )
by Roeland
13:35 queued 14s
created
apps/files_external/lib/Lib/Storage/AmazonS3.php 1 patch
Indentation   +647 added lines, -647 removed lines patch added patch discarded remove patch
@@ -50,651 +50,651 @@
 block discarded – undo
50 50
 use OCP\Constants;
51 51
 
52 52
 class AmazonS3 extends \OC\Files\Storage\Common {
53
-	use S3ConnectionTrait;
54
-	use S3ObjectTrait;
55
-
56
-	public function needsPartFile() {
57
-		return false;
58
-	}
59
-
60
-	/** @var CappedMemoryCache|Result[] */
61
-	private $objectCache;
62
-
63
-	/** @var CappedMemoryCache|bool[] */
64
-	private $directoryCache;
65
-
66
-	/** @var CappedMemoryCache|array */
67
-	private $filesCache;
68
-
69
-	public function __construct($parameters) {
70
-		parent::__construct($parameters);
71
-		$this->parseParams($parameters);
72
-		$this->objectCache = new CappedMemoryCache();
73
-		$this->directoryCache = new CappedMemoryCache();
74
-		$this->filesCache = new CappedMemoryCache();
75
-	}
76
-
77
-	/**
78
-	 * @param string $path
79
-	 * @return string correctly encoded path
80
-	 */
81
-	private function normalizePath($path) {
82
-		$path = trim($path, '/');
83
-
84
-		if (!$path) {
85
-			$path = '.';
86
-		}
87
-
88
-		return $path;
89
-	}
90
-
91
-	private function isRoot($path) {
92
-		return $path === '.';
93
-	}
94
-
95
-	private function cleanKey($path) {
96
-		if ($this->isRoot($path)) {
97
-			return '/';
98
-		}
99
-		return $path;
100
-	}
101
-
102
-	private function clearCache() {
103
-		$this->objectCache = new CappedMemoryCache();
104
-		$this->directoryCache = new CappedMemoryCache();
105
-		$this->filesCache = new CappedMemoryCache();
106
-	}
107
-
108
-	private function invalidateCache($key) {
109
-		unset($this->objectCache[$key]);
110
-		$keys = array_keys($this->objectCache->getData());
111
-		$keyLength = strlen($key);
112
-		foreach ($keys as $existingKey) {
113
-			if (substr($existingKey, 0, $keyLength) === $key) {
114
-				unset($this->objectCache[$existingKey]);
115
-			}
116
-		}
117
-		unset($this->directoryCache[$key], $this->filesCache[$key]);
118
-	}
119
-
120
-	/**
121
-	 * @param $key
122
-	 * @return Result|boolean
123
-	 */
124
-	private function headObject($key) {
125
-		if (!isset($this->objectCache[$key])) {
126
-			try {
127
-				$this->objectCache[$key] = $this->getConnection()->headObject([
128
-					'Bucket' => $this->bucket,
129
-					'Key' => $key
130
-				]);
131
-			} catch (S3Exception $e) {
132
-				if ($e->getStatusCode() >= 500) {
133
-					throw $e;
134
-				}
135
-				$this->objectCache[$key] = false;
136
-			}
137
-		}
138
-
139
-		return $this->objectCache[$key];
140
-	}
141
-
142
-	/**
143
-	 * Return true if directory exists
144
-	 *
145
-	 * There are no folders in s3. A folder like structure could be archived
146
-	 * by prefixing files with the folder name.
147
-	 *
148
-	 * Implementation from flysystem-aws-s3-v3:
149
-	 * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
150
-	 *
151
-	 * @param $path
152
-	 * @return bool
153
-	 * @throws \Exception
154
-	 */
155
-	private function doesDirectoryExist($path) {
156
-		if (!isset($this->directoryCache[$path])) {
157
-			// Maybe this isn't an actual key, but a prefix.
158
-			// Do a prefix listing of objects to determine.
159
-			try {
160
-				$result = $this->getConnection()->listObjects([
161
-					'Bucket' => $this->bucket,
162
-					'Prefix' => rtrim($path, '/') . '/',
163
-					'MaxKeys' => 1,
164
-				]);
165
-				$this->directoryCache[$path] = $result['Contents'] || $result['CommonPrefixes'];
166
-			} catch (S3Exception $e) {
167
-				if ($e->getStatusCode() === 403) {
168
-					$this->directoryCache[$path] = false;
169
-				}
170
-				throw $e;
171
-			}
172
-		}
173
-
174
-		return $this->directoryCache[$path];
175
-	}
176
-
177
-	/**
178
-	 * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name.
179
-	 * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home
180
-	 *
181
-	 * @param array $params
182
-	 */
183
-	public function updateLegacyId(array $params) {
184
-		$oldId = 'amazon::' . $params['key'] . md5($params['secret']);
185
-
186
-		// find by old id or bucket
187
-		$stmt = \OC::$server->getDatabaseConnection()->prepare(
188
-			'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)'
189
-		);
190
-		$stmt->execute([$oldId, $this->id]);
191
-		while ($row = $stmt->fetch()) {
192
-			$storages[$row['id']] = $row['numeric_id'];
193
-		}
194
-
195
-		if (isset($storages[$this->id]) && isset($storages[$oldId])) {
196
-			// if both ids exist, delete the old storage and corresponding filecache entries
197
-			\OC\Files\Cache\Storage::remove($oldId);
198
-		} elseif (isset($storages[$oldId])) {
199
-			// if only the old id exists do an update
200
-			$stmt = \OC::$server->getDatabaseConnection()->prepare(
201
-				'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?'
202
-			);
203
-			$stmt->execute([$this->id, $oldId]);
204
-		}
205
-		// only the bucket based id may exist, do nothing
206
-	}
207
-
208
-	/**
209
-	 * Remove a file or folder
210
-	 *
211
-	 * @param string $path
212
-	 * @return bool
213
-	 */
214
-	protected function remove($path) {
215
-		// remember fileType to reduce http calls
216
-		$fileType = $this->filetype($path);
217
-		if ($fileType === 'dir') {
218
-			return $this->rmdir($path);
219
-		} elseif ($fileType === 'file') {
220
-			return $this->unlink($path);
221
-		} else {
222
-			return false;
223
-		}
224
-	}
225
-
226
-	public function mkdir($path) {
227
-		$path = $this->normalizePath($path);
228
-
229
-		if ($this->is_dir($path)) {
230
-			return false;
231
-		}
232
-
233
-		try {
234
-			$this->getConnection()->putObject([
235
-				'Bucket' => $this->bucket,
236
-				'Key' => $path . '/',
237
-				'Body' => '',
238
-				'ContentType' => 'httpd/unix-directory'
239
-			]);
240
-			$this->testTimeout();
241
-		} catch (S3Exception $e) {
242
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
243
-			return false;
244
-		}
245
-
246
-		$this->invalidateCache($path);
247
-
248
-		return true;
249
-	}
250
-
251
-	public function file_exists($path) {
252
-		return $this->filetype($path) !== false;
253
-	}
254
-
255
-
256
-	public function rmdir($path) {
257
-		$path = $this->normalizePath($path);
258
-
259
-		if ($this->isRoot($path)) {
260
-			return $this->clearBucket();
261
-		}
262
-
263
-		if (!$this->file_exists($path)) {
264
-			return false;
265
-		}
266
-
267
-		$this->invalidateCache($path);
268
-		return $this->batchDelete($path);
269
-	}
270
-
271
-	protected function clearBucket() {
272
-		$this->clearCache();
273
-		try {
274
-			$this->getConnection()->clearBucket($this->bucket);
275
-			return true;
276
-			// clearBucket() is not working with Ceph, so if it fails we try the slower approach
277
-		} catch (\Exception $e) {
278
-			return $this->batchDelete();
279
-		}
280
-	}
281
-
282
-	private function batchDelete($path = null) {
283
-		$params = [
284
-			'Bucket' => $this->bucket
285
-		];
286
-		if ($path !== null) {
287
-			$params['Prefix'] = $path . '/';
288
-		}
289
-		try {
290
-			$connection = $this->getConnection();
291
-			// Since there are no real directories on S3, we need
292
-			// to delete all objects prefixed with the path.
293
-			do {
294
-				// instead of the iterator, manually loop over the list ...
295
-				$objects = $connection->listObjects($params);
296
-				// ... so we can delete the files in batches
297
-				if (isset($objects['Contents'])) {
298
-					$connection->deleteObjects([
299
-						'Bucket' => $this->bucket,
300
-						'Delete' => [
301
-							'Objects' => $objects['Contents']
302
-						]
303
-					]);
304
-					$this->testTimeout();
305
-				}
306
-				// we reached the end when the list is no longer truncated
307
-			} while ($objects['IsTruncated']);
308
-		} catch (S3Exception $e) {
309
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
310
-			return false;
311
-		}
312
-		return true;
313
-	}
314
-
315
-	public function opendir($path) {
316
-		$path = $this->normalizePath($path);
317
-
318
-		if ($this->isRoot($path)) {
319
-			$path = '';
320
-		} else {
321
-			$path .= '/';
322
-		}
323
-
324
-		try {
325
-			$files = [];
326
-			$results = $this->getConnection()->getPaginator('ListObjects', [
327
-				'Bucket' => $this->bucket,
328
-				'Delimiter' => '/',
329
-				'Prefix' => $path,
330
-			]);
331
-
332
-			foreach ($results as $result) {
333
-				// sub folders
334
-				if (is_array($result['CommonPrefixes'])) {
335
-					foreach ($result['CommonPrefixes'] as $prefix) {
336
-						$directoryName = trim($prefix['Prefix'], '/');
337
-						$files[] = substr($directoryName, strlen($path));
338
-						$this->directoryCache[$directoryName] = true;
339
-					}
340
-				}
341
-				if (is_array($result['Contents'])) {
342
-					foreach ($result['Contents'] as $object) {
343
-						if (isset($object['Key']) && $object['Key'] === $path) {
344
-							// it's the directory itself, skip
345
-							continue;
346
-						}
347
-						$file = basename(
348
-							isset($object['Key']) ? $object['Key'] : $object['Prefix']
349
-						);
350
-						$files[] = $file;
351
-
352
-						// store this information for later usage
353
-						$this->filesCache[$path . $file] = [
354
-							'ContentLength' => $object['Size'],
355
-							'LastModified' => (string)$object['LastModified'],
356
-						];
357
-					}
358
-				}
359
-			}
360
-
361
-			return IteratorDirectory::wrap($files);
362
-		} catch (S3Exception $e) {
363
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
364
-			return false;
365
-		}
366
-	}
367
-
368
-	public function stat($path) {
369
-		$path = $this->normalizePath($path);
370
-
371
-		try {
372
-			$stat = [];
373
-			if ($this->is_dir($path)) {
374
-				//folders don't really exist
375
-				$stat['size'] = -1; //unknown
376
-				$stat['mtime'] = time();
377
-			} else {
378
-				$stat['size'] = $this->getContentLength($path);
379
-				$stat['mtime'] = strtotime($this->getLastModified($path));
380
-			}
381
-			$stat['atime'] = time();
382
-
383
-			return $stat;
384
-		} catch (S3Exception $e) {
385
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
386
-			return false;
387
-		}
388
-	}
389
-
390
-	/**
391
-	 * Return content length for object
392
-	 *
393
-	 * When the information is already present (e.g. opendir has been called before)
394
-	 * this value is return. Otherwise a headObject is emitted.
395
-	 *
396
-	 * @param $path
397
-	 * @return int|mixed
398
-	 */
399
-	private function getContentLength($path) {
400
-		if (isset($this->filesCache[$path])) {
401
-			return $this->filesCache[$path]['ContentLength'];
402
-		}
403
-
404
-		$result = $this->headObject($path);
405
-		if (isset($result['ContentLength'])) {
406
-			return $result['ContentLength'];
407
-		}
408
-
409
-		return 0;
410
-	}
411
-
412
-	/**
413
-	 * Return last modified for object
414
-	 *
415
-	 * When the information is already present (e.g. opendir has been called before)
416
-	 * this value is return. Otherwise a headObject is emitted.
417
-	 *
418
-	 * @param $path
419
-	 * @return mixed|string
420
-	 */
421
-	private function getLastModified($path) {
422
-		if (isset($this->filesCache[$path])) {
423
-			return $this->filesCache[$path]['LastModified'];
424
-		}
425
-
426
-		$result = $this->headObject($path);
427
-		if (isset($result['LastModified'])) {
428
-			return $result['LastModified'];
429
-		}
430
-
431
-		return 'now';
432
-	}
433
-
434
-	public function is_dir($path) {
435
-		$path = $this->normalizePath($path);
436
-
437
-		if (isset($this->filesCache[$path])) {
438
-			return false;
439
-		}
440
-
441
-		try {
442
-			return $this->isRoot($path) || $this->doesDirectoryExist($path);
443
-		} catch (S3Exception $e) {
444
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
445
-			return false;
446
-		}
447
-	}
448
-
449
-	public function filetype($path) {
450
-		$path = $this->normalizePath($path);
451
-
452
-		if ($this->isRoot($path)) {
453
-			return 'dir';
454
-		}
455
-
456
-		try {
457
-			if (isset($this->filesCache[$path]) || $this->headObject($path)) {
458
-				return 'file';
459
-			}
460
-			if ($this->doesDirectoryExist($path)) {
461
-				return 'dir';
462
-			}
463
-		} catch (S3Exception $e) {
464
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
465
-			return false;
466
-		}
467
-
468
-		return false;
469
-	}
470
-
471
-	public function getPermissions($path) {
472
-		$type = $this->filetype($path);
473
-		if (!$type) {
474
-			return 0;
475
-		}
476
-		return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
477
-	}
478
-
479
-	public function unlink($path) {
480
-		$path = $this->normalizePath($path);
481
-
482
-		if ($this->is_dir($path)) {
483
-			return $this->rmdir($path);
484
-		}
485
-
486
-		try {
487
-			$this->deleteObject($path);
488
-			$this->invalidateCache($path);
489
-		} catch (S3Exception $e) {
490
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
491
-			return false;
492
-		}
493
-
494
-		return true;
495
-	}
496
-
497
-	public function fopen($path, $mode) {
498
-		$path = $this->normalizePath($path);
499
-
500
-		switch ($mode) {
501
-			case 'r':
502
-			case 'rb':
503
-				try {
504
-					return $this->readObject($path);
505
-				} catch (S3Exception $e) {
506
-					\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
507
-					return false;
508
-				}
509
-			case 'w':
510
-			case 'wb':
511
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
512
-
513
-				$handle = fopen($tmpFile, 'w');
514
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
515
-					$this->writeBack($tmpFile, $path);
516
-				});
517
-			case 'a':
518
-			case 'ab':
519
-			case 'r+':
520
-			case 'w+':
521
-			case 'wb+':
522
-			case 'a+':
523
-			case 'x':
524
-			case 'x+':
525
-			case 'c':
526
-			case 'c+':
527
-				if (strrpos($path, '.') !== false) {
528
-					$ext = substr($path, strrpos($path, '.'));
529
-				} else {
530
-					$ext = '';
531
-				}
532
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
533
-				if ($this->file_exists($path)) {
534
-					$source = $this->readObject($path);
535
-					file_put_contents($tmpFile, $source);
536
-				}
537
-
538
-				$handle = fopen($tmpFile, $mode);
539
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
540
-					$this->writeBack($tmpFile, $path);
541
-				});
542
-		}
543
-		return false;
544
-	}
545
-
546
-	public function touch($path, $mtime = null) {
547
-		$path = $this->normalizePath($path);
548
-
549
-		$metadata = [];
550
-		if (is_null($mtime)) {
551
-			$mtime = time();
552
-		}
553
-		$metadata = [
554
-			'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
555
-		];
556
-
557
-		$fileType = $this->filetype($path);
558
-		try {
559
-			if ($fileType !== false) {
560
-				if ($fileType === 'dir' && !$this->isRoot($path)) {
561
-					$path .= '/';
562
-				}
563
-				$this->getConnection()->copyObject([
564
-					'Bucket' => $this->bucket,
565
-					'Key' => $this->cleanKey($path),
566
-					'Metadata' => $metadata,
567
-					'CopySource' => $this->bucket . '/' . $path,
568
-					'MetadataDirective' => 'REPLACE',
569
-				]);
570
-				$this->testTimeout();
571
-			} else {
572
-				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
573
-				$this->getConnection()->putObject([
574
-					'Bucket' => $this->bucket,
575
-					'Key' => $this->cleanKey($path),
576
-					'Metadata' => $metadata,
577
-					'Body' => '',
578
-					'ContentType' => $mimeType,
579
-					'MetadataDirective' => 'REPLACE',
580
-				]);
581
-				$this->testTimeout();
582
-			}
583
-		} catch (S3Exception $e) {
584
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
585
-			return false;
586
-		}
587
-
588
-		$this->invalidateCache($path);
589
-		return true;
590
-	}
591
-
592
-	public function copy($path1, $path2) {
593
-		$path1 = $this->normalizePath($path1);
594
-		$path2 = $this->normalizePath($path2);
595
-
596
-		if ($this->is_file($path1)) {
597
-			try {
598
-				$this->getConnection()->copyObject([
599
-					'Bucket' => $this->bucket,
600
-					'Key' => $this->cleanKey($path2),
601
-					'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
602
-				]);
603
-				$this->testTimeout();
604
-			} catch (S3Exception $e) {
605
-				\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
606
-				return false;
607
-			}
608
-		} else {
609
-			$this->remove($path2);
610
-
611
-			try {
612
-				$this->getConnection()->copyObject([
613
-					'Bucket' => $this->bucket,
614
-					'Key' => $path2 . '/',
615
-					'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/')
616
-				]);
617
-				$this->testTimeout();
618
-			} catch (S3Exception $e) {
619
-				\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
620
-				return false;
621
-			}
622
-
623
-			$dh = $this->opendir($path1);
624
-			if (is_resource($dh)) {
625
-				while (($file = readdir($dh)) !== false) {
626
-					if (\OC\Files\Filesystem::isIgnoredDir($file)) {
627
-						continue;
628
-					}
629
-
630
-					$source = $path1 . '/' . $file;
631
-					$target = $path2 . '/' . $file;
632
-					$this->copy($source, $target);
633
-				}
634
-			}
635
-		}
636
-
637
-		$this->invalidateCache($path2);
638
-
639
-		return true;
640
-	}
641
-
642
-	public function rename($path1, $path2) {
643
-		$path1 = $this->normalizePath($path1);
644
-		$path2 = $this->normalizePath($path2);
645
-
646
-		if ($this->is_file($path1)) {
647
-			if ($this->copy($path1, $path2) === false) {
648
-				return false;
649
-			}
650
-
651
-			if ($this->unlink($path1) === false) {
652
-				$this->unlink($path2);
653
-				return false;
654
-			}
655
-		} else {
656
-			if ($this->copy($path1, $path2) === false) {
657
-				return false;
658
-			}
659
-
660
-			if ($this->rmdir($path1) === false) {
661
-				$this->rmdir($path2);
662
-				return false;
663
-			}
664
-		}
665
-
666
-		return true;
667
-	}
668
-
669
-	public function test() {
670
-		$this->getConnection()->headBucket([
671
-			'Bucket' => $this->bucket
672
-		]);
673
-		return true;
674
-	}
675
-
676
-	public function getId() {
677
-		return $this->id;
678
-	}
679
-
680
-	public function writeBack($tmpFile, $path) {
681
-		try {
682
-			$source = fopen($tmpFile, 'r');
683
-			$this->writeObject($path, $source);
684
-			$this->invalidateCache($path);
685
-
686
-			unlink($tmpFile);
687
-			return true;
688
-		} catch (S3Exception $e) {
689
-			\OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
690
-			return false;
691
-		}
692
-	}
693
-
694
-	/**
695
-	 * check if curl is installed
696
-	 */
697
-	public static function checkDependencies() {
698
-		return true;
699
-	}
53
+    use S3ConnectionTrait;
54
+    use S3ObjectTrait;
55
+
56
+    public function needsPartFile() {
57
+        return false;
58
+    }
59
+
60
+    /** @var CappedMemoryCache|Result[] */
61
+    private $objectCache;
62
+
63
+    /** @var CappedMemoryCache|bool[] */
64
+    private $directoryCache;
65
+
66
+    /** @var CappedMemoryCache|array */
67
+    private $filesCache;
68
+
69
+    public function __construct($parameters) {
70
+        parent::__construct($parameters);
71
+        $this->parseParams($parameters);
72
+        $this->objectCache = new CappedMemoryCache();
73
+        $this->directoryCache = new CappedMemoryCache();
74
+        $this->filesCache = new CappedMemoryCache();
75
+    }
76
+
77
+    /**
78
+     * @param string $path
79
+     * @return string correctly encoded path
80
+     */
81
+    private function normalizePath($path) {
82
+        $path = trim($path, '/');
83
+
84
+        if (!$path) {
85
+            $path = '.';
86
+        }
87
+
88
+        return $path;
89
+    }
90
+
91
+    private function isRoot($path) {
92
+        return $path === '.';
93
+    }
94
+
95
+    private function cleanKey($path) {
96
+        if ($this->isRoot($path)) {
97
+            return '/';
98
+        }
99
+        return $path;
100
+    }
101
+
102
+    private function clearCache() {
103
+        $this->objectCache = new CappedMemoryCache();
104
+        $this->directoryCache = new CappedMemoryCache();
105
+        $this->filesCache = new CappedMemoryCache();
106
+    }
107
+
108
+    private function invalidateCache($key) {
109
+        unset($this->objectCache[$key]);
110
+        $keys = array_keys($this->objectCache->getData());
111
+        $keyLength = strlen($key);
112
+        foreach ($keys as $existingKey) {
113
+            if (substr($existingKey, 0, $keyLength) === $key) {
114
+                unset($this->objectCache[$existingKey]);
115
+            }
116
+        }
117
+        unset($this->directoryCache[$key], $this->filesCache[$key]);
118
+    }
119
+
120
+    /**
121
+     * @param $key
122
+     * @return Result|boolean
123
+     */
124
+    private function headObject($key) {
125
+        if (!isset($this->objectCache[$key])) {
126
+            try {
127
+                $this->objectCache[$key] = $this->getConnection()->headObject([
128
+                    'Bucket' => $this->bucket,
129
+                    'Key' => $key
130
+                ]);
131
+            } catch (S3Exception $e) {
132
+                if ($e->getStatusCode() >= 500) {
133
+                    throw $e;
134
+                }
135
+                $this->objectCache[$key] = false;
136
+            }
137
+        }
138
+
139
+        return $this->objectCache[$key];
140
+    }
141
+
142
+    /**
143
+     * Return true if directory exists
144
+     *
145
+     * There are no folders in s3. A folder like structure could be archived
146
+     * by prefixing files with the folder name.
147
+     *
148
+     * Implementation from flysystem-aws-s3-v3:
149
+     * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
150
+     *
151
+     * @param $path
152
+     * @return bool
153
+     * @throws \Exception
154
+     */
155
+    private function doesDirectoryExist($path) {
156
+        if (!isset($this->directoryCache[$path])) {
157
+            // Maybe this isn't an actual key, but a prefix.
158
+            // Do a prefix listing of objects to determine.
159
+            try {
160
+                $result = $this->getConnection()->listObjects([
161
+                    'Bucket' => $this->bucket,
162
+                    'Prefix' => rtrim($path, '/') . '/',
163
+                    'MaxKeys' => 1,
164
+                ]);
165
+                $this->directoryCache[$path] = $result['Contents'] || $result['CommonPrefixes'];
166
+            } catch (S3Exception $e) {
167
+                if ($e->getStatusCode() === 403) {
168
+                    $this->directoryCache[$path] = false;
169
+                }
170
+                throw $e;
171
+            }
172
+        }
173
+
174
+        return $this->directoryCache[$path];
175
+    }
176
+
177
+    /**
178
+     * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name.
179
+     * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home
180
+     *
181
+     * @param array $params
182
+     */
183
+    public function updateLegacyId(array $params) {
184
+        $oldId = 'amazon::' . $params['key'] . md5($params['secret']);
185
+
186
+        // find by old id or bucket
187
+        $stmt = \OC::$server->getDatabaseConnection()->prepare(
188
+            'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)'
189
+        );
190
+        $stmt->execute([$oldId, $this->id]);
191
+        while ($row = $stmt->fetch()) {
192
+            $storages[$row['id']] = $row['numeric_id'];
193
+        }
194
+
195
+        if (isset($storages[$this->id]) && isset($storages[$oldId])) {
196
+            // if both ids exist, delete the old storage and corresponding filecache entries
197
+            \OC\Files\Cache\Storage::remove($oldId);
198
+        } elseif (isset($storages[$oldId])) {
199
+            // if only the old id exists do an update
200
+            $stmt = \OC::$server->getDatabaseConnection()->prepare(
201
+                'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?'
202
+            );
203
+            $stmt->execute([$this->id, $oldId]);
204
+        }
205
+        // only the bucket based id may exist, do nothing
206
+    }
207
+
208
+    /**
209
+     * Remove a file or folder
210
+     *
211
+     * @param string $path
212
+     * @return bool
213
+     */
214
+    protected function remove($path) {
215
+        // remember fileType to reduce http calls
216
+        $fileType = $this->filetype($path);
217
+        if ($fileType === 'dir') {
218
+            return $this->rmdir($path);
219
+        } elseif ($fileType === 'file') {
220
+            return $this->unlink($path);
221
+        } else {
222
+            return false;
223
+        }
224
+    }
225
+
226
+    public function mkdir($path) {
227
+        $path = $this->normalizePath($path);
228
+
229
+        if ($this->is_dir($path)) {
230
+            return false;
231
+        }
232
+
233
+        try {
234
+            $this->getConnection()->putObject([
235
+                'Bucket' => $this->bucket,
236
+                'Key' => $path . '/',
237
+                'Body' => '',
238
+                'ContentType' => 'httpd/unix-directory'
239
+            ]);
240
+            $this->testTimeout();
241
+        } catch (S3Exception $e) {
242
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
243
+            return false;
244
+        }
245
+
246
+        $this->invalidateCache($path);
247
+
248
+        return true;
249
+    }
250
+
251
+    public function file_exists($path) {
252
+        return $this->filetype($path) !== false;
253
+    }
254
+
255
+
256
+    public function rmdir($path) {
257
+        $path = $this->normalizePath($path);
258
+
259
+        if ($this->isRoot($path)) {
260
+            return $this->clearBucket();
261
+        }
262
+
263
+        if (!$this->file_exists($path)) {
264
+            return false;
265
+        }
266
+
267
+        $this->invalidateCache($path);
268
+        return $this->batchDelete($path);
269
+    }
270
+
271
+    protected function clearBucket() {
272
+        $this->clearCache();
273
+        try {
274
+            $this->getConnection()->clearBucket($this->bucket);
275
+            return true;
276
+            // clearBucket() is not working with Ceph, so if it fails we try the slower approach
277
+        } catch (\Exception $e) {
278
+            return $this->batchDelete();
279
+        }
280
+    }
281
+
282
+    private function batchDelete($path = null) {
283
+        $params = [
284
+            'Bucket' => $this->bucket
285
+        ];
286
+        if ($path !== null) {
287
+            $params['Prefix'] = $path . '/';
288
+        }
289
+        try {
290
+            $connection = $this->getConnection();
291
+            // Since there are no real directories on S3, we need
292
+            // to delete all objects prefixed with the path.
293
+            do {
294
+                // instead of the iterator, manually loop over the list ...
295
+                $objects = $connection->listObjects($params);
296
+                // ... so we can delete the files in batches
297
+                if (isset($objects['Contents'])) {
298
+                    $connection->deleteObjects([
299
+                        'Bucket' => $this->bucket,
300
+                        'Delete' => [
301
+                            'Objects' => $objects['Contents']
302
+                        ]
303
+                    ]);
304
+                    $this->testTimeout();
305
+                }
306
+                // we reached the end when the list is no longer truncated
307
+            } while ($objects['IsTruncated']);
308
+        } catch (S3Exception $e) {
309
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
310
+            return false;
311
+        }
312
+        return true;
313
+    }
314
+
315
+    public function opendir($path) {
316
+        $path = $this->normalizePath($path);
317
+
318
+        if ($this->isRoot($path)) {
319
+            $path = '';
320
+        } else {
321
+            $path .= '/';
322
+        }
323
+
324
+        try {
325
+            $files = [];
326
+            $results = $this->getConnection()->getPaginator('ListObjects', [
327
+                'Bucket' => $this->bucket,
328
+                'Delimiter' => '/',
329
+                'Prefix' => $path,
330
+            ]);
331
+
332
+            foreach ($results as $result) {
333
+                // sub folders
334
+                if (is_array($result['CommonPrefixes'])) {
335
+                    foreach ($result['CommonPrefixes'] as $prefix) {
336
+                        $directoryName = trim($prefix['Prefix'], '/');
337
+                        $files[] = substr($directoryName, strlen($path));
338
+                        $this->directoryCache[$directoryName] = true;
339
+                    }
340
+                }
341
+                if (is_array($result['Contents'])) {
342
+                    foreach ($result['Contents'] as $object) {
343
+                        if (isset($object['Key']) && $object['Key'] === $path) {
344
+                            // it's the directory itself, skip
345
+                            continue;
346
+                        }
347
+                        $file = basename(
348
+                            isset($object['Key']) ? $object['Key'] : $object['Prefix']
349
+                        );
350
+                        $files[] = $file;
351
+
352
+                        // store this information for later usage
353
+                        $this->filesCache[$path . $file] = [
354
+                            'ContentLength' => $object['Size'],
355
+                            'LastModified' => (string)$object['LastModified'],
356
+                        ];
357
+                    }
358
+                }
359
+            }
360
+
361
+            return IteratorDirectory::wrap($files);
362
+        } catch (S3Exception $e) {
363
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
364
+            return false;
365
+        }
366
+    }
367
+
368
+    public function stat($path) {
369
+        $path = $this->normalizePath($path);
370
+
371
+        try {
372
+            $stat = [];
373
+            if ($this->is_dir($path)) {
374
+                //folders don't really exist
375
+                $stat['size'] = -1; //unknown
376
+                $stat['mtime'] = time();
377
+            } else {
378
+                $stat['size'] = $this->getContentLength($path);
379
+                $stat['mtime'] = strtotime($this->getLastModified($path));
380
+            }
381
+            $stat['atime'] = time();
382
+
383
+            return $stat;
384
+        } catch (S3Exception $e) {
385
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
386
+            return false;
387
+        }
388
+    }
389
+
390
+    /**
391
+     * Return content length for object
392
+     *
393
+     * When the information is already present (e.g. opendir has been called before)
394
+     * this value is return. Otherwise a headObject is emitted.
395
+     *
396
+     * @param $path
397
+     * @return int|mixed
398
+     */
399
+    private function getContentLength($path) {
400
+        if (isset($this->filesCache[$path])) {
401
+            return $this->filesCache[$path]['ContentLength'];
402
+        }
403
+
404
+        $result = $this->headObject($path);
405
+        if (isset($result['ContentLength'])) {
406
+            return $result['ContentLength'];
407
+        }
408
+
409
+        return 0;
410
+    }
411
+
412
+    /**
413
+     * Return last modified for object
414
+     *
415
+     * When the information is already present (e.g. opendir has been called before)
416
+     * this value is return. Otherwise a headObject is emitted.
417
+     *
418
+     * @param $path
419
+     * @return mixed|string
420
+     */
421
+    private function getLastModified($path) {
422
+        if (isset($this->filesCache[$path])) {
423
+            return $this->filesCache[$path]['LastModified'];
424
+        }
425
+
426
+        $result = $this->headObject($path);
427
+        if (isset($result['LastModified'])) {
428
+            return $result['LastModified'];
429
+        }
430
+
431
+        return 'now';
432
+    }
433
+
434
+    public function is_dir($path) {
435
+        $path = $this->normalizePath($path);
436
+
437
+        if (isset($this->filesCache[$path])) {
438
+            return false;
439
+        }
440
+
441
+        try {
442
+            return $this->isRoot($path) || $this->doesDirectoryExist($path);
443
+        } catch (S3Exception $e) {
444
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
445
+            return false;
446
+        }
447
+    }
448
+
449
+    public function filetype($path) {
450
+        $path = $this->normalizePath($path);
451
+
452
+        if ($this->isRoot($path)) {
453
+            return 'dir';
454
+        }
455
+
456
+        try {
457
+            if (isset($this->filesCache[$path]) || $this->headObject($path)) {
458
+                return 'file';
459
+            }
460
+            if ($this->doesDirectoryExist($path)) {
461
+                return 'dir';
462
+            }
463
+        } catch (S3Exception $e) {
464
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
465
+            return false;
466
+        }
467
+
468
+        return false;
469
+    }
470
+
471
+    public function getPermissions($path) {
472
+        $type = $this->filetype($path);
473
+        if (!$type) {
474
+            return 0;
475
+        }
476
+        return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
477
+    }
478
+
479
+    public function unlink($path) {
480
+        $path = $this->normalizePath($path);
481
+
482
+        if ($this->is_dir($path)) {
483
+            return $this->rmdir($path);
484
+        }
485
+
486
+        try {
487
+            $this->deleteObject($path);
488
+            $this->invalidateCache($path);
489
+        } catch (S3Exception $e) {
490
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
491
+            return false;
492
+        }
493
+
494
+        return true;
495
+    }
496
+
497
+    public function fopen($path, $mode) {
498
+        $path = $this->normalizePath($path);
499
+
500
+        switch ($mode) {
501
+            case 'r':
502
+            case 'rb':
503
+                try {
504
+                    return $this->readObject($path);
505
+                } catch (S3Exception $e) {
506
+                    \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
507
+                    return false;
508
+                }
509
+            case 'w':
510
+            case 'wb':
511
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
512
+
513
+                $handle = fopen($tmpFile, 'w');
514
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
515
+                    $this->writeBack($tmpFile, $path);
516
+                });
517
+            case 'a':
518
+            case 'ab':
519
+            case 'r+':
520
+            case 'w+':
521
+            case 'wb+':
522
+            case 'a+':
523
+            case 'x':
524
+            case 'x+':
525
+            case 'c':
526
+            case 'c+':
527
+                if (strrpos($path, '.') !== false) {
528
+                    $ext = substr($path, strrpos($path, '.'));
529
+                } else {
530
+                    $ext = '';
531
+                }
532
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
533
+                if ($this->file_exists($path)) {
534
+                    $source = $this->readObject($path);
535
+                    file_put_contents($tmpFile, $source);
536
+                }
537
+
538
+                $handle = fopen($tmpFile, $mode);
539
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
540
+                    $this->writeBack($tmpFile, $path);
541
+                });
542
+        }
543
+        return false;
544
+    }
545
+
546
+    public function touch($path, $mtime = null) {
547
+        $path = $this->normalizePath($path);
548
+
549
+        $metadata = [];
550
+        if (is_null($mtime)) {
551
+            $mtime = time();
552
+        }
553
+        $metadata = [
554
+            'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
555
+        ];
556
+
557
+        $fileType = $this->filetype($path);
558
+        try {
559
+            if ($fileType !== false) {
560
+                if ($fileType === 'dir' && !$this->isRoot($path)) {
561
+                    $path .= '/';
562
+                }
563
+                $this->getConnection()->copyObject([
564
+                    'Bucket' => $this->bucket,
565
+                    'Key' => $this->cleanKey($path),
566
+                    'Metadata' => $metadata,
567
+                    'CopySource' => $this->bucket . '/' . $path,
568
+                    'MetadataDirective' => 'REPLACE',
569
+                ]);
570
+                $this->testTimeout();
571
+            } else {
572
+                $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
573
+                $this->getConnection()->putObject([
574
+                    'Bucket' => $this->bucket,
575
+                    'Key' => $this->cleanKey($path),
576
+                    'Metadata' => $metadata,
577
+                    'Body' => '',
578
+                    'ContentType' => $mimeType,
579
+                    'MetadataDirective' => 'REPLACE',
580
+                ]);
581
+                $this->testTimeout();
582
+            }
583
+        } catch (S3Exception $e) {
584
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
585
+            return false;
586
+        }
587
+
588
+        $this->invalidateCache($path);
589
+        return true;
590
+    }
591
+
592
+    public function copy($path1, $path2) {
593
+        $path1 = $this->normalizePath($path1);
594
+        $path2 = $this->normalizePath($path2);
595
+
596
+        if ($this->is_file($path1)) {
597
+            try {
598
+                $this->getConnection()->copyObject([
599
+                    'Bucket' => $this->bucket,
600
+                    'Key' => $this->cleanKey($path2),
601
+                    'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
602
+                ]);
603
+                $this->testTimeout();
604
+            } catch (S3Exception $e) {
605
+                \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
606
+                return false;
607
+            }
608
+        } else {
609
+            $this->remove($path2);
610
+
611
+            try {
612
+                $this->getConnection()->copyObject([
613
+                    'Bucket' => $this->bucket,
614
+                    'Key' => $path2 . '/',
615
+                    'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/')
616
+                ]);
617
+                $this->testTimeout();
618
+            } catch (S3Exception $e) {
619
+                \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
620
+                return false;
621
+            }
622
+
623
+            $dh = $this->opendir($path1);
624
+            if (is_resource($dh)) {
625
+                while (($file = readdir($dh)) !== false) {
626
+                    if (\OC\Files\Filesystem::isIgnoredDir($file)) {
627
+                        continue;
628
+                    }
629
+
630
+                    $source = $path1 . '/' . $file;
631
+                    $target = $path2 . '/' . $file;
632
+                    $this->copy($source, $target);
633
+                }
634
+            }
635
+        }
636
+
637
+        $this->invalidateCache($path2);
638
+
639
+        return true;
640
+    }
641
+
642
+    public function rename($path1, $path2) {
643
+        $path1 = $this->normalizePath($path1);
644
+        $path2 = $this->normalizePath($path2);
645
+
646
+        if ($this->is_file($path1)) {
647
+            if ($this->copy($path1, $path2) === false) {
648
+                return false;
649
+            }
650
+
651
+            if ($this->unlink($path1) === false) {
652
+                $this->unlink($path2);
653
+                return false;
654
+            }
655
+        } else {
656
+            if ($this->copy($path1, $path2) === false) {
657
+                return false;
658
+            }
659
+
660
+            if ($this->rmdir($path1) === false) {
661
+                $this->rmdir($path2);
662
+                return false;
663
+            }
664
+        }
665
+
666
+        return true;
667
+    }
668
+
669
+    public function test() {
670
+        $this->getConnection()->headBucket([
671
+            'Bucket' => $this->bucket
672
+        ]);
673
+        return true;
674
+    }
675
+
676
+    public function getId() {
677
+        return $this->id;
678
+    }
679
+
680
+    public function writeBack($tmpFile, $path) {
681
+        try {
682
+            $source = fopen($tmpFile, 'r');
683
+            $this->writeObject($path, $source);
684
+            $this->invalidateCache($path);
685
+
686
+            unlink($tmpFile);
687
+            return true;
688
+        } catch (S3Exception $e) {
689
+            \OC::$server->getLogger()->logException($e, ['app' => 'files_external']);
690
+            return false;
691
+        }
692
+    }
693
+
694
+    /**
695
+     * check if curl is installed
696
+     */
697
+    public static function checkDependencies() {
698
+        return true;
699
+    }
700 700
 }
Please login to merge, or discard this patch.