AmazonS3.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author André Gaul <gaul@web-yard.de>
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bart Visscher <bartv@thisnet.nl>
  8. * @author Christian Berendt <berendt@b1-systems.de>
  9. * @author Christopher T. Johnson <ctjctj@gmail.com>
  10. * @author Johan Björk <johanimon@gmail.com>
  11. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  12. * @author Martin Mattel <martin.mattel@diemattels.at>
  13. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  14. * @author Morris Jobke <hey@morrisjobke.de>
  15. * @author Philipp Kapfer <philipp.kapfer@gmx.at>
  16. * @author Robin Appelman <robin@icewind.nl>
  17. * @author Robin McCorkell <robin@mccorkell.me.uk>
  18. * @author Thomas Müller <thomas.mueller@tmit.eu>
  19. * @author Vincent Petry <pvince81@owncloud.com>
  20. *
  21. * @license AGPL-3.0
  22. *
  23. * This code is free software: you can redistribute it and/or modify
  24. * it under the terms of the GNU Affero General Public License, version 3,
  25. * as published by the Free Software Foundation.
  26. *
  27. * This program is distributed in the hope that it will be useful,
  28. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  29. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  30. * GNU Affero General Public License for more details.
  31. *
  32. * You should have received a copy of the GNU Affero General Public License, version 3,
  33. * along with this program. If not, see <http://www.gnu.org/licenses/>
  34. *
  35. */
  36. namespace OCA\Files_External\Lib\Storage;
  37. set_include_path(get_include_path() . PATH_SEPARATOR .
  38. \OC_App::getAppPath('files_external') . '/3rdparty/aws-sdk-php');
  39. require_once 'aws-autoloader.php';
  40. use Aws\S3\S3Client;
  41. use Aws\S3\Exception\S3Exception;
  42. use Icewind\Streams\CallbackWrapper;
  43. use Icewind\Streams\IteratorDirectory;
  44. use OC\Files\ObjectStore\S3ConnectionTrait;
  45. class AmazonS3 extends \OC\Files\Storage\Common {
  46. use S3ConnectionTrait;
  47. /**
  48. * @var array
  49. */
  50. private static $tmpFiles = array();
  51. /**
  52. * @var int in seconds
  53. */
  54. private $rescanDelay = 10;
  55. public function __construct($parameters) {
  56. parent::__construct($parameters);
  57. $this->parseParams($parameters);
  58. }
  59. /**
  60. * @param string $path
  61. * @return string correctly encoded path
  62. */
  63. private function normalizePath($path) {
  64. $path = trim($path, '/');
  65. if (!$path) {
  66. $path = '.';
  67. }
  68. return $path;
  69. }
  70. private function isRoot($path) {
  71. return $path === '.';
  72. }
  73. private function cleanKey($path) {
  74. if ($this->isRoot($path)) {
  75. return '/';
  76. }
  77. return $path;
  78. }
  79. /**
  80. * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name.
  81. * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home
  82. *
  83. * @param array $params
  84. */
  85. public function updateLegacyId (array $params) {
  86. $oldId = 'amazon::' . $params['key'] . md5($params['secret']);
  87. // find by old id or bucket
  88. $stmt = \OC::$server->getDatabaseConnection()->prepare(
  89. 'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)'
  90. );
  91. $stmt->execute(array($oldId, $this->id));
  92. while ($row = $stmt->fetch()) {
  93. $storages[$row['id']] = $row['numeric_id'];
  94. }
  95. if (isset($storages[$this->id]) && isset($storages[$oldId])) {
  96. // if both ids exist, delete the old storage and corresponding filecache entries
  97. \OC\Files\Cache\Storage::remove($oldId);
  98. } else if (isset($storages[$oldId])) {
  99. // if only the old id exists do an update
  100. $stmt = \OC::$server->getDatabaseConnection()->prepare(
  101. 'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?'
  102. );
  103. $stmt->execute(array($this->id, $oldId));
  104. }
  105. // only the bucket based id may exist, do nothing
  106. }
  107. /**
  108. * Remove a file or folder
  109. *
  110. * @param string $path
  111. * @return bool
  112. */
  113. protected function remove($path) {
  114. // remember fileType to reduce http calls
  115. $fileType = $this->filetype($path);
  116. if ($fileType === 'dir') {
  117. return $this->rmdir($path);
  118. } else if ($fileType === 'file') {
  119. return $this->unlink($path);
  120. } else {
  121. return false;
  122. }
  123. }
  124. public function mkdir($path) {
  125. $path = $this->normalizePath($path);
  126. if ($this->is_dir($path)) {
  127. return false;
  128. }
  129. try {
  130. $this->getConnection()->putObject(array(
  131. 'Bucket' => $this->bucket,
  132. 'Key' => $path . '/',
  133. 'Body' => '',
  134. 'ContentType' => 'httpd/unix-directory'
  135. ));
  136. $this->testTimeout();
  137. } catch (S3Exception $e) {
  138. \OCP\Util::logException('files_external', $e);
  139. return false;
  140. }
  141. return true;
  142. }
  143. public function file_exists($path) {
  144. return $this->filetype($path) !== false;
  145. }
  146. public function rmdir($path) {
  147. $path = $this->normalizePath($path);
  148. if ($this->isRoot($path)) {
  149. return $this->clearBucket();
  150. }
  151. if (!$this->file_exists($path)) {
  152. return false;
  153. }
  154. return $this->batchDelete($path);
  155. }
  156. protected function clearBucket() {
  157. try {
  158. $this->getConnection()->clearBucket($this->bucket);
  159. return true;
  160. // clearBucket() is not working with Ceph, so if it fails we try the slower approach
  161. } catch (\Exception $e) {
  162. return $this->batchDelete();
  163. }
  164. return false;
  165. }
  166. private function batchDelete ($path = null) {
  167. $params = array(
  168. 'Bucket' => $this->bucket
  169. );
  170. if ($path !== null) {
  171. $params['Prefix'] = $path . '/';
  172. }
  173. try {
  174. // Since there are no real directories on S3, we need
  175. // to delete all objects prefixed with the path.
  176. do {
  177. // instead of the iterator, manually loop over the list ...
  178. $objects = $this->getConnection()->listObjects($params);
  179. // ... so we can delete the files in batches
  180. $this->getConnection()->deleteObjects(array(
  181. 'Bucket' => $this->bucket,
  182. 'Objects' => $objects['Contents']
  183. ));
  184. $this->testTimeout();
  185. // we reached the end when the list is no longer truncated
  186. } while ($objects['IsTruncated']);
  187. } catch (S3Exception $e) {
  188. \OCP\Util::logException('files_external', $e);
  189. return false;
  190. }
  191. return true;
  192. }
  193. public function opendir($path) {
  194. $path = $this->normalizePath($path);
  195. if ($this->isRoot($path)) {
  196. $path = '';
  197. } else {
  198. $path .= '/';
  199. }
  200. try {
  201. $files = array();
  202. $result = $this->getConnection()->getIterator('ListObjects', array(
  203. 'Bucket' => $this->bucket,
  204. 'Delimiter' => '/',
  205. 'Prefix' => $path
  206. ), array('return_prefixes' => true));
  207. foreach ($result as $object) {
  208. if (isset($object['Key']) && $object['Key'] === $path) {
  209. // it's the directory itself, skip
  210. continue;
  211. }
  212. $file = basename(
  213. isset($object['Key']) ? $object['Key'] : $object['Prefix']
  214. );
  215. $files[] = $file;
  216. }
  217. return IteratorDirectory::wrap($files);
  218. } catch (S3Exception $e) {
  219. \OCP\Util::logException('files_external', $e);
  220. return false;
  221. }
  222. }
  223. public function stat($path) {
  224. $path = $this->normalizePath($path);
  225. try {
  226. $stat = array();
  227. if ($this->is_dir($path)) {
  228. //folders don't really exist
  229. $stat['size'] = -1; //unknown
  230. $stat['mtime'] = time() - $this->rescanDelay * 1000;
  231. } else {
  232. $result = $this->getConnection()->headObject(array(
  233. 'Bucket' => $this->bucket,
  234. 'Key' => $path
  235. ));
  236. $stat['size'] = $result['ContentLength'] ? $result['ContentLength'] : 0;
  237. if ($result['Metadata']['lastmodified']) {
  238. $stat['mtime'] = strtotime($result['Metadata']['lastmodified']);
  239. } else {
  240. $stat['mtime'] = strtotime($result['LastModified']);
  241. }
  242. }
  243. $stat['atime'] = time();
  244. return $stat;
  245. } catch(S3Exception $e) {
  246. \OCP\Util::logException('files_external', $e);
  247. return false;
  248. }
  249. }
  250. public function filetype($path) {
  251. $path = $this->normalizePath($path);
  252. if ($this->isRoot($path)) {
  253. return 'dir';
  254. }
  255. try {
  256. if ($this->getConnection()->doesObjectExist($this->bucket, $path)) {
  257. return 'file';
  258. }
  259. if ($this->getConnection()->doesObjectExist($this->bucket, $path.'/')) {
  260. return 'dir';
  261. }
  262. } catch (S3Exception $e) {
  263. \OCP\Util::logException('files_external', $e);
  264. return false;
  265. }
  266. return false;
  267. }
  268. public function unlink($path) {
  269. $path = $this->normalizePath($path);
  270. if ($this->is_dir($path)) {
  271. return $this->rmdir($path);
  272. }
  273. try {
  274. $this->getConnection()->deleteObject(array(
  275. 'Bucket' => $this->bucket,
  276. 'Key' => $path
  277. ));
  278. $this->testTimeout();
  279. } catch (S3Exception $e) {
  280. \OCP\Util::logException('files_external', $e);
  281. return false;
  282. }
  283. return true;
  284. }
  285. public function fopen($path, $mode) {
  286. $path = $this->normalizePath($path);
  287. switch ($mode) {
  288. case 'r':
  289. case 'rb':
  290. $tmpFile = \OCP\Files::tmpFile();
  291. self::$tmpFiles[$tmpFile] = $path;
  292. try {
  293. $this->getConnection()->getObject(array(
  294. 'Bucket' => $this->bucket,
  295. 'Key' => $path,
  296. 'SaveAs' => $tmpFile
  297. ));
  298. } catch (S3Exception $e) {
  299. \OCP\Util::logException('files_external', $e);
  300. return false;
  301. }
  302. return fopen($tmpFile, 'r');
  303. case 'w':
  304. case 'wb':
  305. case 'a':
  306. case 'ab':
  307. case 'r+':
  308. case 'w+':
  309. case 'wb+':
  310. case 'a+':
  311. case 'x':
  312. case 'x+':
  313. case 'c':
  314. case 'c+':
  315. if (strrpos($path, '.') !== false) {
  316. $ext = substr($path, strrpos($path, '.'));
  317. } else {
  318. $ext = '';
  319. }
  320. $tmpFile = \OCP\Files::tmpFile($ext);
  321. if ($this->file_exists($path)) {
  322. $source = $this->fopen($path, 'r');
  323. file_put_contents($tmpFile, $source);
  324. }
  325. $handle = fopen($tmpFile, $mode);
  326. return CallbackWrapper::wrap($handle, null, null, function() use ($path, $tmpFile) {
  327. $this->writeBack($tmpFile, $path);
  328. });
  329. }
  330. return false;
  331. }
  332. public function touch($path, $mtime = null) {
  333. $path = $this->normalizePath($path);
  334. $metadata = array();
  335. if (is_null($mtime)) {
  336. $mtime = time();
  337. }
  338. $metadata = [
  339. 'lastmodified' => gmdate(\Aws\Common\Enum\DateFormat::RFC1123, $mtime)
  340. ];
  341. $fileType = $this->filetype($path);
  342. try {
  343. if ($fileType !== false) {
  344. if ($fileType === 'dir' && ! $this->isRoot($path)) {
  345. $path .= '/';
  346. }
  347. $this->getConnection()->copyObject([
  348. 'Bucket' => $this->bucket,
  349. 'Key' => $this->cleanKey($path),
  350. 'Metadata' => $metadata,
  351. 'CopySource' => $this->bucket . '/' . $path,
  352. 'MetadataDirective' => 'REPLACE',
  353. ]);
  354. $this->testTimeout();
  355. } else {
  356. $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
  357. $this->getConnection()->putObject([
  358. 'Bucket' => $this->bucket,
  359. 'Key' => $this->cleanKey($path),
  360. 'Metadata' => $metadata,
  361. 'Body' => '',
  362. 'ContentType' => $mimeType,
  363. 'MetadataDirective' => 'REPLACE',
  364. ]);
  365. $this->testTimeout();
  366. }
  367. } catch (S3Exception $e) {
  368. \OCP\Util::logException('files_external', $e);
  369. return false;
  370. }
  371. return true;
  372. }
  373. public function copy($path1, $path2) {
  374. $path1 = $this->normalizePath($path1);
  375. $path2 = $this->normalizePath($path2);
  376. if ($this->is_file($path1)) {
  377. try {
  378. $this->getConnection()->copyObject(array(
  379. 'Bucket' => $this->bucket,
  380. 'Key' => $this->cleanKey($path2),
  381. 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
  382. ));
  383. $this->testTimeout();
  384. } catch (S3Exception $e) {
  385. \OCP\Util::logException('files_external', $e);
  386. return false;
  387. }
  388. } else {
  389. $this->remove($path2);
  390. try {
  391. $this->getConnection()->copyObject(array(
  392. 'Bucket' => $this->bucket,
  393. 'Key' => $path2 . '/',
  394. 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/')
  395. ));
  396. $this->testTimeout();
  397. } catch (S3Exception $e) {
  398. \OCP\Util::logException('files_external', $e);
  399. return false;
  400. }
  401. $dh = $this->opendir($path1);
  402. if (is_resource($dh)) {
  403. while (($file = readdir($dh)) !== false) {
  404. if (\OC\Files\Filesystem::isIgnoredDir($file)) {
  405. continue;
  406. }
  407. $source = $path1 . '/' . $file;
  408. $target = $path2 . '/' . $file;
  409. $this->copy($source, $target);
  410. }
  411. }
  412. }
  413. return true;
  414. }
  415. public function rename($path1, $path2) {
  416. $path1 = $this->normalizePath($path1);
  417. $path2 = $this->normalizePath($path2);
  418. if ($this->is_file($path1)) {
  419. if ($this->copy($path1, $path2) === false) {
  420. return false;
  421. }
  422. if ($this->unlink($path1) === false) {
  423. $this->unlink($path2);
  424. return false;
  425. }
  426. } else {
  427. if ($this->copy($path1, $path2) === false) {
  428. return false;
  429. }
  430. if ($this->rmdir($path1) === false) {
  431. $this->rmdir($path2);
  432. return false;
  433. }
  434. }
  435. return true;
  436. }
  437. public function test() {
  438. $test = $this->getConnection()->getBucketAcl(array(
  439. 'Bucket' => $this->bucket,
  440. ));
  441. if (isset($test) && !is_null($test->getPath('Owner/ID'))) {
  442. return true;
  443. }
  444. return false;
  445. }
  446. public function getId() {
  447. return $this->id;
  448. }
  449. public function writeBack($tmpFile, $path) {
  450. try {
  451. $this->getConnection()->putObject(array(
  452. 'Bucket' => $this->bucket,
  453. 'Key' => $this->cleanKey($path),
  454. 'SourceFile' => $tmpFile,
  455. 'ContentType' => \OC::$server->getMimeTypeDetector()->detect($tmpFile),
  456. 'ContentLength' => filesize($tmpFile)
  457. ));
  458. $this->testTimeout();
  459. unlink($tmpFile);
  460. } catch (S3Exception $e) {
  461. \OCP\Util::logException('files_external', $e);
  462. return false;
  463. }
  464. }
  465. /**
  466. * check if curl is installed
  467. */
  468. public static function checkDependencies() {
  469. return true;
  470. }
  471. }