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