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