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