You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

581 lines
16 KiB

9 years ago
9 years ago
13 years ago
13 years ago
13 years ago
13 years ago
12 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author aler9 <46489434+aler9@users.noreply.github.com>
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Bart Visscher <bartv@thisnet.nl>
  8. * @author Boris Rybalkin <ribalkin@gmail.com>
  9. * @author Brice Maron <brice@bmaron.net>
  10. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  11. * @author J0WI <J0WI@users.noreply.github.com>
  12. * @author Jakob Sack <mail@jakobsack.de>
  13. * @author Joas Schilling <coding@schilljs.com>
  14. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  15. * @author Klaas Freitag <freitag@owncloud.com>
  16. * @author Lukas Reschke <lukas@statuscode.ch>
  17. * @author Michael Gapczynski <GapczynskiM@gmail.com>
  18. * @author Morris Jobke <hey@morrisjobke.de>
  19. * @author Robin Appelman <robin@icewind.nl>
  20. * @author Roeland Jago Douma <roeland@famdouma.nl>
  21. * @author Sjors van der Pluijm <sjors@desjors.nl>
  22. * @author Stefan Weil <sw@weilnetz.de>
  23. * @author Thomas Müller <thomas.mueller@tmit.eu>
  24. * @author Tigran Mkrtchyan <tigran.mkrtchyan@desy.de>
  25. * @author Vincent Petry <vincent@nextcloud.com>
  26. *
  27. * @license AGPL-3.0
  28. *
  29. * This code is free software: you can redistribute it and/or modify
  30. * it under the terms of the GNU Affero General Public License, version 3,
  31. * as published by the Free Software Foundation.
  32. *
  33. * This program is distributed in the hope that it will be useful,
  34. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  35. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  36. * GNU Affero General Public License for more details.
  37. *
  38. * You should have received a copy of the GNU Affero General Public License, version 3,
  39. * along with this program. If not, see <http://www.gnu.org/licenses/>
  40. *
  41. */
  42. namespace OC\Files\Storage;
  43. use OC\Files\Filesystem;
  44. use OC\Files\Storage\Wrapper\Jail;
  45. use OCP\Constants;
  46. use OCP\Files\ForbiddenException;
  47. use OCP\Files\GenericFileException;
  48. use OCP\Files\Storage\IStorage;
  49. use OCP\ILogger;
  50. /**
  51. * for local filestore, we only have to map the paths
  52. */
  53. class Local extends \OC\Files\Storage\Common {
  54. protected $datadir;
  55. protected $dataDirLength;
  56. protected $realDataDir;
  57. public function __construct($arguments) {
  58. if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
  59. throw new \InvalidArgumentException('No data directory set for local storage');
  60. }
  61. $this->datadir = str_replace('//', '/', $arguments['datadir']);
  62. // some crazy code uses a local storage on root...
  63. if ($this->datadir === '/') {
  64. $this->realDataDir = $this->datadir;
  65. } else {
  66. $realPath = realpath($this->datadir) ?: $this->datadir;
  67. $this->realDataDir = rtrim($realPath, '/') . '/';
  68. }
  69. if (substr($this->datadir, -1) !== '/') {
  70. $this->datadir .= '/';
  71. }
  72. $this->dataDirLength = strlen($this->realDataDir);
  73. }
  74. public function __destruct() {
  75. }
  76. public function getId() {
  77. return 'local::' . $this->datadir;
  78. }
  79. public function mkdir($path) {
  80. $sourcePath = $this->getSourcePath($path);
  81. $oldMask = umask(022);
  82. $result = @mkdir($sourcePath, 0777, true);
  83. umask($oldMask);
  84. return $result;
  85. }
  86. public function rmdir($path) {
  87. if (!$this->isDeletable($path)) {
  88. return false;
  89. }
  90. try {
  91. $it = new \RecursiveIteratorIterator(
  92. new \RecursiveDirectoryIterator($this->getSourcePath($path)),
  93. \RecursiveIteratorIterator::CHILD_FIRST
  94. );
  95. /**
  96. * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
  97. * This bug is fixed in PHP 5.5.9 or before
  98. * See #8376
  99. */
  100. $it->rewind();
  101. while ($it->valid()) {
  102. /**
  103. * @var \SplFileInfo $file
  104. */
  105. $file = $it->current();
  106. clearstatcache(true, $this->getSourcePath($file));
  107. if (in_array($file->getBasename(), ['.', '..'])) {
  108. $it->next();
  109. continue;
  110. } elseif ($file->isDir()) {
  111. rmdir($file->getPathname());
  112. } elseif ($file->isFile() || $file->isLink()) {
  113. unlink($file->getPathname());
  114. }
  115. $it->next();
  116. }
  117. clearstatcache(true, $this->getSourcePath($path));
  118. return rmdir($this->getSourcePath($path));
  119. } catch (\UnexpectedValueException $e) {
  120. return false;
  121. }
  122. }
  123. public function opendir($path) {
  124. return opendir($this->getSourcePath($path));
  125. }
  126. public function is_dir($path) {
  127. if (substr($path, -1) == '/') {
  128. $path = substr($path, 0, -1);
  129. }
  130. return is_dir($this->getSourcePath($path));
  131. }
  132. public function is_file($path) {
  133. return is_file($this->getSourcePath($path));
  134. }
  135. public function stat($path) {
  136. $fullPath = $this->getSourcePath($path);
  137. clearstatcache(true, $fullPath);
  138. $statResult = @stat($fullPath);
  139. if (PHP_INT_SIZE === 4 && $statResult && !$this->is_dir($path)) {
  140. $filesize = $this->filesize($path);
  141. $statResult['size'] = $filesize;
  142. $statResult[7] = $filesize;
  143. }
  144. return $statResult;
  145. }
  146. /**
  147. * @inheritdoc
  148. */
  149. public function getMetaData($path) {
  150. $stat = $this->stat($path);
  151. if (!$stat) {
  152. return null;
  153. }
  154. $permissions = Constants::PERMISSION_SHARE;
  155. $statPermissions = $stat['mode'];
  156. $isDir = ($statPermissions & 0x4000) === 0x4000;
  157. if ($statPermissions & 0x0100) {
  158. $permissions += Constants::PERMISSION_READ;
  159. }
  160. if ($statPermissions & 0x0080) {
  161. $permissions += Constants::PERMISSION_UPDATE;
  162. if ($isDir) {
  163. $permissions += Constants::PERMISSION_CREATE;
  164. }
  165. }
  166. if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
  167. $fullPath = $this->getSourcePath($path);
  168. $parent = dirname($fullPath);
  169. if (is_writable($parent)) {
  170. $permissions += Constants::PERMISSION_DELETE;
  171. }
  172. }
  173. $data = [];
  174. $data['mimetype'] = $isDir ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($path);
  175. $data['mtime'] = $stat['mtime'];
  176. if ($data['mtime'] === false) {
  177. $data['mtime'] = time();
  178. }
  179. if ($isDir) {
  180. $data['size'] = -1; //unknown
  181. } else {
  182. $data['size'] = $stat['size'];
  183. }
  184. $data['etag'] = $this->calculateEtag($path, $stat);
  185. $data['storage_mtime'] = $data['mtime'];
  186. $data['permissions'] = $permissions;
  187. $data['name'] = basename($path);
  188. return $data;
  189. }
  190. public function filetype($path) {
  191. $filetype = filetype($this->getSourcePath($path));
  192. if ($filetype == 'link') {
  193. $filetype = filetype(realpath($this->getSourcePath($path)));
  194. }
  195. return $filetype;
  196. }
  197. public function filesize($path) {
  198. if ($this->is_dir($path)) {
  199. return 0;
  200. }
  201. $fullPath = $this->getSourcePath($path);
  202. if (PHP_INT_SIZE === 4) {
  203. $helper = new \OC\LargeFileHelper;
  204. return $helper->getFileSize($fullPath);
  205. }
  206. return filesize($fullPath);
  207. }
  208. public function isReadable($path) {
  209. return is_readable($this->getSourcePath($path));
  210. }
  211. public function isUpdatable($path) {
  212. return is_writable($this->getSourcePath($path));
  213. }
  214. public function file_exists($path) {
  215. return file_exists($this->getSourcePath($path));
  216. }
  217. public function filemtime($path) {
  218. $fullPath = $this->getSourcePath($path);
  219. clearstatcache(true, $fullPath);
  220. if (!$this->file_exists($path)) {
  221. return false;
  222. }
  223. if (PHP_INT_SIZE === 4) {
  224. $helper = new \OC\LargeFileHelper();
  225. return $helper->getFileMtime($fullPath);
  226. }
  227. return filemtime($fullPath);
  228. }
  229. public function touch($path, $mtime = null) {
  230. // sets the modification time of the file to the given value.
  231. // If mtime is nil the current time is set.
  232. // note that the access time of the file always changes to the current time.
  233. if ($this->file_exists($path) and !$this->isUpdatable($path)) {
  234. return false;
  235. }
  236. $oldMask = umask(022);
  237. if (!is_null($mtime)) {
  238. $result = @touch($this->getSourcePath($path), $mtime);
  239. } else {
  240. $result = @touch($this->getSourcePath($path));
  241. }
  242. umask($oldMask);
  243. if ($result) {
  244. clearstatcache(true, $this->getSourcePath($path));
  245. }
  246. return $result;
  247. }
  248. public function file_get_contents($path) {
  249. return file_get_contents($this->getSourcePath($path));
  250. }
  251. public function file_put_contents($path, $data) {
  252. $oldMask = umask(022);
  253. $result = file_put_contents($this->getSourcePath($path), $data);
  254. umask($oldMask);
  255. return $result;
  256. }
  257. public function unlink($path) {
  258. if ($this->is_dir($path)) {
  259. return $this->rmdir($path);
  260. } elseif ($this->is_file($path)) {
  261. return unlink($this->getSourcePath($path));
  262. } else {
  263. return false;
  264. }
  265. }
  266. private function treeContainsBlacklistedFile(string $path): bool {
  267. $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
  268. foreach ($iterator as $file) {
  269. /** @var \SplFileInfo $file */
  270. if (Filesystem::isFileBlacklisted($file->getBasename())) {
  271. return true;
  272. }
  273. }
  274. return false;
  275. }
  276. public function rename($path1, $path2) {
  277. $srcParent = dirname($path1);
  278. $dstParent = dirname($path2);
  279. if (!$this->isUpdatable($srcParent)) {
  280. \OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, ILogger::ERROR);
  281. return false;
  282. }
  283. if (!$this->isUpdatable($dstParent)) {
  284. \OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, ILogger::ERROR);
  285. return false;
  286. }
  287. if (!$this->file_exists($path1)) {
  288. \OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, ILogger::ERROR);
  289. return false;
  290. }
  291. if ($this->is_dir($path2)) {
  292. $this->rmdir($path2);
  293. } elseif ($this->is_file($path2)) {
  294. $this->unlink($path2);
  295. }
  296. if ($this->is_dir($path1)) {
  297. // we can't move folders across devices, use copy instead
  298. $stat1 = stat(dirname($this->getSourcePath($path1)));
  299. $stat2 = stat(dirname($this->getSourcePath($path2)));
  300. if ($stat1['dev'] !== $stat2['dev']) {
  301. $result = $this->copy($path1, $path2);
  302. if ($result) {
  303. $result &= $this->rmdir($path1);
  304. }
  305. return $result;
  306. }
  307. if ($this->treeContainsBlacklistedFile($this->getSourcePath($path1))) {
  308. throw new ForbiddenException('Invalid path', false);
  309. }
  310. }
  311. return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
  312. }
  313. public function copy($path1, $path2) {
  314. if ($this->is_dir($path1)) {
  315. return parent::copy($path1, $path2);
  316. } else {
  317. $oldMask = umask(022);
  318. $result = copy($this->getSourcePath($path1), $this->getSourcePath($path2));
  319. umask($oldMask);
  320. return $result;
  321. }
  322. }
  323. public function fopen($path, $mode) {
  324. $oldMask = umask(022);
  325. $result = fopen($this->getSourcePath($path), $mode);
  326. umask($oldMask);
  327. return $result;
  328. }
  329. public function hash($type, $path, $raw = false) {
  330. return hash_file($type, $this->getSourcePath($path), $raw);
  331. }
  332. public function free_space($path) {
  333. $sourcePath = $this->getSourcePath($path);
  334. // using !is_dir because $sourcePath might be a part file or
  335. // non-existing file, so we'd still want to use the parent dir
  336. // in such cases
  337. if (!is_dir($sourcePath)) {
  338. // disk_free_space doesn't work on files
  339. $sourcePath = dirname($sourcePath);
  340. }
  341. $space = @disk_free_space($sourcePath);
  342. if ($space === false || is_null($space)) {
  343. return \OCP\Files\FileInfo::SPACE_UNKNOWN;
  344. }
  345. return $space;
  346. }
  347. public function search($query) {
  348. return $this->searchInDir($query);
  349. }
  350. public function getLocalFile($path) {
  351. return $this->getSourcePath($path);
  352. }
  353. public function getLocalFolder($path) {
  354. return $this->getSourcePath($path);
  355. }
  356. /**
  357. * @param string $query
  358. * @param string $dir
  359. * @return array
  360. */
  361. protected function searchInDir($query, $dir = '') {
  362. $files = [];
  363. $physicalDir = $this->getSourcePath($dir);
  364. foreach (scandir($physicalDir) as $item) {
  365. if (\OC\Files\Filesystem::isIgnoredDir($item)) {
  366. continue;
  367. }
  368. $physicalItem = $physicalDir . '/' . $item;
  369. if (strstr(strtolower($item), strtolower($query)) !== false) {
  370. $files[] = $dir . '/' . $item;
  371. }
  372. if (is_dir($physicalItem)) {
  373. $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
  374. }
  375. }
  376. return $files;
  377. }
  378. /**
  379. * check if a file or folder has been updated since $time
  380. *
  381. * @param string $path
  382. * @param int $time
  383. * @return bool
  384. */
  385. public function hasUpdated($path, $time) {
  386. if ($this->file_exists($path)) {
  387. return $this->filemtime($path) > $time;
  388. } else {
  389. return true;
  390. }
  391. }
  392. /**
  393. * Get the source path (on disk) of a given path
  394. *
  395. * @param string $path
  396. * @return string
  397. * @throws ForbiddenException
  398. */
  399. public function getSourcePath($path) {
  400. if (Filesystem::isFileBlacklisted($path)) {
  401. throw new ForbiddenException('Invalid path', false);
  402. }
  403. $fullPath = $this->datadir . $path;
  404. $currentPath = $path;
  405. $allowSymlinks = \OC::$server->getConfig()->getSystemValue('localstorage.allowsymlinks', false);
  406. if ($allowSymlinks || $currentPath === '') {
  407. return $fullPath;
  408. }
  409. $pathToResolve = $fullPath;
  410. $realPath = realpath($pathToResolve);
  411. while ($realPath === false) { // for non existing files check the parent directory
  412. $currentPath = dirname($currentPath);
  413. if ($currentPath === '' || $currentPath === '.') {
  414. return $fullPath;
  415. }
  416. $realPath = realpath($this->datadir . $currentPath);
  417. }
  418. if ($realPath) {
  419. $realPath = $realPath . '/';
  420. }
  421. if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
  422. return $fullPath;
  423. }
  424. \OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ILogger::ERROR);
  425. throw new ForbiddenException('Following symlinks is not allowed', false);
  426. }
  427. /**
  428. * {@inheritdoc}
  429. */
  430. public function isLocal() {
  431. return true;
  432. }
  433. /**
  434. * get the ETag for a file or folder
  435. *
  436. * @param string $path
  437. * @return string
  438. */
  439. public function getETag($path) {
  440. return $this->calculateEtag($path, $this->stat($path));
  441. }
  442. private function calculateEtag(string $path, array $stat): string {
  443. if ($stat['mode'] & 0x4000) { // is_dir
  444. return parent::getETag($path);
  445. } else {
  446. if ($stat === false) {
  447. return md5('');
  448. }
  449. $toHash = '';
  450. if (isset($stat['mtime'])) {
  451. $toHash .= $stat['mtime'];
  452. }
  453. if (isset($stat['ino'])) {
  454. $toHash .= $stat['ino'];
  455. }
  456. if (isset($stat['dev'])) {
  457. $toHash .= $stat['dev'];
  458. }
  459. if (isset($stat['size'])) {
  460. $toHash .= $stat['size'];
  461. }
  462. return md5($toHash);
  463. }
  464. }
  465. /**
  466. * @param IStorage $sourceStorage
  467. * @param string $sourceInternalPath
  468. * @param string $targetInternalPath
  469. * @param bool $preserveMtime
  470. * @return bool
  471. */
  472. public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
  473. if ($sourceStorage->instanceOfStorage(Local::class)) {
  474. if ($sourceStorage->instanceOfStorage(Jail::class)) {
  475. /**
  476. * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
  477. */
  478. $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
  479. }
  480. /**
  481. * @var \OC\Files\Storage\Local $sourceStorage
  482. */
  483. $rootStorage = new Local(['datadir' => '/']);
  484. return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
  485. } else {
  486. return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  487. }
  488. }
  489. /**
  490. * @param IStorage $sourceStorage
  491. * @param string $sourceInternalPath
  492. * @param string $targetInternalPath
  493. * @return bool
  494. */
  495. public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
  496. if ($sourceStorage->instanceOfStorage(Local::class)) {
  497. if ($sourceStorage->instanceOfStorage(Jail::class)) {
  498. /**
  499. * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
  500. */
  501. $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
  502. }
  503. /**
  504. * @var \OC\Files\Storage\Local $sourceStorage
  505. */
  506. $rootStorage = new Local(['datadir' => '/']);
  507. return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
  508. } else {
  509. return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  510. }
  511. }
  512. public function writeStream(string $path, $stream, int $size = null): int {
  513. $result = $this->file_put_contents($path, $stream);
  514. if ($result === false) {
  515. throw new GenericFileException("Failed write steam to $path");
  516. } else {
  517. return $result;
  518. }
  519. }
  520. }