File.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Cache;
  8. use OC\Files\Filesystem;
  9. use OC\Files\View;
  10. use OCP\ICache;
  11. use OCP\IUserSession;
  12. use OCP\Security\ISecureRandom;
  13. use OCP\Server;
  14. use Psr\Log\LoggerInterface;
  15. class File implements ICache {
  16. /** @var View */
  17. protected $storage;
  18. /**
  19. * Returns the cache storage for the logged in user
  20. *
  21. * @return \OC\Files\View cache storage
  22. * @throws \OC\ForbiddenException
  23. * @throws \OC\User\NoUserException
  24. */
  25. protected function getStorage() {
  26. if ($this->storage !== null) {
  27. return $this->storage;
  28. }
  29. $session = Server::get(IUserSession::class);
  30. if ($session->isLoggedIn()) {
  31. $rootView = new View();
  32. $userId = $session->getUser()->getUID();
  33. Filesystem::initMountPoints($userId);
  34. if (!$rootView->file_exists('/' . $userId . '/cache')) {
  35. $rootView->mkdir('/' . $userId . '/cache');
  36. }
  37. $this->storage = new View('/' . $userId . '/cache');
  38. return $this->storage;
  39. } else {
  40. Server::get(LoggerInterface::class)->error('Can\'t get cache storage, user not logged in', ['app' => 'core']);
  41. throw new \OC\ForbiddenException('Can\t get cache storage, user not logged in');
  42. }
  43. }
  44. /**
  45. * @param string $key
  46. * @return mixed|null
  47. * @throws \OC\ForbiddenException
  48. */
  49. public function get($key) {
  50. $result = null;
  51. if ($this->hasKey($key)) {
  52. $storage = $this->getStorage();
  53. $result = $storage->file_get_contents($key);
  54. }
  55. return $result;
  56. }
  57. /**
  58. * Returns the size of the stored/cached data
  59. *
  60. * @param string $key
  61. * @return int
  62. */
  63. public function size($key) {
  64. $result = 0;
  65. if ($this->hasKey($key)) {
  66. $storage = $this->getStorage();
  67. $result = $storage->filesize($key);
  68. }
  69. return $result;
  70. }
  71. /**
  72. * @param string $key
  73. * @param mixed $value
  74. * @param int $ttl
  75. * @return bool|mixed
  76. * @throws \OC\ForbiddenException
  77. */
  78. public function set($key, $value, $ttl = 0) {
  79. $storage = $this->getStorage();
  80. $result = false;
  81. // unique id to avoid chunk collision, just in case
  82. $uniqueId = Server::get(ISecureRandom::class)->generate(
  83. 16,
  84. ISecureRandom::CHAR_ALPHANUMERIC
  85. );
  86. // use part file to prevent hasKey() to find the key
  87. // while it is being written
  88. $keyPart = $key . '.' . $uniqueId . '.part';
  89. if ($storage && $storage->file_put_contents($keyPart, $value)) {
  90. if ($ttl === 0) {
  91. $ttl = 86400; // 60*60*24
  92. }
  93. $result = $storage->touch($keyPart, time() + $ttl);
  94. $result &= $storage->rename($keyPart, $key);
  95. }
  96. return $result;
  97. }
  98. /**
  99. * @param string $key
  100. * @return bool
  101. * @throws \OC\ForbiddenException
  102. */
  103. public function hasKey($key) {
  104. $storage = $this->getStorage();
  105. if ($storage && $storage->is_file($key) && $storage->isReadable($key)) {
  106. return true;
  107. }
  108. return false;
  109. }
  110. /**
  111. * @param string $key
  112. * @return bool|mixed
  113. * @throws \OC\ForbiddenException
  114. */
  115. public function remove($key) {
  116. $storage = $this->getStorage();
  117. if (!$storage) {
  118. return false;
  119. }
  120. return $storage->unlink($key);
  121. }
  122. /**
  123. * @param string $prefix
  124. * @return bool
  125. * @throws \OC\ForbiddenException
  126. */
  127. public function clear($prefix = '') {
  128. $storage = $this->getStorage();
  129. if ($storage && $storage->is_dir('/')) {
  130. $dh = $storage->opendir('/');
  131. if (is_resource($dh)) {
  132. while (($file = readdir($dh)) !== false) {
  133. if ($file !== '.' && $file !== '..' && ($prefix === '' || str_starts_with($file, $prefix))) {
  134. $storage->unlink('/' . $file);
  135. }
  136. }
  137. }
  138. }
  139. return true;
  140. }
  141. /**
  142. * Runs GC
  143. * @throws \OC\ForbiddenException
  144. */
  145. public function gc() {
  146. $storage = $this->getStorage();
  147. if ($storage) {
  148. // extra hour safety, in case of stray part chunks that take longer to write,
  149. // because touch() is only called after the chunk was finished
  150. $now = time() - 3600;
  151. $dh = $storage->opendir('/');
  152. if (!is_resource($dh)) {
  153. return null;
  154. }
  155. while (($file = readdir($dh)) !== false) {
  156. if ($file !== '.' && $file !== '..') {
  157. try {
  158. $mtime = $storage->filemtime('/' . $file);
  159. if ($mtime < $now) {
  160. $storage->unlink('/' . $file);
  161. }
  162. } catch (\OCP\Lock\LockedException $e) {
  163. // ignore locked chunks
  164. Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
  165. } catch (\OCP\Files\ForbiddenException $e) {
  166. Server::get(LoggerInterface::class)->debug('Could not cleanup forbidden chunk "' . $file . '"', ['app' => 'core']);
  167. } catch (\OCP\Files\LockNotAcquiredException $e) {
  168. Server::get(LoggerInterface::class)->debug('Could not cleanup locked chunk "' . $file . '"', ['app' => 'core']);
  169. }
  170. }
  171. }
  172. }
  173. }
  174. public static function isAvailable(): bool {
  175. return true;
  176. }
  177. }