@@ -18,269 +18,269 @@ |
||
| 18 | 18 | * Throws a StorageNotAvailableException for storages with known failures |
| 19 | 19 | */ |
| 20 | 20 | class Availability extends Wrapper { |
| 21 | - public const RECHECK_TTL_SEC = 600; // 10 minutes |
|
| 22 | - |
|
| 23 | - /** @var IConfig */ |
|
| 24 | - protected $config; |
|
| 25 | - protected ?bool $available = null; |
|
| 26 | - |
|
| 27 | - public function __construct(array $parameters) { |
|
| 28 | - $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class); |
|
| 29 | - parent::__construct($parameters); |
|
| 30 | - } |
|
| 31 | - |
|
| 32 | - public static function shouldRecheck($availability): bool { |
|
| 33 | - if (!$availability['available']) { |
|
| 34 | - // trigger a recheck if TTL reached |
|
| 35 | - if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) { |
|
| 36 | - return true; |
|
| 37 | - } |
|
| 38 | - } |
|
| 39 | - return false; |
|
| 40 | - } |
|
| 41 | - |
|
| 42 | - /** |
|
| 43 | - * Only called if availability === false |
|
| 44 | - */ |
|
| 45 | - private function updateAvailability(): bool { |
|
| 46 | - // reset availability to false so that multiple requests don't recheck concurrently |
|
| 47 | - $this->setAvailability(false); |
|
| 48 | - try { |
|
| 49 | - $result = $this->test(); |
|
| 50 | - } catch (\Exception $e) { |
|
| 51 | - $result = false; |
|
| 52 | - } |
|
| 53 | - $this->setAvailability($result); |
|
| 54 | - return $result; |
|
| 55 | - } |
|
| 56 | - |
|
| 57 | - private function isAvailable(): bool { |
|
| 58 | - if (is_null($this->available)) { |
|
| 59 | - $availability = $this->getAvailability(); |
|
| 60 | - if (self::shouldRecheck($availability)) { |
|
| 61 | - return $this->updateAvailability(); |
|
| 62 | - } |
|
| 63 | - $this->available = $availability['available']; |
|
| 64 | - } |
|
| 65 | - return $this->available; |
|
| 66 | - } |
|
| 67 | - |
|
| 68 | - /** |
|
| 69 | - * @throws StorageNotAvailableException |
|
| 70 | - */ |
|
| 71 | - private function checkAvailability(): void { |
|
| 72 | - if (!$this->isAvailable()) { |
|
| 73 | - throw new StorageNotAvailableException(); |
|
| 74 | - } |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - /** |
|
| 78 | - * Handles availability checks and delegates method calls dynamically |
|
| 79 | - */ |
|
| 80 | - private function handleAvailability(string $method, mixed ...$args): mixed { |
|
| 81 | - $this->checkAvailability(); |
|
| 82 | - try { |
|
| 83 | - return call_user_func_array([parent::class, $method], $args); |
|
| 84 | - } catch (StorageNotAvailableException $e) { |
|
| 85 | - $this->setUnavailable($e); |
|
| 86 | - return false; |
|
| 87 | - } |
|
| 88 | - } |
|
| 89 | - |
|
| 90 | - public function mkdir(string $path): bool { |
|
| 91 | - return $this->handleAvailability('mkdir', $path); |
|
| 92 | - } |
|
| 93 | - |
|
| 94 | - public function rmdir(string $path): bool { |
|
| 95 | - return $this->handleAvailability('rmdir', $path); |
|
| 96 | - } |
|
| 97 | - |
|
| 98 | - public function opendir(string $path) { |
|
| 99 | - return $this->handleAvailability('opendir', $path); |
|
| 100 | - } |
|
| 101 | - |
|
| 102 | - public function is_dir(string $path): bool { |
|
| 103 | - return $this->handleAvailability('is_dir', $path); |
|
| 104 | - } |
|
| 105 | - |
|
| 106 | - public function is_file(string $path): bool { |
|
| 107 | - return $this->handleAvailability('is_file', $path); |
|
| 108 | - } |
|
| 109 | - |
|
| 110 | - public function stat(string $path): array|false { |
|
| 111 | - return $this->handleAvailability('stat', $path); |
|
| 112 | - } |
|
| 113 | - |
|
| 114 | - public function filetype(string $path): string|false { |
|
| 115 | - return $this->handleAvailability('filetype', $path); |
|
| 116 | - } |
|
| 117 | - |
|
| 118 | - public function filesize(string $path): int|float|false { |
|
| 119 | - return $this->handleAvailability('filesize', $path); |
|
| 120 | - } |
|
| 121 | - |
|
| 122 | - public function isCreatable(string $path): bool { |
|
| 123 | - return $this->handleAvailability('isCreatable', $path); |
|
| 124 | - } |
|
| 125 | - |
|
| 126 | - public function isReadable(string $path): bool { |
|
| 127 | - return $this->handleAvailability('isReadable', $path); |
|
| 128 | - } |
|
| 129 | - |
|
| 130 | - public function isUpdatable(string $path): bool { |
|
| 131 | - return $this->handleAvailability('isUpdatable', $path); |
|
| 132 | - } |
|
| 133 | - |
|
| 134 | - public function isDeletable(string $path): bool { |
|
| 135 | - return $this->handleAvailability('isDeletable', $path); |
|
| 136 | - } |
|
| 137 | - |
|
| 138 | - public function isSharable(string $path): bool { |
|
| 139 | - return $this->handleAvailability('isSharable', $path); |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - public function getPermissions(string $path): int { |
|
| 143 | - return $this->handleAvailability('getPermissions', $path); |
|
| 144 | - } |
|
| 145 | - |
|
| 146 | - public function file_exists(string $path): bool { |
|
| 147 | - if ($path === '') { |
|
| 148 | - return true; |
|
| 149 | - } |
|
| 150 | - return $this->handleAvailability('file_exists', $path); |
|
| 151 | - } |
|
| 152 | - |
|
| 153 | - public function filemtime(string $path): int|false { |
|
| 154 | - return $this->handleAvailability('filemtime', $path); |
|
| 155 | - } |
|
| 156 | - |
|
| 157 | - public function file_get_contents(string $path): string|false { |
|
| 158 | - return $this->handleAvailability('file_get_contents', $path); |
|
| 159 | - } |
|
| 160 | - |
|
| 161 | - public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 162 | - return $this->handleAvailability('file_put_contents', $path, $data); |
|
| 163 | - } |
|
| 164 | - |
|
| 165 | - public function unlink(string $path): bool { |
|
| 166 | - return $this->handleAvailability('unlink', $path); |
|
| 167 | - } |
|
| 168 | - |
|
| 169 | - public function rename(string $source, string $target): bool { |
|
| 170 | - return $this->handleAvailability('rename', $source, $target); |
|
| 171 | - } |
|
| 172 | - |
|
| 173 | - public function copy(string $source, string $target): bool { |
|
| 174 | - return $this->handleAvailability('copy', $source, $target); |
|
| 175 | - } |
|
| 176 | - |
|
| 177 | - public function fopen(string $path, string $mode) { |
|
| 178 | - return $this->handleAvailability('fopen', $path, $mode); |
|
| 179 | - } |
|
| 180 | - |
|
| 181 | - public function getMimeType(string $path): string|false { |
|
| 182 | - return $this->handleAvailability('getMimeType', $path); |
|
| 183 | - } |
|
| 184 | - |
|
| 185 | - public function hash(string $type, string $path, bool $raw = false): string|false { |
|
| 186 | - return $this->handleAvailability('hash', $type, $path, $raw); |
|
| 187 | - } |
|
| 188 | - |
|
| 189 | - public function free_space(string $path): int|float|false { |
|
| 190 | - return $this->handleAvailability('free_space', $path); |
|
| 191 | - } |
|
| 192 | - |
|
| 193 | - public function touch(string $path, ?int $mtime = null): bool { |
|
| 194 | - return $this->handleAvailability('touch', $path, $mtime); |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - public function getLocalFile(string $path): string|false { |
|
| 198 | - return $this->handleAvailability('getLocalFile', $path); |
|
| 199 | - } |
|
| 200 | - |
|
| 201 | - public function hasUpdated(string $path, int $time): bool { |
|
| 202 | - if (!$this->isAvailable()) { |
|
| 203 | - return false; |
|
| 204 | - } |
|
| 205 | - try { |
|
| 206 | - return parent::hasUpdated($path, $time); |
|
| 207 | - } catch (StorageNotAvailableException $e) { |
|
| 208 | - // set unavailable but don't rethrow |
|
| 209 | - $this->setUnavailable(null); |
|
| 210 | - return false; |
|
| 211 | - } |
|
| 212 | - } |
|
| 213 | - |
|
| 214 | - public function getOwner(string $path): string|false { |
|
| 215 | - try { |
|
| 216 | - return parent::getOwner($path); |
|
| 217 | - } catch (StorageNotAvailableException $e) { |
|
| 218 | - $this->setUnavailable($e); |
|
| 219 | - return false; |
|
| 220 | - } |
|
| 221 | - } |
|
| 222 | - |
|
| 223 | - public function getETag(string $path): string|false { |
|
| 224 | - return $this->handleAvailability('getETag', $path); |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - public function getDirectDownload(string $path): array|false { |
|
| 228 | - return $this->handleAvailability('getDirectDownload', $path); |
|
| 229 | - } |
|
| 230 | - |
|
| 231 | - public function getDirectDownloadById(string $fileId): array|false { |
|
| 232 | - return $this->handleAvailability('getDirectDownloadById', $fileId); |
|
| 233 | - } |
|
| 234 | - |
|
| 235 | - public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 236 | - return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 237 | - } |
|
| 238 | - |
|
| 239 | - public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 240 | - return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 241 | - } |
|
| 242 | - |
|
| 243 | - public function getMetaData(string $path): ?array { |
|
| 244 | - $this->checkAvailability(); |
|
| 245 | - try { |
|
| 246 | - return parent::getMetaData($path); |
|
| 247 | - } catch (StorageNotAvailableException $e) { |
|
| 248 | - $this->setUnavailable($e); |
|
| 249 | - return null; |
|
| 250 | - } |
|
| 251 | - } |
|
| 252 | - |
|
| 253 | - /** |
|
| 254 | - * @template T of StorageNotAvailableException|null |
|
| 255 | - * @param T $e |
|
| 256 | - * @psalm-return (T is null ? void : never) |
|
| 257 | - * @throws StorageNotAvailableException |
|
| 258 | - */ |
|
| 259 | - protected function setUnavailable(?StorageNotAvailableException $e): void { |
|
| 260 | - $delay = self::RECHECK_TTL_SEC; |
|
| 261 | - if ($e instanceof StorageAuthException) { |
|
| 262 | - $delay = max( |
|
| 263 | - // 30min |
|
| 264 | - $this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800), |
|
| 265 | - self::RECHECK_TTL_SEC |
|
| 266 | - ); |
|
| 267 | - } |
|
| 268 | - $this->available = false; |
|
| 269 | - $this->getStorageCache()->setAvailability(false, $delay); |
|
| 270 | - if ($e !== null) { |
|
| 271 | - throw $e; |
|
| 272 | - } |
|
| 273 | - } |
|
| 274 | - |
|
| 275 | - |
|
| 276 | - |
|
| 277 | - public function getDirectoryContent(string $directory): \Traversable { |
|
| 278 | - $this->checkAvailability(); |
|
| 279 | - try { |
|
| 280 | - return parent::getDirectoryContent($directory); |
|
| 281 | - } catch (StorageNotAvailableException $e) { |
|
| 282 | - $this->setUnavailable($e); |
|
| 283 | - return new \EmptyIterator(); |
|
| 284 | - } |
|
| 285 | - } |
|
| 21 | + public const RECHECK_TTL_SEC = 600; // 10 minutes |
|
| 22 | + |
|
| 23 | + /** @var IConfig */ |
|
| 24 | + protected $config; |
|
| 25 | + protected ?bool $available = null; |
|
| 26 | + |
|
| 27 | + public function __construct(array $parameters) { |
|
| 28 | + $this->config = $parameters['config'] ?? \OCP\Server::get(IConfig::class); |
|
| 29 | + parent::__construct($parameters); |
|
| 30 | + } |
|
| 31 | + |
|
| 32 | + public static function shouldRecheck($availability): bool { |
|
| 33 | + if (!$availability['available']) { |
|
| 34 | + // trigger a recheck if TTL reached |
|
| 35 | + if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) { |
|
| 36 | + return true; |
|
| 37 | + } |
|
| 38 | + } |
|
| 39 | + return false; |
|
| 40 | + } |
|
| 41 | + |
|
| 42 | + /** |
|
| 43 | + * Only called if availability === false |
|
| 44 | + */ |
|
| 45 | + private function updateAvailability(): bool { |
|
| 46 | + // reset availability to false so that multiple requests don't recheck concurrently |
|
| 47 | + $this->setAvailability(false); |
|
| 48 | + try { |
|
| 49 | + $result = $this->test(); |
|
| 50 | + } catch (\Exception $e) { |
|
| 51 | + $result = false; |
|
| 52 | + } |
|
| 53 | + $this->setAvailability($result); |
|
| 54 | + return $result; |
|
| 55 | + } |
|
| 56 | + |
|
| 57 | + private function isAvailable(): bool { |
|
| 58 | + if (is_null($this->available)) { |
|
| 59 | + $availability = $this->getAvailability(); |
|
| 60 | + if (self::shouldRecheck($availability)) { |
|
| 61 | + return $this->updateAvailability(); |
|
| 62 | + } |
|
| 63 | + $this->available = $availability['available']; |
|
| 64 | + } |
|
| 65 | + return $this->available; |
|
| 66 | + } |
|
| 67 | + |
|
| 68 | + /** |
|
| 69 | + * @throws StorageNotAvailableException |
|
| 70 | + */ |
|
| 71 | + private function checkAvailability(): void { |
|
| 72 | + if (!$this->isAvailable()) { |
|
| 73 | + throw new StorageNotAvailableException(); |
|
| 74 | + } |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + /** |
|
| 78 | + * Handles availability checks and delegates method calls dynamically |
|
| 79 | + */ |
|
| 80 | + private function handleAvailability(string $method, mixed ...$args): mixed { |
|
| 81 | + $this->checkAvailability(); |
|
| 82 | + try { |
|
| 83 | + return call_user_func_array([parent::class, $method], $args); |
|
| 84 | + } catch (StorageNotAvailableException $e) { |
|
| 85 | + $this->setUnavailable($e); |
|
| 86 | + return false; |
|
| 87 | + } |
|
| 88 | + } |
|
| 89 | + |
|
| 90 | + public function mkdir(string $path): bool { |
|
| 91 | + return $this->handleAvailability('mkdir', $path); |
|
| 92 | + } |
|
| 93 | + |
|
| 94 | + public function rmdir(string $path): bool { |
|
| 95 | + return $this->handleAvailability('rmdir', $path); |
|
| 96 | + } |
|
| 97 | + |
|
| 98 | + public function opendir(string $path) { |
|
| 99 | + return $this->handleAvailability('opendir', $path); |
|
| 100 | + } |
|
| 101 | + |
|
| 102 | + public function is_dir(string $path): bool { |
|
| 103 | + return $this->handleAvailability('is_dir', $path); |
|
| 104 | + } |
|
| 105 | + |
|
| 106 | + public function is_file(string $path): bool { |
|
| 107 | + return $this->handleAvailability('is_file', $path); |
|
| 108 | + } |
|
| 109 | + |
|
| 110 | + public function stat(string $path): array|false { |
|
| 111 | + return $this->handleAvailability('stat', $path); |
|
| 112 | + } |
|
| 113 | + |
|
| 114 | + public function filetype(string $path): string|false { |
|
| 115 | + return $this->handleAvailability('filetype', $path); |
|
| 116 | + } |
|
| 117 | + |
|
| 118 | + public function filesize(string $path): int|float|false { |
|
| 119 | + return $this->handleAvailability('filesize', $path); |
|
| 120 | + } |
|
| 121 | + |
|
| 122 | + public function isCreatable(string $path): bool { |
|
| 123 | + return $this->handleAvailability('isCreatable', $path); |
|
| 124 | + } |
|
| 125 | + |
|
| 126 | + public function isReadable(string $path): bool { |
|
| 127 | + return $this->handleAvailability('isReadable', $path); |
|
| 128 | + } |
|
| 129 | + |
|
| 130 | + public function isUpdatable(string $path): bool { |
|
| 131 | + return $this->handleAvailability('isUpdatable', $path); |
|
| 132 | + } |
|
| 133 | + |
|
| 134 | + public function isDeletable(string $path): bool { |
|
| 135 | + return $this->handleAvailability('isDeletable', $path); |
|
| 136 | + } |
|
| 137 | + |
|
| 138 | + public function isSharable(string $path): bool { |
|
| 139 | + return $this->handleAvailability('isSharable', $path); |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + public function getPermissions(string $path): int { |
|
| 143 | + return $this->handleAvailability('getPermissions', $path); |
|
| 144 | + } |
|
| 145 | + |
|
| 146 | + public function file_exists(string $path): bool { |
|
| 147 | + if ($path === '') { |
|
| 148 | + return true; |
|
| 149 | + } |
|
| 150 | + return $this->handleAvailability('file_exists', $path); |
|
| 151 | + } |
|
| 152 | + |
|
| 153 | + public function filemtime(string $path): int|false { |
|
| 154 | + return $this->handleAvailability('filemtime', $path); |
|
| 155 | + } |
|
| 156 | + |
|
| 157 | + public function file_get_contents(string $path): string|false { |
|
| 158 | + return $this->handleAvailability('file_get_contents', $path); |
|
| 159 | + } |
|
| 160 | + |
|
| 161 | + public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 162 | + return $this->handleAvailability('file_put_contents', $path, $data); |
|
| 163 | + } |
|
| 164 | + |
|
| 165 | + public function unlink(string $path): bool { |
|
| 166 | + return $this->handleAvailability('unlink', $path); |
|
| 167 | + } |
|
| 168 | + |
|
| 169 | + public function rename(string $source, string $target): bool { |
|
| 170 | + return $this->handleAvailability('rename', $source, $target); |
|
| 171 | + } |
|
| 172 | + |
|
| 173 | + public function copy(string $source, string $target): bool { |
|
| 174 | + return $this->handleAvailability('copy', $source, $target); |
|
| 175 | + } |
|
| 176 | + |
|
| 177 | + public function fopen(string $path, string $mode) { |
|
| 178 | + return $this->handleAvailability('fopen', $path, $mode); |
|
| 179 | + } |
|
| 180 | + |
|
| 181 | + public function getMimeType(string $path): string|false { |
|
| 182 | + return $this->handleAvailability('getMimeType', $path); |
|
| 183 | + } |
|
| 184 | + |
|
| 185 | + public function hash(string $type, string $path, bool $raw = false): string|false { |
|
| 186 | + return $this->handleAvailability('hash', $type, $path, $raw); |
|
| 187 | + } |
|
| 188 | + |
|
| 189 | + public function free_space(string $path): int|float|false { |
|
| 190 | + return $this->handleAvailability('free_space', $path); |
|
| 191 | + } |
|
| 192 | + |
|
| 193 | + public function touch(string $path, ?int $mtime = null): bool { |
|
| 194 | + return $this->handleAvailability('touch', $path, $mtime); |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + public function getLocalFile(string $path): string|false { |
|
| 198 | + return $this->handleAvailability('getLocalFile', $path); |
|
| 199 | + } |
|
| 200 | + |
|
| 201 | + public function hasUpdated(string $path, int $time): bool { |
|
| 202 | + if (!$this->isAvailable()) { |
|
| 203 | + return false; |
|
| 204 | + } |
|
| 205 | + try { |
|
| 206 | + return parent::hasUpdated($path, $time); |
|
| 207 | + } catch (StorageNotAvailableException $e) { |
|
| 208 | + // set unavailable but don't rethrow |
|
| 209 | + $this->setUnavailable(null); |
|
| 210 | + return false; |
|
| 211 | + } |
|
| 212 | + } |
|
| 213 | + |
|
| 214 | + public function getOwner(string $path): string|false { |
|
| 215 | + try { |
|
| 216 | + return parent::getOwner($path); |
|
| 217 | + } catch (StorageNotAvailableException $e) { |
|
| 218 | + $this->setUnavailable($e); |
|
| 219 | + return false; |
|
| 220 | + } |
|
| 221 | + } |
|
| 222 | + |
|
| 223 | + public function getETag(string $path): string|false { |
|
| 224 | + return $this->handleAvailability('getETag', $path); |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + public function getDirectDownload(string $path): array|false { |
|
| 228 | + return $this->handleAvailability('getDirectDownload', $path); |
|
| 229 | + } |
|
| 230 | + |
|
| 231 | + public function getDirectDownloadById(string $fileId): array|false { |
|
| 232 | + return $this->handleAvailability('getDirectDownloadById', $fileId); |
|
| 233 | + } |
|
| 234 | + |
|
| 235 | + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 236 | + return $this->handleAvailability('copyFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 237 | + } |
|
| 238 | + |
|
| 239 | + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 240 | + return $this->handleAvailability('moveFromStorage', $sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 241 | + } |
|
| 242 | + |
|
| 243 | + public function getMetaData(string $path): ?array { |
|
| 244 | + $this->checkAvailability(); |
|
| 245 | + try { |
|
| 246 | + return parent::getMetaData($path); |
|
| 247 | + } catch (StorageNotAvailableException $e) { |
|
| 248 | + $this->setUnavailable($e); |
|
| 249 | + return null; |
|
| 250 | + } |
|
| 251 | + } |
|
| 252 | + |
|
| 253 | + /** |
|
| 254 | + * @template T of StorageNotAvailableException|null |
|
| 255 | + * @param T $e |
|
| 256 | + * @psalm-return (T is null ? void : never) |
|
| 257 | + * @throws StorageNotAvailableException |
|
| 258 | + */ |
|
| 259 | + protected function setUnavailable(?StorageNotAvailableException $e): void { |
|
| 260 | + $delay = self::RECHECK_TTL_SEC; |
|
| 261 | + if ($e instanceof StorageAuthException) { |
|
| 262 | + $delay = max( |
|
| 263 | + // 30min |
|
| 264 | + $this->config->getSystemValueInt('external_storage.auth_availability_delay', 1800), |
|
| 265 | + self::RECHECK_TTL_SEC |
|
| 266 | + ); |
|
| 267 | + } |
|
| 268 | + $this->available = false; |
|
| 269 | + $this->getStorageCache()->setAvailability(false, $delay); |
|
| 270 | + if ($e !== null) { |
|
| 271 | + throw $e; |
|
| 272 | + } |
|
| 273 | + } |
|
| 274 | + |
|
| 275 | + |
|
| 276 | + |
|
| 277 | + public function getDirectoryContent(string $directory): \Traversable { |
|
| 278 | + $this->checkAvailability(); |
|
| 279 | + try { |
|
| 280 | + return parent::getDirectoryContent($directory); |
|
| 281 | + } catch (StorageNotAvailableException $e) { |
|
| 282 | + $this->setUnavailable($e); |
|
| 283 | + return new \EmptyIterator(); |
|
| 284 | + } |
|
| 285 | + } |
|
| 286 | 286 | } |
@@ -16,180 +16,180 @@ |
||
| 16 | 16 | * Storage placeholder to represent a missing precondition, storage unavailable |
| 17 | 17 | */ |
| 18 | 18 | class FailedStorage extends Common { |
| 19 | - /** @var \Exception */ |
|
| 20 | - protected $e; |
|
| 21 | - |
|
| 22 | - /** |
|
| 23 | - * @param array $parameters ['exception' => \Exception] |
|
| 24 | - */ |
|
| 25 | - public function __construct(array $parameters) { |
|
| 26 | - $this->e = $parameters['exception']; |
|
| 27 | - if (!$this->e) { |
|
| 28 | - throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor'); |
|
| 29 | - } |
|
| 30 | - } |
|
| 31 | - |
|
| 32 | - public function getId(): string { |
|
| 33 | - // we can't return anything sane here |
|
| 34 | - return 'failedstorage'; |
|
| 35 | - } |
|
| 36 | - |
|
| 37 | - public function mkdir(string $path): never { |
|
| 38 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 39 | - } |
|
| 40 | - |
|
| 41 | - public function rmdir(string $path): never { |
|
| 42 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 43 | - } |
|
| 44 | - |
|
| 45 | - public function opendir(string $path): never { |
|
| 46 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 47 | - } |
|
| 48 | - |
|
| 49 | - public function is_dir(string $path): never { |
|
| 50 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 51 | - } |
|
| 52 | - |
|
| 53 | - public function is_file(string $path): never { |
|
| 54 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 55 | - } |
|
| 56 | - |
|
| 57 | - public function stat(string $path): never { |
|
| 58 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 59 | - } |
|
| 60 | - |
|
| 61 | - public function filetype(string $path): never { |
|
| 62 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 63 | - } |
|
| 64 | - |
|
| 65 | - public function filesize(string $path): never { |
|
| 66 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 67 | - } |
|
| 68 | - |
|
| 69 | - public function isCreatable(string $path): never { |
|
| 70 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 71 | - } |
|
| 72 | - |
|
| 73 | - public function isReadable(string $path): never { |
|
| 74 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - public function isUpdatable(string $path): never { |
|
| 78 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 79 | - } |
|
| 80 | - |
|
| 81 | - public function isDeletable(string $path): never { |
|
| 82 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 83 | - } |
|
| 84 | - |
|
| 85 | - public function isSharable(string $path): never { |
|
| 86 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 87 | - } |
|
| 88 | - |
|
| 89 | - public function getPermissions(string $path): never { |
|
| 90 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 91 | - } |
|
| 92 | - |
|
| 93 | - public function file_exists(string $path): never { |
|
| 94 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 95 | - } |
|
| 96 | - |
|
| 97 | - public function filemtime(string $path): never { |
|
| 98 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 99 | - } |
|
| 100 | - |
|
| 101 | - public function file_get_contents(string $path): never { |
|
| 102 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 103 | - } |
|
| 104 | - |
|
| 105 | - public function file_put_contents(string $path, mixed $data): never { |
|
| 106 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 107 | - } |
|
| 108 | - |
|
| 109 | - public function unlink(string $path): never { |
|
| 110 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 111 | - } |
|
| 112 | - |
|
| 113 | - public function rename(string $source, string $target): never { |
|
| 114 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 115 | - } |
|
| 116 | - |
|
| 117 | - public function copy(string $source, string $target): never { |
|
| 118 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 119 | - } |
|
| 120 | - |
|
| 121 | - public function fopen(string $path, string $mode): never { |
|
| 122 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 123 | - } |
|
| 124 | - |
|
| 125 | - public function getMimeType(string $path): never { |
|
| 126 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 127 | - } |
|
| 19 | + /** @var \Exception */ |
|
| 20 | + protected $e; |
|
| 21 | + |
|
| 22 | + /** |
|
| 23 | + * @param array $parameters ['exception' => \Exception] |
|
| 24 | + */ |
|
| 25 | + public function __construct(array $parameters) { |
|
| 26 | + $this->e = $parameters['exception']; |
|
| 27 | + if (!$this->e) { |
|
| 28 | + throw new \InvalidArgumentException('Missing "exception" argument in FailedStorage constructor'); |
|
| 29 | + } |
|
| 30 | + } |
|
| 31 | + |
|
| 32 | + public function getId(): string { |
|
| 33 | + // we can't return anything sane here |
|
| 34 | + return 'failedstorage'; |
|
| 35 | + } |
|
| 36 | + |
|
| 37 | + public function mkdir(string $path): never { |
|
| 38 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 39 | + } |
|
| 40 | + |
|
| 41 | + public function rmdir(string $path): never { |
|
| 42 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 43 | + } |
|
| 44 | + |
|
| 45 | + public function opendir(string $path): never { |
|
| 46 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 47 | + } |
|
| 48 | + |
|
| 49 | + public function is_dir(string $path): never { |
|
| 50 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 51 | + } |
|
| 52 | + |
|
| 53 | + public function is_file(string $path): never { |
|
| 54 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 55 | + } |
|
| 56 | + |
|
| 57 | + public function stat(string $path): never { |
|
| 58 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 59 | + } |
|
| 60 | + |
|
| 61 | + public function filetype(string $path): never { |
|
| 62 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 63 | + } |
|
| 64 | + |
|
| 65 | + public function filesize(string $path): never { |
|
| 66 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 67 | + } |
|
| 68 | + |
|
| 69 | + public function isCreatable(string $path): never { |
|
| 70 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 71 | + } |
|
| 72 | + |
|
| 73 | + public function isReadable(string $path): never { |
|
| 74 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + public function isUpdatable(string $path): never { |
|
| 78 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 79 | + } |
|
| 80 | + |
|
| 81 | + public function isDeletable(string $path): never { |
|
| 82 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 83 | + } |
|
| 84 | + |
|
| 85 | + public function isSharable(string $path): never { |
|
| 86 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 87 | + } |
|
| 88 | + |
|
| 89 | + public function getPermissions(string $path): never { |
|
| 90 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 91 | + } |
|
| 92 | + |
|
| 93 | + public function file_exists(string $path): never { |
|
| 94 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 95 | + } |
|
| 96 | + |
|
| 97 | + public function filemtime(string $path): never { |
|
| 98 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 99 | + } |
|
| 100 | + |
|
| 101 | + public function file_get_contents(string $path): never { |
|
| 102 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + public function file_put_contents(string $path, mixed $data): never { |
|
| 106 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + public function unlink(string $path): never { |
|
| 110 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 111 | + } |
|
| 112 | + |
|
| 113 | + public function rename(string $source, string $target): never { |
|
| 114 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 115 | + } |
|
| 116 | + |
|
| 117 | + public function copy(string $source, string $target): never { |
|
| 118 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 119 | + } |
|
| 120 | + |
|
| 121 | + public function fopen(string $path, string $mode): never { |
|
| 122 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 123 | + } |
|
| 124 | + |
|
| 125 | + public function getMimeType(string $path): never { |
|
| 126 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 127 | + } |
|
| 128 | 128 | |
| 129 | - public function hash(string $type, string $path, bool $raw = false): never { |
|
| 130 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 131 | - } |
|
| 129 | + public function hash(string $type, string $path, bool $raw = false): never { |
|
| 130 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 131 | + } |
|
| 132 | 132 | |
| 133 | - public function free_space(string $path): never { |
|
| 134 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 135 | - } |
|
| 133 | + public function free_space(string $path): never { |
|
| 134 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 135 | + } |
|
| 136 | 136 | |
| 137 | - public function touch(string $path, ?int $mtime = null): never { |
|
| 138 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 139 | - } |
|
| 137 | + public function touch(string $path, ?int $mtime = null): never { |
|
| 138 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 139 | + } |
|
| 140 | 140 | |
| 141 | - public function getLocalFile(string $path): never { |
|
| 142 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 143 | - } |
|
| 141 | + public function getLocalFile(string $path): never { |
|
| 142 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 143 | + } |
|
| 144 | 144 | |
| 145 | - public function hasUpdated(string $path, int $time): never { |
|
| 146 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 147 | - } |
|
| 145 | + public function hasUpdated(string $path, int $time): never { |
|
| 146 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 147 | + } |
|
| 148 | 148 | |
| 149 | - public function getETag(string $path): never { |
|
| 150 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 151 | - } |
|
| 149 | + public function getETag(string $path): never { |
|
| 150 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 151 | + } |
|
| 152 | 152 | |
| 153 | - public function getDirectDownload(string $path): never { |
|
| 154 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 155 | - } |
|
| 153 | + public function getDirectDownload(string $path): never { |
|
| 154 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 155 | + } |
|
| 156 | 156 | |
| 157 | - public function getDirectDownloadById(string $fileId): never { |
|
| 158 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 159 | - } |
|
| 157 | + public function getDirectDownloadById(string $fileId): never { |
|
| 158 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 159 | + } |
|
| 160 | 160 | |
| 161 | - public function verifyPath(string $path, string $fileName): void { |
|
| 162 | - } |
|
| 161 | + public function verifyPath(string $path, string $fileName): void { |
|
| 162 | + } |
|
| 163 | 163 | |
| 164 | - public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never { |
|
| 165 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 166 | - } |
|
| 164 | + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): never { |
|
| 165 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 166 | + } |
|
| 167 | 167 | |
| 168 | - public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): never { |
|
| 169 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 170 | - } |
|
| 168 | + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): never { |
|
| 169 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 170 | + } |
|
| 171 | 171 | |
| 172 | - public function acquireLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 173 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 174 | - } |
|
| 172 | + public function acquireLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 173 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 174 | + } |
|
| 175 | 175 | |
| 176 | - public function releaseLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 177 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 178 | - } |
|
| 176 | + public function releaseLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 177 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 178 | + } |
|
| 179 | 179 | |
| 180 | - public function changeLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 181 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 182 | - } |
|
| 180 | + public function changeLock(string $path, int $type, ILockingProvider $provider): never { |
|
| 181 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 182 | + } |
|
| 183 | 183 | |
| 184 | - public function getAvailability(): never { |
|
| 185 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 186 | - } |
|
| 184 | + public function getAvailability(): never { |
|
| 185 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 186 | + } |
|
| 187 | 187 | |
| 188 | - public function setAvailability(bool $isAvailable): never { |
|
| 189 | - throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 190 | - } |
|
| 188 | + public function setAvailability(bool $isAvailable): never { |
|
| 189 | + throw new StorageNotAvailableException($this->e->getMessage(), $this->e->getCode(), $this->e); |
|
| 190 | + } |
|
| 191 | 191 | |
| 192 | - public function getCache(string $path = '', ?IStorage $storage = null): FailedCache { |
|
| 193 | - return new FailedCache(); |
|
| 194 | - } |
|
| 192 | + public function getCache(string $path = '', ?IStorage $storage = null): FailedCache { |
|
| 193 | + return new FailedCache(); |
|
| 194 | + } |
|
| 195 | 195 | } |
@@ -53,719 +53,719 @@ |
||
| 53 | 53 | * in classes which extend it, e.g. $this->stat() . |
| 54 | 54 | */ |
| 55 | 55 | abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage { |
| 56 | - use LocalTempFileTrait; |
|
| 57 | - |
|
| 58 | - protected ?Cache $cache = null; |
|
| 59 | - protected ?Scanner $scanner = null; |
|
| 60 | - protected ?Watcher $watcher = null; |
|
| 61 | - protected ?Propagator $propagator = null; |
|
| 62 | - protected $storageCache; |
|
| 63 | - protected ?Updater $updater = null; |
|
| 64 | - |
|
| 65 | - protected array $mountOptions = []; |
|
| 66 | - protected $owner = null; |
|
| 67 | - |
|
| 68 | - private ?bool $shouldLogLocks = null; |
|
| 69 | - private ?LoggerInterface $logger = null; |
|
| 70 | - private ?IFilenameValidator $filenameValidator = null; |
|
| 71 | - |
|
| 72 | - public function __construct(array $parameters) { |
|
| 73 | - } |
|
| 74 | - |
|
| 75 | - protected function remove(string $path): bool { |
|
| 76 | - if ($this->file_exists($path)) { |
|
| 77 | - if ($this->is_dir($path)) { |
|
| 78 | - return $this->rmdir($path); |
|
| 79 | - } elseif ($this->is_file($path)) { |
|
| 80 | - return $this->unlink($path); |
|
| 81 | - } |
|
| 82 | - } |
|
| 83 | - return false; |
|
| 84 | - } |
|
| 85 | - |
|
| 86 | - public function is_dir(string $path): bool { |
|
| 87 | - return $this->filetype($path) === 'dir'; |
|
| 88 | - } |
|
| 89 | - |
|
| 90 | - public function is_file(string $path): bool { |
|
| 91 | - return $this->filetype($path) === 'file'; |
|
| 92 | - } |
|
| 93 | - |
|
| 94 | - public function filesize(string $path): int|float|false { |
|
| 95 | - if ($this->is_dir($path)) { |
|
| 96 | - return 0; //by definition |
|
| 97 | - } else { |
|
| 98 | - $stat = $this->stat($path); |
|
| 99 | - return isset($stat['size']) ? $stat['size'] : 0; |
|
| 100 | - } |
|
| 101 | - } |
|
| 102 | - |
|
| 103 | - public function isReadable(string $path): bool { |
|
| 104 | - // at least check whether it exists |
|
| 105 | - // subclasses might want to implement this more thoroughly |
|
| 106 | - return $this->file_exists($path); |
|
| 107 | - } |
|
| 108 | - |
|
| 109 | - public function isUpdatable(string $path): bool { |
|
| 110 | - // at least check whether it exists |
|
| 111 | - // subclasses might want to implement this more thoroughly |
|
| 112 | - // a non-existing file/folder isn't updatable |
|
| 113 | - return $this->file_exists($path); |
|
| 114 | - } |
|
| 115 | - |
|
| 116 | - public function isCreatable(string $path): bool { |
|
| 117 | - if ($this->is_dir($path) && $this->isUpdatable($path)) { |
|
| 118 | - return true; |
|
| 119 | - } |
|
| 120 | - return false; |
|
| 121 | - } |
|
| 122 | - |
|
| 123 | - public function isDeletable(string $path): bool { |
|
| 124 | - if ($path === '' || $path === '/') { |
|
| 125 | - return $this->isUpdatable($path); |
|
| 126 | - } |
|
| 127 | - $parent = dirname($path); |
|
| 128 | - return $this->isUpdatable($parent) && $this->isUpdatable($path); |
|
| 129 | - } |
|
| 130 | - |
|
| 131 | - public function isSharable(string $path): bool { |
|
| 132 | - return $this->isReadable($path); |
|
| 133 | - } |
|
| 134 | - |
|
| 135 | - public function getPermissions(string $path): int { |
|
| 136 | - $permissions = 0; |
|
| 137 | - if ($this->isCreatable($path)) { |
|
| 138 | - $permissions |= \OCP\Constants::PERMISSION_CREATE; |
|
| 139 | - } |
|
| 140 | - if ($this->isReadable($path)) { |
|
| 141 | - $permissions |= \OCP\Constants::PERMISSION_READ; |
|
| 142 | - } |
|
| 143 | - if ($this->isUpdatable($path)) { |
|
| 144 | - $permissions |= \OCP\Constants::PERMISSION_UPDATE; |
|
| 145 | - } |
|
| 146 | - if ($this->isDeletable($path)) { |
|
| 147 | - $permissions |= \OCP\Constants::PERMISSION_DELETE; |
|
| 148 | - } |
|
| 149 | - if ($this->isSharable($path)) { |
|
| 150 | - $permissions |= \OCP\Constants::PERMISSION_SHARE; |
|
| 151 | - } |
|
| 152 | - return $permissions; |
|
| 153 | - } |
|
| 154 | - |
|
| 155 | - public function filemtime(string $path): int|false { |
|
| 156 | - $stat = $this->stat($path); |
|
| 157 | - if (isset($stat['mtime']) && $stat['mtime'] > 0) { |
|
| 158 | - return $stat['mtime']; |
|
| 159 | - } else { |
|
| 160 | - return 0; |
|
| 161 | - } |
|
| 162 | - } |
|
| 163 | - |
|
| 164 | - public function file_get_contents(string $path): string|false { |
|
| 165 | - $handle = $this->fopen($path, 'r'); |
|
| 166 | - if (!$handle) { |
|
| 167 | - return false; |
|
| 168 | - } |
|
| 169 | - $data = stream_get_contents($handle); |
|
| 170 | - fclose($handle); |
|
| 171 | - return $data; |
|
| 172 | - } |
|
| 173 | - |
|
| 174 | - public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 175 | - $handle = $this->fopen($path, 'w'); |
|
| 176 | - if (!$handle) { |
|
| 177 | - return false; |
|
| 178 | - } |
|
| 179 | - $this->removeCachedFile($path); |
|
| 180 | - $count = fwrite($handle, $data); |
|
| 181 | - fclose($handle); |
|
| 182 | - return $count; |
|
| 183 | - } |
|
| 184 | - |
|
| 185 | - public function rename(string $source, string $target): bool { |
|
| 186 | - $this->remove($target); |
|
| 187 | - |
|
| 188 | - $this->removeCachedFile($source); |
|
| 189 | - return $this->copy($source, $target) && $this->remove($source); |
|
| 190 | - } |
|
| 191 | - |
|
| 192 | - public function copy(string $source, string $target): bool { |
|
| 193 | - if ($this->is_dir($source)) { |
|
| 194 | - $this->remove($target); |
|
| 195 | - $dir = $this->opendir($source); |
|
| 196 | - $this->mkdir($target); |
|
| 197 | - while (($file = readdir($dir)) !== false) { |
|
| 198 | - if (!Filesystem::isIgnoredDir($file)) { |
|
| 199 | - if (!$this->copy($source . '/' . $file, $target . '/' . $file)) { |
|
| 200 | - closedir($dir); |
|
| 201 | - return false; |
|
| 202 | - } |
|
| 203 | - } |
|
| 204 | - } |
|
| 205 | - closedir($dir); |
|
| 206 | - return true; |
|
| 207 | - } else { |
|
| 208 | - $sourceStream = $this->fopen($source, 'r'); |
|
| 209 | - $targetStream = $this->fopen($target, 'w'); |
|
| 210 | - [, $result] = Files::streamCopy($sourceStream, $targetStream, true); |
|
| 211 | - if (!$result) { |
|
| 212 | - Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target"); |
|
| 213 | - } |
|
| 214 | - $this->removeCachedFile($target); |
|
| 215 | - return $result; |
|
| 216 | - } |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - public function getMimeType(string $path): string|false { |
|
| 220 | - if ($this->is_dir($path)) { |
|
| 221 | - return 'httpd/unix-directory'; |
|
| 222 | - } elseif ($this->file_exists($path)) { |
|
| 223 | - return \OC::$server->getMimeTypeDetector()->detectPath($path); |
|
| 224 | - } else { |
|
| 225 | - return false; |
|
| 226 | - } |
|
| 227 | - } |
|
| 228 | - |
|
| 229 | - public function hash(string $type, string $path, bool $raw = false): string|false { |
|
| 230 | - $fh = $this->fopen($path, 'rb'); |
|
| 231 | - if (!$fh) { |
|
| 232 | - return false; |
|
| 233 | - } |
|
| 234 | - $ctx = hash_init($type); |
|
| 235 | - hash_update_stream($ctx, $fh); |
|
| 236 | - fclose($fh); |
|
| 237 | - return hash_final($ctx, $raw); |
|
| 238 | - } |
|
| 239 | - |
|
| 240 | - public function getLocalFile(string $path): string|false { |
|
| 241 | - return $this->getCachedFile($path); |
|
| 242 | - } |
|
| 243 | - |
|
| 244 | - private function addLocalFolder(string $path, string $target): void { |
|
| 245 | - $dh = $this->opendir($path); |
|
| 246 | - if (is_resource($dh)) { |
|
| 247 | - while (($file = readdir($dh)) !== false) { |
|
| 248 | - if (!Filesystem::isIgnoredDir($file)) { |
|
| 249 | - if ($this->is_dir($path . '/' . $file)) { |
|
| 250 | - mkdir($target . '/' . $file); |
|
| 251 | - $this->addLocalFolder($path . '/' . $file, $target . '/' . $file); |
|
| 252 | - } else { |
|
| 253 | - $tmp = $this->toTmpFile($path . '/' . $file); |
|
| 254 | - rename($tmp, $target . '/' . $file); |
|
| 255 | - } |
|
| 256 | - } |
|
| 257 | - } |
|
| 258 | - } |
|
| 259 | - } |
|
| 260 | - |
|
| 261 | - protected function searchInDir(string $query, string $dir = ''): array { |
|
| 262 | - $files = []; |
|
| 263 | - $dh = $this->opendir($dir); |
|
| 264 | - if (is_resource($dh)) { |
|
| 265 | - while (($item = readdir($dh)) !== false) { |
|
| 266 | - if (Filesystem::isIgnoredDir($item)) { |
|
| 267 | - continue; |
|
| 268 | - } |
|
| 269 | - if (strstr(strtolower($item), strtolower($query)) !== false) { |
|
| 270 | - $files[] = $dir . '/' . $item; |
|
| 271 | - } |
|
| 272 | - if ($this->is_dir($dir . '/' . $item)) { |
|
| 273 | - $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item)); |
|
| 274 | - } |
|
| 275 | - } |
|
| 276 | - } |
|
| 277 | - closedir($dh); |
|
| 278 | - return $files; |
|
| 279 | - } |
|
| 280 | - |
|
| 281 | - /** |
|
| 282 | - * @inheritDoc |
|
| 283 | - * Check if a file or folder has been updated since $time |
|
| 284 | - * |
|
| 285 | - * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking |
|
| 286 | - * the mtime should always return false here. As a result storage implementations that always return false expect |
|
| 287 | - * exclusive access to the backend and will not pick up files that have been added in a way that circumvents |
|
| 288 | - * Nextcloud filesystem. |
|
| 289 | - */ |
|
| 290 | - public function hasUpdated(string $path, int $time): bool { |
|
| 291 | - return $this->filemtime($path) > $time; |
|
| 292 | - } |
|
| 293 | - |
|
| 294 | - protected function getCacheDependencies(): CacheDependencies { |
|
| 295 | - static $dependencies = null; |
|
| 296 | - if (!$dependencies) { |
|
| 297 | - $dependencies = Server::get(CacheDependencies::class); |
|
| 298 | - } |
|
| 299 | - return $dependencies; |
|
| 300 | - } |
|
| 301 | - |
|
| 302 | - public function getCache(string $path = '', ?IStorage $storage = null): ICache { |
|
| 303 | - if (!$storage) { |
|
| 304 | - $storage = $this; |
|
| 305 | - } |
|
| 306 | - /** @var self $storage */ |
|
| 307 | - if (!isset($storage->cache)) { |
|
| 308 | - $storage->cache = new Cache($storage, $this->getCacheDependencies()); |
|
| 309 | - } |
|
| 310 | - return $storage->cache; |
|
| 311 | - } |
|
| 312 | - |
|
| 313 | - public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { |
|
| 314 | - if (!$storage) { |
|
| 315 | - $storage = $this; |
|
| 316 | - } |
|
| 317 | - if (!$storage->instanceOfStorage(self::class)) { |
|
| 318 | - throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 319 | - } |
|
| 320 | - if (!isset($storage->scanner)) { |
|
| 321 | - $storage->scanner = new Scanner($storage); |
|
| 322 | - } |
|
| 323 | - return $storage->scanner; |
|
| 324 | - } |
|
| 325 | - |
|
| 326 | - public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { |
|
| 327 | - if (!$storage) { |
|
| 328 | - $storage = $this; |
|
| 329 | - } |
|
| 330 | - if (!isset($this->watcher)) { |
|
| 331 | - $this->watcher = new Watcher($storage); |
|
| 332 | - $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER); |
|
| 333 | - $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy)); |
|
| 334 | - } |
|
| 335 | - return $this->watcher; |
|
| 336 | - } |
|
| 337 | - |
|
| 338 | - public function getPropagator(?IStorage $storage = null): IPropagator { |
|
| 339 | - if (!$storage) { |
|
| 340 | - $storage = $this; |
|
| 341 | - } |
|
| 342 | - if (!$storage->instanceOfStorage(self::class)) { |
|
| 343 | - throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 344 | - } |
|
| 345 | - /** @var self $storage */ |
|
| 346 | - if (!isset($storage->propagator)) { |
|
| 347 | - $config = Server::get(IConfig::class); |
|
| 348 | - $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]); |
|
| 349 | - } |
|
| 350 | - return $storage->propagator; |
|
| 351 | - } |
|
| 352 | - |
|
| 353 | - public function getUpdater(?IStorage $storage = null): IUpdater { |
|
| 354 | - if (!$storage) { |
|
| 355 | - $storage = $this; |
|
| 356 | - } |
|
| 357 | - if (!$storage->instanceOfStorage(self::class)) { |
|
| 358 | - throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 359 | - } |
|
| 360 | - /** @var self $storage */ |
|
| 361 | - if (!isset($storage->updater)) { |
|
| 362 | - $storage->updater = new Updater($storage); |
|
| 363 | - } |
|
| 364 | - return $storage->updater; |
|
| 365 | - } |
|
| 366 | - |
|
| 367 | - public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage { |
|
| 368 | - /** @var Cache $cache */ |
|
| 369 | - $cache = $this->getCache(storage: $storage); |
|
| 370 | - return $cache->getStorageCache(); |
|
| 371 | - } |
|
| 372 | - |
|
| 373 | - public function getOwner(string $path): string|false { |
|
| 374 | - if ($this->owner === null) { |
|
| 375 | - $this->owner = \OC_User::getUser(); |
|
| 376 | - } |
|
| 377 | - |
|
| 378 | - return $this->owner; |
|
| 379 | - } |
|
| 380 | - |
|
| 381 | - public function getETag(string $path): string|false { |
|
| 382 | - return uniqid(); |
|
| 383 | - } |
|
| 384 | - |
|
| 385 | - /** |
|
| 386 | - * clean a path, i.e. remove all redundant '.' and '..' |
|
| 387 | - * making sure that it can't point to higher than '/' |
|
| 388 | - * |
|
| 389 | - * @param string $path The path to clean |
|
| 390 | - * @return string cleaned path |
|
| 391 | - */ |
|
| 392 | - public function cleanPath(string $path): string { |
|
| 393 | - if (strlen($path) == 0 || $path[0] != '/') { |
|
| 394 | - $path = '/' . $path; |
|
| 395 | - } |
|
| 396 | - |
|
| 397 | - $output = []; |
|
| 398 | - foreach (explode('/', $path) as $chunk) { |
|
| 399 | - if ($chunk == '..') { |
|
| 400 | - array_pop($output); |
|
| 401 | - } elseif ($chunk == '.') { |
|
| 402 | - } else { |
|
| 403 | - $output[] = $chunk; |
|
| 404 | - } |
|
| 405 | - } |
|
| 406 | - return implode('/', $output); |
|
| 407 | - } |
|
| 408 | - |
|
| 409 | - /** |
|
| 410 | - * Test a storage for availability |
|
| 411 | - */ |
|
| 412 | - public function test(): bool { |
|
| 413 | - try { |
|
| 414 | - if ($this->stat('')) { |
|
| 415 | - return true; |
|
| 416 | - } |
|
| 417 | - Server::get(LoggerInterface::class)->info('External storage not available: stat() failed'); |
|
| 418 | - return false; |
|
| 419 | - } catch (\Exception $e) { |
|
| 420 | - Server::get(LoggerInterface::class)->warning( |
|
| 421 | - 'External storage not available: ' . $e->getMessage(), |
|
| 422 | - ['exception' => $e] |
|
| 423 | - ); |
|
| 424 | - return false; |
|
| 425 | - } |
|
| 426 | - } |
|
| 427 | - |
|
| 428 | - public function free_space(string $path): int|float|false { |
|
| 429 | - return \OCP\Files\FileInfo::SPACE_UNKNOWN; |
|
| 430 | - } |
|
| 431 | - |
|
| 432 | - public function isLocal(): bool { |
|
| 433 | - // the common implementation returns a temporary file by |
|
| 434 | - // default, which is not local |
|
| 435 | - return false; |
|
| 436 | - } |
|
| 437 | - |
|
| 438 | - /** |
|
| 439 | - * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class |
|
| 440 | - */ |
|
| 441 | - public function instanceOfStorage(string $class): bool { |
|
| 442 | - if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') { |
|
| 443 | - // FIXME Temporary fix to keep existing checks working |
|
| 444 | - $class = '\OCA\Files_Sharing\SharedStorage'; |
|
| 445 | - } |
|
| 446 | - return is_a($this, $class); |
|
| 447 | - } |
|
| 448 | - |
|
| 449 | - #[Override] |
|
| 450 | - public function getDirectDownload(string $path): array|false { |
|
| 451 | - return false; |
|
| 452 | - } |
|
| 453 | - |
|
| 454 | - #[Override] |
|
| 455 | - public function getDirectDownloadById(string $fileId): array|false { |
|
| 456 | - return false; |
|
| 457 | - } |
|
| 458 | - |
|
| 459 | - public function verifyPath(string $path, string $fileName): void { |
|
| 460 | - $this->getFilenameValidator() |
|
| 461 | - ->validateFilename($fileName); |
|
| 462 | - |
|
| 463 | - // verify also the path is valid |
|
| 464 | - if ($path && $path !== '/' && $path !== '.') { |
|
| 465 | - try { |
|
| 466 | - $this->verifyPath(dirname($path), basename($path)); |
|
| 467 | - } catch (InvalidPathException $e) { |
|
| 468 | - // Ignore invalid file type exceptions on directories |
|
| 469 | - if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) { |
|
| 470 | - $l = \OCP\Util::getL10N('lib'); |
|
| 471 | - throw new InvalidPathException($l->t('Invalid parent path'), previous: $e); |
|
| 472 | - } |
|
| 473 | - } |
|
| 474 | - } |
|
| 475 | - } |
|
| 476 | - |
|
| 477 | - /** |
|
| 478 | - * Get the filename validator |
|
| 479 | - * (cached for performance) |
|
| 480 | - */ |
|
| 481 | - protected function getFilenameValidator(): IFilenameValidator { |
|
| 482 | - if ($this->filenameValidator === null) { |
|
| 483 | - $this->filenameValidator = Server::get(IFilenameValidator::class); |
|
| 484 | - } |
|
| 485 | - return $this->filenameValidator; |
|
| 486 | - } |
|
| 487 | - |
|
| 488 | - public function setMountOptions(array $options): void { |
|
| 489 | - $this->mountOptions = $options; |
|
| 490 | - } |
|
| 491 | - |
|
| 492 | - public function getMountOption(string $name, mixed $default = null): mixed { |
|
| 493 | - return $this->mountOptions[$name] ?? $default; |
|
| 494 | - } |
|
| 495 | - |
|
| 496 | - public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { |
|
| 497 | - if ($sourceStorage === $this) { |
|
| 498 | - return $this->copy($sourceInternalPath, $targetInternalPath); |
|
| 499 | - } |
|
| 500 | - |
|
| 501 | - if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 502 | - $dh = $sourceStorage->opendir($sourceInternalPath); |
|
| 503 | - $result = $this->mkdir($targetInternalPath); |
|
| 504 | - if (is_resource($dh)) { |
|
| 505 | - $result = true; |
|
| 506 | - while ($result && ($file = readdir($dh)) !== false) { |
|
| 507 | - if (!Filesystem::isIgnoredDir($file)) { |
|
| 508 | - $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file); |
|
| 509 | - } |
|
| 510 | - } |
|
| 511 | - } |
|
| 512 | - } else { |
|
| 513 | - $source = $sourceStorage->fopen($sourceInternalPath, 'r'); |
|
| 514 | - $result = false; |
|
| 515 | - if ($source) { |
|
| 516 | - try { |
|
| 517 | - $this->writeStream($targetInternalPath, $source); |
|
| 518 | - $result = true; |
|
| 519 | - } catch (\Exception $e) { |
|
| 520 | - Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]); |
|
| 521 | - } |
|
| 522 | - } |
|
| 523 | - |
|
| 524 | - if ($result && $preserveMtime) { |
|
| 525 | - $mtime = $sourceStorage->filemtime($sourceInternalPath); |
|
| 526 | - $this->touch($targetInternalPath, is_int($mtime) ? $mtime : null); |
|
| 527 | - } |
|
| 528 | - |
|
| 529 | - if (!$result) { |
|
| 530 | - // delete partially written target file |
|
| 531 | - $this->unlink($targetInternalPath); |
|
| 532 | - // delete cache entry that was created by fopen |
|
| 533 | - $this->getCache()->remove($targetInternalPath); |
|
| 534 | - } |
|
| 535 | - } |
|
| 536 | - return (bool)$result; |
|
| 537 | - } |
|
| 538 | - |
|
| 539 | - /** |
|
| 540 | - * Check if a storage is the same as the current one, including wrapped storages |
|
| 541 | - */ |
|
| 542 | - private function isSameStorage(IStorage $storage): bool { |
|
| 543 | - while ($storage->instanceOfStorage(Wrapper::class)) { |
|
| 544 | - /** |
|
| 545 | - * @var Wrapper $storage |
|
| 546 | - */ |
|
| 547 | - $storage = $storage->getWrapperStorage(); |
|
| 548 | - } |
|
| 549 | - |
|
| 550 | - return $storage === $this; |
|
| 551 | - } |
|
| 552 | - |
|
| 553 | - public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 554 | - if ( |
|
| 555 | - !$sourceStorage->instanceOfStorage(Encryption::class) |
|
| 556 | - && $this->isSameStorage($sourceStorage) |
|
| 557 | - ) { |
|
| 558 | - // resolve any jailed paths |
|
| 559 | - while ($sourceStorage->instanceOfStorage(Jail::class)) { |
|
| 560 | - /** |
|
| 561 | - * @var Jail $sourceStorage |
|
| 562 | - */ |
|
| 563 | - $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); |
|
| 564 | - $sourceStorage = $sourceStorage->getUnjailedStorage(); |
|
| 565 | - } |
|
| 566 | - |
|
| 567 | - return $this->rename($sourceInternalPath, $targetInternalPath); |
|
| 568 | - } |
|
| 569 | - |
|
| 570 | - if (!$sourceStorage->isDeletable($sourceInternalPath)) { |
|
| 571 | - return false; |
|
| 572 | - } |
|
| 573 | - |
|
| 574 | - $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); |
|
| 575 | - if ($result) { |
|
| 576 | - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { |
|
| 577 | - /** @var ObjectStoreStorage $sourceStorage */ |
|
| 578 | - $sourceStorage->setPreserveCacheOnDelete(true); |
|
| 579 | - } |
|
| 580 | - try { |
|
| 581 | - if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 582 | - $result = $sourceStorage->rmdir($sourceInternalPath); |
|
| 583 | - } else { |
|
| 584 | - $result = $sourceStorage->unlink($sourceInternalPath); |
|
| 585 | - } |
|
| 586 | - } finally { |
|
| 587 | - if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { |
|
| 588 | - /** @var ObjectStoreStorage $sourceStorage */ |
|
| 589 | - $sourceStorage->setPreserveCacheOnDelete(false); |
|
| 590 | - } |
|
| 591 | - } |
|
| 592 | - } |
|
| 593 | - return $result; |
|
| 594 | - } |
|
| 595 | - |
|
| 596 | - public function getMetaData(string $path): ?array { |
|
| 597 | - if (Filesystem::isFileBlacklisted($path)) { |
|
| 598 | - throw new ForbiddenException('Invalid path: ' . $path, false); |
|
| 599 | - } |
|
| 600 | - |
|
| 601 | - $permissions = $this->getPermissions($path); |
|
| 602 | - if (!$permissions & \OCP\Constants::PERMISSION_READ) { |
|
| 603 | - //can't read, nothing we can do |
|
| 604 | - return null; |
|
| 605 | - } |
|
| 606 | - |
|
| 607 | - $data = []; |
|
| 608 | - $data['mimetype'] = $this->getMimeType($path); |
|
| 609 | - $data['mtime'] = $this->filemtime($path); |
|
| 610 | - if ($data['mtime'] === false) { |
|
| 611 | - $data['mtime'] = time(); |
|
| 612 | - } |
|
| 613 | - if ($data['mimetype'] == 'httpd/unix-directory') { |
|
| 614 | - $data['size'] = -1; //unknown |
|
| 615 | - } else { |
|
| 616 | - $data['size'] = $this->filesize($path); |
|
| 617 | - } |
|
| 618 | - $data['etag'] = $this->getETag($path); |
|
| 619 | - $data['storage_mtime'] = $data['mtime']; |
|
| 620 | - $data['permissions'] = $permissions; |
|
| 621 | - $data['name'] = basename($path); |
|
| 622 | - |
|
| 623 | - return $data; |
|
| 624 | - } |
|
| 625 | - |
|
| 626 | - public function acquireLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 627 | - $logger = $this->getLockLogger(); |
|
| 628 | - if ($logger) { |
|
| 629 | - $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 630 | - $logger->info( |
|
| 631 | - sprintf( |
|
| 632 | - 'acquire %s lock on "%s" on storage "%s"', |
|
| 633 | - $typeString, |
|
| 634 | - $path, |
|
| 635 | - $this->getId() |
|
| 636 | - ), |
|
| 637 | - [ |
|
| 638 | - 'app' => 'locking', |
|
| 639 | - ] |
|
| 640 | - ); |
|
| 641 | - } |
|
| 642 | - try { |
|
| 643 | - $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path); |
|
| 644 | - } catch (LockedException $e) { |
|
| 645 | - $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 646 | - if ($logger) { |
|
| 647 | - $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 648 | - } |
|
| 649 | - throw $e; |
|
| 650 | - } |
|
| 651 | - } |
|
| 652 | - |
|
| 653 | - public function releaseLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 654 | - $logger = $this->getLockLogger(); |
|
| 655 | - if ($logger) { |
|
| 656 | - $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 657 | - $logger->info( |
|
| 658 | - sprintf( |
|
| 659 | - 'release %s lock on "%s" on storage "%s"', |
|
| 660 | - $typeString, |
|
| 661 | - $path, |
|
| 662 | - $this->getId() |
|
| 663 | - ), |
|
| 664 | - [ |
|
| 665 | - 'app' => 'locking', |
|
| 666 | - ] |
|
| 667 | - ); |
|
| 668 | - } |
|
| 669 | - try { |
|
| 670 | - $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); |
|
| 671 | - } catch (LockedException $e) { |
|
| 672 | - $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 673 | - if ($logger) { |
|
| 674 | - $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 675 | - } |
|
| 676 | - throw $e; |
|
| 677 | - } |
|
| 678 | - } |
|
| 679 | - |
|
| 680 | - public function changeLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 681 | - $logger = $this->getLockLogger(); |
|
| 682 | - if ($logger) { |
|
| 683 | - $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 684 | - $logger->info( |
|
| 685 | - sprintf( |
|
| 686 | - 'change lock on "%s" to %s on storage "%s"', |
|
| 687 | - $path, |
|
| 688 | - $typeString, |
|
| 689 | - $this->getId() |
|
| 690 | - ), |
|
| 691 | - [ |
|
| 692 | - 'app' => 'locking', |
|
| 693 | - ] |
|
| 694 | - ); |
|
| 695 | - } |
|
| 696 | - try { |
|
| 697 | - $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); |
|
| 698 | - } catch (LockedException $e) { |
|
| 699 | - $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 700 | - if ($logger) { |
|
| 701 | - $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 702 | - } |
|
| 703 | - throw $e; |
|
| 704 | - } |
|
| 705 | - } |
|
| 706 | - |
|
| 707 | - private function getLockLogger(): ?LoggerInterface { |
|
| 708 | - if (is_null($this->shouldLogLocks)) { |
|
| 709 | - $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false); |
|
| 710 | - $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null; |
|
| 711 | - } |
|
| 712 | - return $this->logger; |
|
| 713 | - } |
|
| 714 | - |
|
| 715 | - /** |
|
| 716 | - * @return array{available: bool, last_checked: int} |
|
| 717 | - */ |
|
| 718 | - public function getAvailability(): array { |
|
| 719 | - return $this->getStorageCache()->getAvailability(); |
|
| 720 | - } |
|
| 721 | - |
|
| 722 | - public function setAvailability(bool $isAvailable): void { |
|
| 723 | - $this->getStorageCache()->setAvailability($isAvailable); |
|
| 724 | - } |
|
| 725 | - |
|
| 726 | - public function setOwner(?string $user): void { |
|
| 727 | - $this->owner = $user; |
|
| 728 | - } |
|
| 729 | - |
|
| 730 | - public function needsPartFile(): bool { |
|
| 731 | - return true; |
|
| 732 | - } |
|
| 733 | - |
|
| 734 | - public function writeStream(string $path, $stream, ?int $size = null): int { |
|
| 735 | - $target = $this->fopen($path, 'w'); |
|
| 736 | - if (!$target) { |
|
| 737 | - throw new GenericFileException("Failed to open $path for writing"); |
|
| 738 | - } |
|
| 739 | - try { |
|
| 740 | - [$count, $result] = Files::streamCopy($stream, $target, true); |
|
| 741 | - if (!$result) { |
|
| 742 | - throw new GenericFileException('Failed to copy stream'); |
|
| 743 | - } |
|
| 744 | - } finally { |
|
| 745 | - fclose($target); |
|
| 746 | - fclose($stream); |
|
| 747 | - } |
|
| 748 | - return $count; |
|
| 749 | - } |
|
| 750 | - |
|
| 751 | - public function getDirectoryContent(string $directory): \Traversable { |
|
| 752 | - $dh = $this->opendir($directory); |
|
| 753 | - |
|
| 754 | - if ($dh === false) { |
|
| 755 | - throw new StorageNotAvailableException('Directory listing failed'); |
|
| 756 | - } |
|
| 757 | - |
|
| 758 | - if (is_resource($dh)) { |
|
| 759 | - $basePath = rtrim($directory, '/'); |
|
| 760 | - while (($file = readdir($dh)) !== false) { |
|
| 761 | - if (!Filesystem::isIgnoredDir($file)) { |
|
| 762 | - $childPath = $basePath . '/' . trim($file, '/'); |
|
| 763 | - $metadata = $this->getMetaData($childPath); |
|
| 764 | - if ($metadata !== null) { |
|
| 765 | - yield $metadata; |
|
| 766 | - } |
|
| 767 | - } |
|
| 768 | - } |
|
| 769 | - } |
|
| 770 | - } |
|
| 56 | + use LocalTempFileTrait; |
|
| 57 | + |
|
| 58 | + protected ?Cache $cache = null; |
|
| 59 | + protected ?Scanner $scanner = null; |
|
| 60 | + protected ?Watcher $watcher = null; |
|
| 61 | + protected ?Propagator $propagator = null; |
|
| 62 | + protected $storageCache; |
|
| 63 | + protected ?Updater $updater = null; |
|
| 64 | + |
|
| 65 | + protected array $mountOptions = []; |
|
| 66 | + protected $owner = null; |
|
| 67 | + |
|
| 68 | + private ?bool $shouldLogLocks = null; |
|
| 69 | + private ?LoggerInterface $logger = null; |
|
| 70 | + private ?IFilenameValidator $filenameValidator = null; |
|
| 71 | + |
|
| 72 | + public function __construct(array $parameters) { |
|
| 73 | + } |
|
| 74 | + |
|
| 75 | + protected function remove(string $path): bool { |
|
| 76 | + if ($this->file_exists($path)) { |
|
| 77 | + if ($this->is_dir($path)) { |
|
| 78 | + return $this->rmdir($path); |
|
| 79 | + } elseif ($this->is_file($path)) { |
|
| 80 | + return $this->unlink($path); |
|
| 81 | + } |
|
| 82 | + } |
|
| 83 | + return false; |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + public function is_dir(string $path): bool { |
|
| 87 | + return $this->filetype($path) === 'dir'; |
|
| 88 | + } |
|
| 89 | + |
|
| 90 | + public function is_file(string $path): bool { |
|
| 91 | + return $this->filetype($path) === 'file'; |
|
| 92 | + } |
|
| 93 | + |
|
| 94 | + public function filesize(string $path): int|float|false { |
|
| 95 | + if ($this->is_dir($path)) { |
|
| 96 | + return 0; //by definition |
|
| 97 | + } else { |
|
| 98 | + $stat = $this->stat($path); |
|
| 99 | + return isset($stat['size']) ? $stat['size'] : 0; |
|
| 100 | + } |
|
| 101 | + } |
|
| 102 | + |
|
| 103 | + public function isReadable(string $path): bool { |
|
| 104 | + // at least check whether it exists |
|
| 105 | + // subclasses might want to implement this more thoroughly |
|
| 106 | + return $this->file_exists($path); |
|
| 107 | + } |
|
| 108 | + |
|
| 109 | + public function isUpdatable(string $path): bool { |
|
| 110 | + // at least check whether it exists |
|
| 111 | + // subclasses might want to implement this more thoroughly |
|
| 112 | + // a non-existing file/folder isn't updatable |
|
| 113 | + return $this->file_exists($path); |
|
| 114 | + } |
|
| 115 | + |
|
| 116 | + public function isCreatable(string $path): bool { |
|
| 117 | + if ($this->is_dir($path) && $this->isUpdatable($path)) { |
|
| 118 | + return true; |
|
| 119 | + } |
|
| 120 | + return false; |
|
| 121 | + } |
|
| 122 | + |
|
| 123 | + public function isDeletable(string $path): bool { |
|
| 124 | + if ($path === '' || $path === '/') { |
|
| 125 | + return $this->isUpdatable($path); |
|
| 126 | + } |
|
| 127 | + $parent = dirname($path); |
|
| 128 | + return $this->isUpdatable($parent) && $this->isUpdatable($path); |
|
| 129 | + } |
|
| 130 | + |
|
| 131 | + public function isSharable(string $path): bool { |
|
| 132 | + return $this->isReadable($path); |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + public function getPermissions(string $path): int { |
|
| 136 | + $permissions = 0; |
|
| 137 | + if ($this->isCreatable($path)) { |
|
| 138 | + $permissions |= \OCP\Constants::PERMISSION_CREATE; |
|
| 139 | + } |
|
| 140 | + if ($this->isReadable($path)) { |
|
| 141 | + $permissions |= \OCP\Constants::PERMISSION_READ; |
|
| 142 | + } |
|
| 143 | + if ($this->isUpdatable($path)) { |
|
| 144 | + $permissions |= \OCP\Constants::PERMISSION_UPDATE; |
|
| 145 | + } |
|
| 146 | + if ($this->isDeletable($path)) { |
|
| 147 | + $permissions |= \OCP\Constants::PERMISSION_DELETE; |
|
| 148 | + } |
|
| 149 | + if ($this->isSharable($path)) { |
|
| 150 | + $permissions |= \OCP\Constants::PERMISSION_SHARE; |
|
| 151 | + } |
|
| 152 | + return $permissions; |
|
| 153 | + } |
|
| 154 | + |
|
| 155 | + public function filemtime(string $path): int|false { |
|
| 156 | + $stat = $this->stat($path); |
|
| 157 | + if (isset($stat['mtime']) && $stat['mtime'] > 0) { |
|
| 158 | + return $stat['mtime']; |
|
| 159 | + } else { |
|
| 160 | + return 0; |
|
| 161 | + } |
|
| 162 | + } |
|
| 163 | + |
|
| 164 | + public function file_get_contents(string $path): string|false { |
|
| 165 | + $handle = $this->fopen($path, 'r'); |
|
| 166 | + if (!$handle) { |
|
| 167 | + return false; |
|
| 168 | + } |
|
| 169 | + $data = stream_get_contents($handle); |
|
| 170 | + fclose($handle); |
|
| 171 | + return $data; |
|
| 172 | + } |
|
| 173 | + |
|
| 174 | + public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 175 | + $handle = $this->fopen($path, 'w'); |
|
| 176 | + if (!$handle) { |
|
| 177 | + return false; |
|
| 178 | + } |
|
| 179 | + $this->removeCachedFile($path); |
|
| 180 | + $count = fwrite($handle, $data); |
|
| 181 | + fclose($handle); |
|
| 182 | + return $count; |
|
| 183 | + } |
|
| 184 | + |
|
| 185 | + public function rename(string $source, string $target): bool { |
|
| 186 | + $this->remove($target); |
|
| 187 | + |
|
| 188 | + $this->removeCachedFile($source); |
|
| 189 | + return $this->copy($source, $target) && $this->remove($source); |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + public function copy(string $source, string $target): bool { |
|
| 193 | + if ($this->is_dir($source)) { |
|
| 194 | + $this->remove($target); |
|
| 195 | + $dir = $this->opendir($source); |
|
| 196 | + $this->mkdir($target); |
|
| 197 | + while (($file = readdir($dir)) !== false) { |
|
| 198 | + if (!Filesystem::isIgnoredDir($file)) { |
|
| 199 | + if (!$this->copy($source . '/' . $file, $target . '/' . $file)) { |
|
| 200 | + closedir($dir); |
|
| 201 | + return false; |
|
| 202 | + } |
|
| 203 | + } |
|
| 204 | + } |
|
| 205 | + closedir($dir); |
|
| 206 | + return true; |
|
| 207 | + } else { |
|
| 208 | + $sourceStream = $this->fopen($source, 'r'); |
|
| 209 | + $targetStream = $this->fopen($target, 'w'); |
|
| 210 | + [, $result] = Files::streamCopy($sourceStream, $targetStream, true); |
|
| 211 | + if (!$result) { |
|
| 212 | + Server::get(LoggerInterface::class)->warning("Failed to write data while copying $source to $target"); |
|
| 213 | + } |
|
| 214 | + $this->removeCachedFile($target); |
|
| 215 | + return $result; |
|
| 216 | + } |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + public function getMimeType(string $path): string|false { |
|
| 220 | + if ($this->is_dir($path)) { |
|
| 221 | + return 'httpd/unix-directory'; |
|
| 222 | + } elseif ($this->file_exists($path)) { |
|
| 223 | + return \OC::$server->getMimeTypeDetector()->detectPath($path); |
|
| 224 | + } else { |
|
| 225 | + return false; |
|
| 226 | + } |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + public function hash(string $type, string $path, bool $raw = false): string|false { |
|
| 230 | + $fh = $this->fopen($path, 'rb'); |
|
| 231 | + if (!$fh) { |
|
| 232 | + return false; |
|
| 233 | + } |
|
| 234 | + $ctx = hash_init($type); |
|
| 235 | + hash_update_stream($ctx, $fh); |
|
| 236 | + fclose($fh); |
|
| 237 | + return hash_final($ctx, $raw); |
|
| 238 | + } |
|
| 239 | + |
|
| 240 | + public function getLocalFile(string $path): string|false { |
|
| 241 | + return $this->getCachedFile($path); |
|
| 242 | + } |
|
| 243 | + |
|
| 244 | + private function addLocalFolder(string $path, string $target): void { |
|
| 245 | + $dh = $this->opendir($path); |
|
| 246 | + if (is_resource($dh)) { |
|
| 247 | + while (($file = readdir($dh)) !== false) { |
|
| 248 | + if (!Filesystem::isIgnoredDir($file)) { |
|
| 249 | + if ($this->is_dir($path . '/' . $file)) { |
|
| 250 | + mkdir($target . '/' . $file); |
|
| 251 | + $this->addLocalFolder($path . '/' . $file, $target . '/' . $file); |
|
| 252 | + } else { |
|
| 253 | + $tmp = $this->toTmpFile($path . '/' . $file); |
|
| 254 | + rename($tmp, $target . '/' . $file); |
|
| 255 | + } |
|
| 256 | + } |
|
| 257 | + } |
|
| 258 | + } |
|
| 259 | + } |
|
| 260 | + |
|
| 261 | + protected function searchInDir(string $query, string $dir = ''): array { |
|
| 262 | + $files = []; |
|
| 263 | + $dh = $this->opendir($dir); |
|
| 264 | + if (is_resource($dh)) { |
|
| 265 | + while (($item = readdir($dh)) !== false) { |
|
| 266 | + if (Filesystem::isIgnoredDir($item)) { |
|
| 267 | + continue; |
|
| 268 | + } |
|
| 269 | + if (strstr(strtolower($item), strtolower($query)) !== false) { |
|
| 270 | + $files[] = $dir . '/' . $item; |
|
| 271 | + } |
|
| 272 | + if ($this->is_dir($dir . '/' . $item)) { |
|
| 273 | + $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item)); |
|
| 274 | + } |
|
| 275 | + } |
|
| 276 | + } |
|
| 277 | + closedir($dh); |
|
| 278 | + return $files; |
|
| 279 | + } |
|
| 280 | + |
|
| 281 | + /** |
|
| 282 | + * @inheritDoc |
|
| 283 | + * Check if a file or folder has been updated since $time |
|
| 284 | + * |
|
| 285 | + * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking |
|
| 286 | + * the mtime should always return false here. As a result storage implementations that always return false expect |
|
| 287 | + * exclusive access to the backend and will not pick up files that have been added in a way that circumvents |
|
| 288 | + * Nextcloud filesystem. |
|
| 289 | + */ |
|
| 290 | + public function hasUpdated(string $path, int $time): bool { |
|
| 291 | + return $this->filemtime($path) > $time; |
|
| 292 | + } |
|
| 293 | + |
|
| 294 | + protected function getCacheDependencies(): CacheDependencies { |
|
| 295 | + static $dependencies = null; |
|
| 296 | + if (!$dependencies) { |
|
| 297 | + $dependencies = Server::get(CacheDependencies::class); |
|
| 298 | + } |
|
| 299 | + return $dependencies; |
|
| 300 | + } |
|
| 301 | + |
|
| 302 | + public function getCache(string $path = '', ?IStorage $storage = null): ICache { |
|
| 303 | + if (!$storage) { |
|
| 304 | + $storage = $this; |
|
| 305 | + } |
|
| 306 | + /** @var self $storage */ |
|
| 307 | + if (!isset($storage->cache)) { |
|
| 308 | + $storage->cache = new Cache($storage, $this->getCacheDependencies()); |
|
| 309 | + } |
|
| 310 | + return $storage->cache; |
|
| 311 | + } |
|
| 312 | + |
|
| 313 | + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { |
|
| 314 | + if (!$storage) { |
|
| 315 | + $storage = $this; |
|
| 316 | + } |
|
| 317 | + if (!$storage->instanceOfStorage(self::class)) { |
|
| 318 | + throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 319 | + } |
|
| 320 | + if (!isset($storage->scanner)) { |
|
| 321 | + $storage->scanner = new Scanner($storage); |
|
| 322 | + } |
|
| 323 | + return $storage->scanner; |
|
| 324 | + } |
|
| 325 | + |
|
| 326 | + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { |
|
| 327 | + if (!$storage) { |
|
| 328 | + $storage = $this; |
|
| 329 | + } |
|
| 330 | + if (!isset($this->watcher)) { |
|
| 331 | + $this->watcher = new Watcher($storage); |
|
| 332 | + $globalPolicy = Server::get(IConfig::class)->getSystemValueInt('filesystem_check_changes', Watcher::CHECK_NEVER); |
|
| 333 | + $this->watcher->setPolicy((int)$this->getMountOption('filesystem_check_changes', $globalPolicy)); |
|
| 334 | + } |
|
| 335 | + return $this->watcher; |
|
| 336 | + } |
|
| 337 | + |
|
| 338 | + public function getPropagator(?IStorage $storage = null): IPropagator { |
|
| 339 | + if (!$storage) { |
|
| 340 | + $storage = $this; |
|
| 341 | + } |
|
| 342 | + if (!$storage->instanceOfStorage(self::class)) { |
|
| 343 | + throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 344 | + } |
|
| 345 | + /** @var self $storage */ |
|
| 346 | + if (!isset($storage->propagator)) { |
|
| 347 | + $config = Server::get(IConfig::class); |
|
| 348 | + $storage->propagator = new Propagator($storage, \OC::$server->getDatabaseConnection(), ['appdata_' . $config->getSystemValueString('instanceid')]); |
|
| 349 | + } |
|
| 350 | + return $storage->propagator; |
|
| 351 | + } |
|
| 352 | + |
|
| 353 | + public function getUpdater(?IStorage $storage = null): IUpdater { |
|
| 354 | + if (!$storage) { |
|
| 355 | + $storage = $this; |
|
| 356 | + } |
|
| 357 | + if (!$storage->instanceOfStorage(self::class)) { |
|
| 358 | + throw new \InvalidArgumentException('Storage is not of the correct class'); |
|
| 359 | + } |
|
| 360 | + /** @var self $storage */ |
|
| 361 | + if (!isset($storage->updater)) { |
|
| 362 | + $storage->updater = new Updater($storage); |
|
| 363 | + } |
|
| 364 | + return $storage->updater; |
|
| 365 | + } |
|
| 366 | + |
|
| 367 | + public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage { |
|
| 368 | + /** @var Cache $cache */ |
|
| 369 | + $cache = $this->getCache(storage: $storage); |
|
| 370 | + return $cache->getStorageCache(); |
|
| 371 | + } |
|
| 372 | + |
|
| 373 | + public function getOwner(string $path): string|false { |
|
| 374 | + if ($this->owner === null) { |
|
| 375 | + $this->owner = \OC_User::getUser(); |
|
| 376 | + } |
|
| 377 | + |
|
| 378 | + return $this->owner; |
|
| 379 | + } |
|
| 380 | + |
|
| 381 | + public function getETag(string $path): string|false { |
|
| 382 | + return uniqid(); |
|
| 383 | + } |
|
| 384 | + |
|
| 385 | + /** |
|
| 386 | + * clean a path, i.e. remove all redundant '.' and '..' |
|
| 387 | + * making sure that it can't point to higher than '/' |
|
| 388 | + * |
|
| 389 | + * @param string $path The path to clean |
|
| 390 | + * @return string cleaned path |
|
| 391 | + */ |
|
| 392 | + public function cleanPath(string $path): string { |
|
| 393 | + if (strlen($path) == 0 || $path[0] != '/') { |
|
| 394 | + $path = '/' . $path; |
|
| 395 | + } |
|
| 396 | + |
|
| 397 | + $output = []; |
|
| 398 | + foreach (explode('/', $path) as $chunk) { |
|
| 399 | + if ($chunk == '..') { |
|
| 400 | + array_pop($output); |
|
| 401 | + } elseif ($chunk == '.') { |
|
| 402 | + } else { |
|
| 403 | + $output[] = $chunk; |
|
| 404 | + } |
|
| 405 | + } |
|
| 406 | + return implode('/', $output); |
|
| 407 | + } |
|
| 408 | + |
|
| 409 | + /** |
|
| 410 | + * Test a storage for availability |
|
| 411 | + */ |
|
| 412 | + public function test(): bool { |
|
| 413 | + try { |
|
| 414 | + if ($this->stat('')) { |
|
| 415 | + return true; |
|
| 416 | + } |
|
| 417 | + Server::get(LoggerInterface::class)->info('External storage not available: stat() failed'); |
|
| 418 | + return false; |
|
| 419 | + } catch (\Exception $e) { |
|
| 420 | + Server::get(LoggerInterface::class)->warning( |
|
| 421 | + 'External storage not available: ' . $e->getMessage(), |
|
| 422 | + ['exception' => $e] |
|
| 423 | + ); |
|
| 424 | + return false; |
|
| 425 | + } |
|
| 426 | + } |
|
| 427 | + |
|
| 428 | + public function free_space(string $path): int|float|false { |
|
| 429 | + return \OCP\Files\FileInfo::SPACE_UNKNOWN; |
|
| 430 | + } |
|
| 431 | + |
|
| 432 | + public function isLocal(): bool { |
|
| 433 | + // the common implementation returns a temporary file by |
|
| 434 | + // default, which is not local |
|
| 435 | + return false; |
|
| 436 | + } |
|
| 437 | + |
|
| 438 | + /** |
|
| 439 | + * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class |
|
| 440 | + */ |
|
| 441 | + public function instanceOfStorage(string $class): bool { |
|
| 442 | + if (ltrim($class, '\\') === 'OC\Files\Storage\Shared') { |
|
| 443 | + // FIXME Temporary fix to keep existing checks working |
|
| 444 | + $class = '\OCA\Files_Sharing\SharedStorage'; |
|
| 445 | + } |
|
| 446 | + return is_a($this, $class); |
|
| 447 | + } |
|
| 448 | + |
|
| 449 | + #[Override] |
|
| 450 | + public function getDirectDownload(string $path): array|false { |
|
| 451 | + return false; |
|
| 452 | + } |
|
| 453 | + |
|
| 454 | + #[Override] |
|
| 455 | + public function getDirectDownloadById(string $fileId): array|false { |
|
| 456 | + return false; |
|
| 457 | + } |
|
| 458 | + |
|
| 459 | + public function verifyPath(string $path, string $fileName): void { |
|
| 460 | + $this->getFilenameValidator() |
|
| 461 | + ->validateFilename($fileName); |
|
| 462 | + |
|
| 463 | + // verify also the path is valid |
|
| 464 | + if ($path && $path !== '/' && $path !== '.') { |
|
| 465 | + try { |
|
| 466 | + $this->verifyPath(dirname($path), basename($path)); |
|
| 467 | + } catch (InvalidPathException $e) { |
|
| 468 | + // Ignore invalid file type exceptions on directories |
|
| 469 | + if ($e->getCode() !== FilenameValidator::INVALID_FILE_TYPE) { |
|
| 470 | + $l = \OCP\Util::getL10N('lib'); |
|
| 471 | + throw new InvalidPathException($l->t('Invalid parent path'), previous: $e); |
|
| 472 | + } |
|
| 473 | + } |
|
| 474 | + } |
|
| 475 | + } |
|
| 476 | + |
|
| 477 | + /** |
|
| 478 | + * Get the filename validator |
|
| 479 | + * (cached for performance) |
|
| 480 | + */ |
|
| 481 | + protected function getFilenameValidator(): IFilenameValidator { |
|
| 482 | + if ($this->filenameValidator === null) { |
|
| 483 | + $this->filenameValidator = Server::get(IFilenameValidator::class); |
|
| 484 | + } |
|
| 485 | + return $this->filenameValidator; |
|
| 486 | + } |
|
| 487 | + |
|
| 488 | + public function setMountOptions(array $options): void { |
|
| 489 | + $this->mountOptions = $options; |
|
| 490 | + } |
|
| 491 | + |
|
| 492 | + public function getMountOption(string $name, mixed $default = null): mixed { |
|
| 493 | + return $this->mountOptions[$name] ?? $default; |
|
| 494 | + } |
|
| 495 | + |
|
| 496 | + public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool { |
|
| 497 | + if ($sourceStorage === $this) { |
|
| 498 | + return $this->copy($sourceInternalPath, $targetInternalPath); |
|
| 499 | + } |
|
| 500 | + |
|
| 501 | + if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 502 | + $dh = $sourceStorage->opendir($sourceInternalPath); |
|
| 503 | + $result = $this->mkdir($targetInternalPath); |
|
| 504 | + if (is_resource($dh)) { |
|
| 505 | + $result = true; |
|
| 506 | + while ($result && ($file = readdir($dh)) !== false) { |
|
| 507 | + if (!Filesystem::isIgnoredDir($file)) { |
|
| 508 | + $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file); |
|
| 509 | + } |
|
| 510 | + } |
|
| 511 | + } |
|
| 512 | + } else { |
|
| 513 | + $source = $sourceStorage->fopen($sourceInternalPath, 'r'); |
|
| 514 | + $result = false; |
|
| 515 | + if ($source) { |
|
| 516 | + try { |
|
| 517 | + $this->writeStream($targetInternalPath, $source); |
|
| 518 | + $result = true; |
|
| 519 | + } catch (\Exception $e) { |
|
| 520 | + Server::get(LoggerInterface::class)->warning('Failed to copy stream to storage', ['exception' => $e]); |
|
| 521 | + } |
|
| 522 | + } |
|
| 523 | + |
|
| 524 | + if ($result && $preserveMtime) { |
|
| 525 | + $mtime = $sourceStorage->filemtime($sourceInternalPath); |
|
| 526 | + $this->touch($targetInternalPath, is_int($mtime) ? $mtime : null); |
|
| 527 | + } |
|
| 528 | + |
|
| 529 | + if (!$result) { |
|
| 530 | + // delete partially written target file |
|
| 531 | + $this->unlink($targetInternalPath); |
|
| 532 | + // delete cache entry that was created by fopen |
|
| 533 | + $this->getCache()->remove($targetInternalPath); |
|
| 534 | + } |
|
| 535 | + } |
|
| 536 | + return (bool)$result; |
|
| 537 | + } |
|
| 538 | + |
|
| 539 | + /** |
|
| 540 | + * Check if a storage is the same as the current one, including wrapped storages |
|
| 541 | + */ |
|
| 542 | + private function isSameStorage(IStorage $storage): bool { |
|
| 543 | + while ($storage->instanceOfStorage(Wrapper::class)) { |
|
| 544 | + /** |
|
| 545 | + * @var Wrapper $storage |
|
| 546 | + */ |
|
| 547 | + $storage = $storage->getWrapperStorage(); |
|
| 548 | + } |
|
| 549 | + |
|
| 550 | + return $storage === $this; |
|
| 551 | + } |
|
| 552 | + |
|
| 553 | + public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool { |
|
| 554 | + if ( |
|
| 555 | + !$sourceStorage->instanceOfStorage(Encryption::class) |
|
| 556 | + && $this->isSameStorage($sourceStorage) |
|
| 557 | + ) { |
|
| 558 | + // resolve any jailed paths |
|
| 559 | + while ($sourceStorage->instanceOfStorage(Jail::class)) { |
|
| 560 | + /** |
|
| 561 | + * @var Jail $sourceStorage |
|
| 562 | + */ |
|
| 563 | + $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath); |
|
| 564 | + $sourceStorage = $sourceStorage->getUnjailedStorage(); |
|
| 565 | + } |
|
| 566 | + |
|
| 567 | + return $this->rename($sourceInternalPath, $targetInternalPath); |
|
| 568 | + } |
|
| 569 | + |
|
| 570 | + if (!$sourceStorage->isDeletable($sourceInternalPath)) { |
|
| 571 | + return false; |
|
| 572 | + } |
|
| 573 | + |
|
| 574 | + $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, true); |
|
| 575 | + if ($result) { |
|
| 576 | + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { |
|
| 577 | + /** @var ObjectStoreStorage $sourceStorage */ |
|
| 578 | + $sourceStorage->setPreserveCacheOnDelete(true); |
|
| 579 | + } |
|
| 580 | + try { |
|
| 581 | + if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 582 | + $result = $sourceStorage->rmdir($sourceInternalPath); |
|
| 583 | + } else { |
|
| 584 | + $result = $sourceStorage->unlink($sourceInternalPath); |
|
| 585 | + } |
|
| 586 | + } finally { |
|
| 587 | + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { |
|
| 588 | + /** @var ObjectStoreStorage $sourceStorage */ |
|
| 589 | + $sourceStorage->setPreserveCacheOnDelete(false); |
|
| 590 | + } |
|
| 591 | + } |
|
| 592 | + } |
|
| 593 | + return $result; |
|
| 594 | + } |
|
| 595 | + |
|
| 596 | + public function getMetaData(string $path): ?array { |
|
| 597 | + if (Filesystem::isFileBlacklisted($path)) { |
|
| 598 | + throw new ForbiddenException('Invalid path: ' . $path, false); |
|
| 599 | + } |
|
| 600 | + |
|
| 601 | + $permissions = $this->getPermissions($path); |
|
| 602 | + if (!$permissions & \OCP\Constants::PERMISSION_READ) { |
|
| 603 | + //can't read, nothing we can do |
|
| 604 | + return null; |
|
| 605 | + } |
|
| 606 | + |
|
| 607 | + $data = []; |
|
| 608 | + $data['mimetype'] = $this->getMimeType($path); |
|
| 609 | + $data['mtime'] = $this->filemtime($path); |
|
| 610 | + if ($data['mtime'] === false) { |
|
| 611 | + $data['mtime'] = time(); |
|
| 612 | + } |
|
| 613 | + if ($data['mimetype'] == 'httpd/unix-directory') { |
|
| 614 | + $data['size'] = -1; //unknown |
|
| 615 | + } else { |
|
| 616 | + $data['size'] = $this->filesize($path); |
|
| 617 | + } |
|
| 618 | + $data['etag'] = $this->getETag($path); |
|
| 619 | + $data['storage_mtime'] = $data['mtime']; |
|
| 620 | + $data['permissions'] = $permissions; |
|
| 621 | + $data['name'] = basename($path); |
|
| 622 | + |
|
| 623 | + return $data; |
|
| 624 | + } |
|
| 625 | + |
|
| 626 | + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 627 | + $logger = $this->getLockLogger(); |
|
| 628 | + if ($logger) { |
|
| 629 | + $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 630 | + $logger->info( |
|
| 631 | + sprintf( |
|
| 632 | + 'acquire %s lock on "%s" on storage "%s"', |
|
| 633 | + $typeString, |
|
| 634 | + $path, |
|
| 635 | + $this->getId() |
|
| 636 | + ), |
|
| 637 | + [ |
|
| 638 | + 'app' => 'locking', |
|
| 639 | + ] |
|
| 640 | + ); |
|
| 641 | + } |
|
| 642 | + try { |
|
| 643 | + $provider->acquireLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type, $this->getId() . '::' . $path); |
|
| 644 | + } catch (LockedException $e) { |
|
| 645 | + $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 646 | + if ($logger) { |
|
| 647 | + $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 648 | + } |
|
| 649 | + throw $e; |
|
| 650 | + } |
|
| 651 | + } |
|
| 652 | + |
|
| 653 | + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 654 | + $logger = $this->getLockLogger(); |
|
| 655 | + if ($logger) { |
|
| 656 | + $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 657 | + $logger->info( |
|
| 658 | + sprintf( |
|
| 659 | + 'release %s lock on "%s" on storage "%s"', |
|
| 660 | + $typeString, |
|
| 661 | + $path, |
|
| 662 | + $this->getId() |
|
| 663 | + ), |
|
| 664 | + [ |
|
| 665 | + 'app' => 'locking', |
|
| 666 | + ] |
|
| 667 | + ); |
|
| 668 | + } |
|
| 669 | + try { |
|
| 670 | + $provider->releaseLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); |
|
| 671 | + } catch (LockedException $e) { |
|
| 672 | + $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 673 | + if ($logger) { |
|
| 674 | + $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 675 | + } |
|
| 676 | + throw $e; |
|
| 677 | + } |
|
| 678 | + } |
|
| 679 | + |
|
| 680 | + public function changeLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 681 | + $logger = $this->getLockLogger(); |
|
| 682 | + if ($logger) { |
|
| 683 | + $typeString = ($type === ILockingProvider::LOCK_SHARED) ? 'shared' : 'exclusive'; |
|
| 684 | + $logger->info( |
|
| 685 | + sprintf( |
|
| 686 | + 'change lock on "%s" to %s on storage "%s"', |
|
| 687 | + $path, |
|
| 688 | + $typeString, |
|
| 689 | + $this->getId() |
|
| 690 | + ), |
|
| 691 | + [ |
|
| 692 | + 'app' => 'locking', |
|
| 693 | + ] |
|
| 694 | + ); |
|
| 695 | + } |
|
| 696 | + try { |
|
| 697 | + $provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type); |
|
| 698 | + } catch (LockedException $e) { |
|
| 699 | + $e = new LockedException($e->getPath(), $e, $e->getExistingLock(), $path); |
|
| 700 | + if ($logger) { |
|
| 701 | + $logger->info($e->getMessage(), ['exception' => $e]); |
|
| 702 | + } |
|
| 703 | + throw $e; |
|
| 704 | + } |
|
| 705 | + } |
|
| 706 | + |
|
| 707 | + private function getLockLogger(): ?LoggerInterface { |
|
| 708 | + if (is_null($this->shouldLogLocks)) { |
|
| 709 | + $this->shouldLogLocks = Server::get(IConfig::class)->getSystemValueBool('filelocking.debug', false); |
|
| 710 | + $this->logger = $this->shouldLogLocks ? Server::get(LoggerInterface::class) : null; |
|
| 711 | + } |
|
| 712 | + return $this->logger; |
|
| 713 | + } |
|
| 714 | + |
|
| 715 | + /** |
|
| 716 | + * @return array{available: bool, last_checked: int} |
|
| 717 | + */ |
|
| 718 | + public function getAvailability(): array { |
|
| 719 | + return $this->getStorageCache()->getAvailability(); |
|
| 720 | + } |
|
| 721 | + |
|
| 722 | + public function setAvailability(bool $isAvailable): void { |
|
| 723 | + $this->getStorageCache()->setAvailability($isAvailable); |
|
| 724 | + } |
|
| 725 | + |
|
| 726 | + public function setOwner(?string $user): void { |
|
| 727 | + $this->owner = $user; |
|
| 728 | + } |
|
| 729 | + |
|
| 730 | + public function needsPartFile(): bool { |
|
| 731 | + return true; |
|
| 732 | + } |
|
| 733 | + |
|
| 734 | + public function writeStream(string $path, $stream, ?int $size = null): int { |
|
| 735 | + $target = $this->fopen($path, 'w'); |
|
| 736 | + if (!$target) { |
|
| 737 | + throw new GenericFileException("Failed to open $path for writing"); |
|
| 738 | + } |
|
| 739 | + try { |
|
| 740 | + [$count, $result] = Files::streamCopy($stream, $target, true); |
|
| 741 | + if (!$result) { |
|
| 742 | + throw new GenericFileException('Failed to copy stream'); |
|
| 743 | + } |
|
| 744 | + } finally { |
|
| 745 | + fclose($target); |
|
| 746 | + fclose($stream); |
|
| 747 | + } |
|
| 748 | + return $count; |
|
| 749 | + } |
|
| 750 | + |
|
| 751 | + public function getDirectoryContent(string $directory): \Traversable { |
|
| 752 | + $dh = $this->opendir($directory); |
|
| 753 | + |
|
| 754 | + if ($dh === false) { |
|
| 755 | + throw new StorageNotAvailableException('Directory listing failed'); |
|
| 756 | + } |
|
| 757 | + |
|
| 758 | + if (is_resource($dh)) { |
|
| 759 | + $basePath = rtrim($directory, '/'); |
|
| 760 | + while (($file = readdir($dh)) !== false) { |
|
| 761 | + if (!Filesystem::isIgnoredDir($file)) { |
|
| 762 | + $childPath = $basePath . '/' . trim($file, '/'); |
|
| 763 | + $metadata = $this->getMetaData($childPath); |
|
| 764 | + if ($metadata !== null) { |
|
| 765 | + yield $metadata; |
|
| 766 | + } |
|
| 767 | + } |
|
| 768 | + } |
|
| 769 | + } |
|
| 770 | + } |
|
| 771 | 771 | } |
@@ -47,527 +47,527 @@ |
||
| 47 | 47 | * Convert target path to source path and pass the function call to the correct storage provider |
| 48 | 48 | */ |
| 49 | 49 | class SharedStorage extends Jail implements LegacyISharedStorage, ISharedStorage, IDisableEncryptionStorage { |
| 50 | - /** @var IShare */ |
|
| 51 | - private $superShare; |
|
| 52 | - |
|
| 53 | - /** @var IShare[] */ |
|
| 54 | - private $groupedShares; |
|
| 55 | - |
|
| 56 | - /** |
|
| 57 | - * @var View |
|
| 58 | - */ |
|
| 59 | - private $ownerView; |
|
| 60 | - |
|
| 61 | - private $initialized = false; |
|
| 62 | - |
|
| 63 | - /** |
|
| 64 | - * @var ICacheEntry |
|
| 65 | - */ |
|
| 66 | - private $sourceRootInfo; |
|
| 67 | - |
|
| 68 | - /** @var string */ |
|
| 69 | - private $user; |
|
| 70 | - |
|
| 71 | - private LoggerInterface $logger; |
|
| 72 | - |
|
| 73 | - /** @var IStorage */ |
|
| 74 | - private $nonMaskedStorage; |
|
| 75 | - |
|
| 76 | - private array $mountOptions = []; |
|
| 77 | - |
|
| 78 | - /** @var boolean */ |
|
| 79 | - private $sharingDisabledForUser; |
|
| 80 | - |
|
| 81 | - /** @var ?Folder $ownerUserFolder */ |
|
| 82 | - private $ownerUserFolder = null; |
|
| 83 | - |
|
| 84 | - private string $sourcePath = ''; |
|
| 85 | - |
|
| 86 | - private static int $initDepth = 0; |
|
| 87 | - |
|
| 88 | - /** |
|
| 89 | - * @psalm-suppress NonInvariantDocblockPropertyType |
|
| 90 | - * @var ?Storage $storage |
|
| 91 | - */ |
|
| 92 | - protected $storage; |
|
| 93 | - |
|
| 94 | - public function __construct(array $parameters) { |
|
| 95 | - $this->ownerView = $parameters['ownerView']; |
|
| 96 | - $this->logger = Server::get(LoggerInterface::class); |
|
| 97 | - |
|
| 98 | - $this->superShare = $parameters['superShare']; |
|
| 99 | - $this->groupedShares = $parameters['groupedShares']; |
|
| 100 | - |
|
| 101 | - $this->user = $parameters['user']; |
|
| 102 | - if (isset($parameters['sharingDisabledForUser'])) { |
|
| 103 | - $this->sharingDisabledForUser = $parameters['sharingDisabledForUser']; |
|
| 104 | - } else { |
|
| 105 | - $this->sharingDisabledForUser = false; |
|
| 106 | - } |
|
| 107 | - |
|
| 108 | - parent::__construct([ |
|
| 109 | - 'storage' => null, |
|
| 110 | - 'root' => null, |
|
| 111 | - ]); |
|
| 112 | - } |
|
| 113 | - |
|
| 114 | - /** |
|
| 115 | - * @return ICacheEntry |
|
| 116 | - */ |
|
| 117 | - private function getSourceRootInfo() { |
|
| 118 | - if (is_null($this->sourceRootInfo)) { |
|
| 119 | - if (is_null($this->superShare->getNodeCacheEntry())) { |
|
| 120 | - $this->init(); |
|
| 121 | - $this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath); |
|
| 122 | - } else { |
|
| 123 | - $this->sourceRootInfo = $this->superShare->getNodeCacheEntry(); |
|
| 124 | - } |
|
| 125 | - } |
|
| 126 | - return $this->sourceRootInfo; |
|
| 127 | - } |
|
| 128 | - |
|
| 129 | - /** |
|
| 130 | - * @psalm-assert Storage $this->storage |
|
| 131 | - */ |
|
| 132 | - private function init() { |
|
| 133 | - if ($this->initialized) { |
|
| 134 | - if (!$this->storage) { |
|
| 135 | - // marked as initialized but no storage set |
|
| 136 | - // this is probably because some code path has caused recursion during the share setup |
|
| 137 | - // we setup a "failed storage" so `getWrapperStorage` doesn't return null. |
|
| 138 | - // If the share setup completes after this the "failed storage" will be overwritten by the correct one |
|
| 139 | - $ex = new \Exception('Possible share setup recursion detected for share ' . $this->superShare->getId()); |
|
| 140 | - $this->logger->warning($ex->getMessage(), ['exception' => $ex, 'app' => 'files_sharing']); |
|
| 141 | - $this->storage = new FailedStorage(['exception' => $ex]); |
|
| 142 | - $this->cache = new FailedCache(); |
|
| 143 | - $this->rootPath = ''; |
|
| 144 | - } |
|
| 145 | - return; |
|
| 146 | - } |
|
| 147 | - |
|
| 148 | - $this->initialized = true; |
|
| 149 | - self::$initDepth++; |
|
| 150 | - |
|
| 151 | - try { |
|
| 152 | - if (self::$initDepth > 10) { |
|
| 153 | - throw new \Exception('Maximum share depth reached'); |
|
| 154 | - } |
|
| 155 | - |
|
| 156 | - /** @var IRootFolder $rootFolder */ |
|
| 157 | - $rootFolder = Server::get(IRootFolder::class); |
|
| 158 | - $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner()); |
|
| 159 | - $sourceId = $this->superShare->getNodeId(); |
|
| 160 | - $ownerNodes = $this->ownerUserFolder->getById($sourceId); |
|
| 161 | - |
|
| 162 | - if (count($ownerNodes) === 0) { |
|
| 163 | - $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]); |
|
| 164 | - $this->cache = new FailedCache(); |
|
| 165 | - $this->rootPath = ''; |
|
| 166 | - } else { |
|
| 167 | - foreach ($ownerNodes as $ownerNode) { |
|
| 168 | - $nonMaskedStorage = $ownerNode->getStorage(); |
|
| 169 | - |
|
| 170 | - // check if potential source node would lead to a recursive share setup |
|
| 171 | - if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) { |
|
| 172 | - continue; |
|
| 173 | - } |
|
| 174 | - $this->nonMaskedStorage = $nonMaskedStorage; |
|
| 175 | - $this->sourcePath = $ownerNode->getPath(); |
|
| 176 | - $this->rootPath = $ownerNode->getInternalPath(); |
|
| 177 | - $this->cache = null; |
|
| 178 | - break; |
|
| 179 | - } |
|
| 180 | - if (!$this->nonMaskedStorage) { |
|
| 181 | - // all potential source nodes would have been recursive |
|
| 182 | - throw new \Exception('recursive share detected'); |
|
| 183 | - } |
|
| 184 | - $this->storage = new PermissionsMask([ |
|
| 185 | - 'storage' => $this->nonMaskedStorage, |
|
| 186 | - 'mask' => $this->superShare->getPermissions(), |
|
| 187 | - ]); |
|
| 188 | - } |
|
| 189 | - } catch (NotFoundException $e) { |
|
| 190 | - // original file not accessible or deleted, set FailedStorage |
|
| 191 | - $this->storage = new FailedStorage(['exception' => $e]); |
|
| 192 | - $this->cache = new FailedCache(); |
|
| 193 | - $this->rootPath = ''; |
|
| 194 | - } catch (NoUserException $e) { |
|
| 195 | - // sharer user deleted, set FailedStorage |
|
| 196 | - $this->storage = new FailedStorage(['exception' => $e]); |
|
| 197 | - $this->cache = new FailedCache(); |
|
| 198 | - $this->rootPath = ''; |
|
| 199 | - } catch (\Exception $e) { |
|
| 200 | - $this->storage = new FailedStorage(['exception' => $e]); |
|
| 201 | - $this->cache = new FailedCache(); |
|
| 202 | - $this->rootPath = ''; |
|
| 203 | - $this->logger->error($e->getMessage(), ['exception' => $e]); |
|
| 204 | - } |
|
| 205 | - |
|
| 206 | - if (!$this->nonMaskedStorage) { |
|
| 207 | - $this->nonMaskedStorage = $this->storage; |
|
| 208 | - } |
|
| 209 | - self::$initDepth--; |
|
| 210 | - } |
|
| 211 | - |
|
| 212 | - public function instanceOfStorage(string $class): bool { |
|
| 213 | - if ($class === '\OC\Files\Storage\Common' || $class == Common::class) { |
|
| 214 | - return true; |
|
| 215 | - } |
|
| 216 | - if (in_array($class, [ |
|
| 217 | - '\OC\Files\Storage\Home', |
|
| 218 | - '\OC\Files\ObjectStore\HomeObjectStoreStorage', |
|
| 219 | - '\OCP\Files\IHomeStorage', |
|
| 220 | - Home::class, |
|
| 221 | - HomeObjectStoreStorage::class, |
|
| 222 | - IHomeStorage::class |
|
| 223 | - ])) { |
|
| 224 | - return false; |
|
| 225 | - } |
|
| 226 | - return parent::instanceOfStorage($class); |
|
| 227 | - } |
|
| 228 | - |
|
| 229 | - /** |
|
| 230 | - * @return string |
|
| 231 | - */ |
|
| 232 | - public function getShareId() { |
|
| 233 | - return $this->superShare->getId(); |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - private function isValid(): bool { |
|
| 237 | - return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; |
|
| 238 | - } |
|
| 239 | - |
|
| 240 | - public function getId(): string { |
|
| 241 | - return 'shared::' . $this->getMountPoint(); |
|
| 242 | - } |
|
| 243 | - |
|
| 244 | - public function getPermissions(string $path = ''): int { |
|
| 245 | - if (!$this->isValid()) { |
|
| 246 | - return 0; |
|
| 247 | - } |
|
| 248 | - $permissions = parent::getPermissions($path) & $this->superShare->getPermissions(); |
|
| 249 | - |
|
| 250 | - // part files and the mount point always have delete permissions |
|
| 251 | - if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') { |
|
| 252 | - $permissions |= Constants::PERMISSION_DELETE; |
|
| 253 | - } |
|
| 254 | - |
|
| 255 | - if ($this->sharingDisabledForUser) { |
|
| 256 | - $permissions &= ~Constants::PERMISSION_SHARE; |
|
| 257 | - } |
|
| 258 | - |
|
| 259 | - return $permissions; |
|
| 260 | - } |
|
| 261 | - |
|
| 262 | - public function isCreatable(string $path): bool { |
|
| 263 | - return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); |
|
| 264 | - } |
|
| 265 | - |
|
| 266 | - public function isReadable(string $path): bool { |
|
| 267 | - if (!$this->isValid()) { |
|
| 268 | - return false; |
|
| 269 | - } |
|
| 270 | - if (!$this->file_exists($path)) { |
|
| 271 | - return false; |
|
| 272 | - } |
|
| 273 | - /** @var IStorage $storage */ |
|
| 274 | - /** @var string $internalPath */ |
|
| 275 | - [$storage, $internalPath] = $this->resolvePath($path); |
|
| 276 | - return $storage->isReadable($internalPath); |
|
| 277 | - } |
|
| 278 | - |
|
| 279 | - public function isUpdatable(string $path): bool { |
|
| 280 | - return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); |
|
| 281 | - } |
|
| 282 | - |
|
| 283 | - public function isDeletable(string $path): bool { |
|
| 284 | - return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); |
|
| 285 | - } |
|
| 286 | - |
|
| 287 | - public function isSharable(string $path): bool { |
|
| 288 | - if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { |
|
| 289 | - return false; |
|
| 290 | - } |
|
| 291 | - return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); |
|
| 292 | - } |
|
| 293 | - |
|
| 294 | - public function fopen(string $path, string $mode) { |
|
| 295 | - $source = $this->getUnjailedPath($path); |
|
| 296 | - switch ($mode) { |
|
| 297 | - case 'r+': |
|
| 298 | - case 'rb+': |
|
| 299 | - case 'w+': |
|
| 300 | - case 'wb+': |
|
| 301 | - case 'x+': |
|
| 302 | - case 'xb+': |
|
| 303 | - case 'a+': |
|
| 304 | - case 'ab+': |
|
| 305 | - case 'w': |
|
| 306 | - case 'wb': |
|
| 307 | - case 'x': |
|
| 308 | - case 'xb': |
|
| 309 | - case 'a': |
|
| 310 | - case 'ab': |
|
| 311 | - $creatable = $this->isCreatable(dirname($path)); |
|
| 312 | - $updatable = $this->isUpdatable($path); |
|
| 313 | - // if neither permissions given, no need to continue |
|
| 314 | - if (!$creatable && !$updatable) { |
|
| 315 | - if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { |
|
| 316 | - $updatable = $this->isUpdatable(dirname($path)); |
|
| 317 | - } |
|
| 318 | - |
|
| 319 | - if (!$updatable) { |
|
| 320 | - return false; |
|
| 321 | - } |
|
| 322 | - } |
|
| 323 | - |
|
| 324 | - $exists = $this->file_exists($path); |
|
| 325 | - // if a file exists, updatable permissions are required |
|
| 326 | - if ($exists && !$updatable) { |
|
| 327 | - return false; |
|
| 328 | - } |
|
| 329 | - |
|
| 330 | - // part file is allowed if !$creatable but the final file is $updatable |
|
| 331 | - if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') { |
|
| 332 | - if (!$exists && !$creatable) { |
|
| 333 | - return false; |
|
| 334 | - } |
|
| 335 | - } |
|
| 336 | - } |
|
| 337 | - $info = [ |
|
| 338 | - 'target' => $this->getMountPoint() . '/' . $path, |
|
| 339 | - 'source' => $source, |
|
| 340 | - 'mode' => $mode, |
|
| 341 | - ]; |
|
| 342 | - Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); |
|
| 343 | - return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode); |
|
| 344 | - } |
|
| 345 | - |
|
| 346 | - public function rename(string $source, string $target): bool { |
|
| 347 | - $this->init(); |
|
| 348 | - $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part'; |
|
| 349 | - $targetExists = $this->file_exists($target); |
|
| 350 | - $sameFolder = dirname($source) === dirname($target); |
|
| 351 | - |
|
| 352 | - if ($targetExists || ($sameFolder && !$isPartFile)) { |
|
| 353 | - if (!$this->isUpdatable('')) { |
|
| 354 | - return false; |
|
| 355 | - } |
|
| 356 | - } else { |
|
| 357 | - if (!$this->isCreatable('')) { |
|
| 358 | - return false; |
|
| 359 | - } |
|
| 360 | - } |
|
| 361 | - |
|
| 362 | - return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); |
|
| 363 | - } |
|
| 364 | - |
|
| 365 | - /** |
|
| 366 | - * return mount point of share, relative to data/user/files |
|
| 367 | - * |
|
| 368 | - * @return string |
|
| 369 | - */ |
|
| 370 | - public function getMountPoint(): string { |
|
| 371 | - return $this->superShare->getTarget(); |
|
| 372 | - } |
|
| 373 | - |
|
| 374 | - public function setMountPoint(string $path): void { |
|
| 375 | - $this->superShare->setTarget($path); |
|
| 376 | - |
|
| 377 | - foreach ($this->groupedShares as $share) { |
|
| 378 | - $share->setTarget($path); |
|
| 379 | - } |
|
| 380 | - } |
|
| 381 | - |
|
| 382 | - /** |
|
| 383 | - * get the user who shared the file |
|
| 384 | - * |
|
| 385 | - * @return string |
|
| 386 | - */ |
|
| 387 | - public function getSharedFrom(): string { |
|
| 388 | - return $this->superShare->getShareOwner(); |
|
| 389 | - } |
|
| 390 | - |
|
| 391 | - public function getShare(): IShare { |
|
| 392 | - return $this->superShare; |
|
| 393 | - } |
|
| 394 | - |
|
| 395 | - /** |
|
| 396 | - * return share type, can be "file" or "folder" |
|
| 397 | - * |
|
| 398 | - * @return string |
|
| 399 | - */ |
|
| 400 | - public function getItemType(): string { |
|
| 401 | - return $this->superShare->getNodeType(); |
|
| 402 | - } |
|
| 403 | - |
|
| 404 | - public function getCache(string $path = '', ?IStorage $storage = null): ICache { |
|
| 405 | - if ($this->cache) { |
|
| 406 | - return $this->cache; |
|
| 407 | - } |
|
| 408 | - if (!$storage) { |
|
| 409 | - $storage = $this; |
|
| 410 | - } |
|
| 411 | - $sourceRoot = $this->getSourceRootInfo(); |
|
| 412 | - if ($this->storage instanceof FailedStorage) { |
|
| 413 | - return new FailedCache(); |
|
| 414 | - } |
|
| 415 | - |
|
| 416 | - $this->cache = new Cache( |
|
| 417 | - $storage, |
|
| 418 | - $sourceRoot, |
|
| 419 | - Server::get(CacheDependencies::class), |
|
| 420 | - $this->getShare() |
|
| 421 | - ); |
|
| 422 | - return $this->cache; |
|
| 423 | - } |
|
| 424 | - |
|
| 425 | - public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { |
|
| 426 | - if (!$storage) { |
|
| 427 | - $storage = $this; |
|
| 428 | - } |
|
| 429 | - return new Scanner($storage); |
|
| 430 | - } |
|
| 431 | - |
|
| 432 | - public function getOwner(string $path): string|false { |
|
| 433 | - return $this->superShare->getShareOwner(); |
|
| 434 | - } |
|
| 435 | - |
|
| 436 | - public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { |
|
| 437 | - if ($this->watcher) { |
|
| 438 | - return $this->watcher; |
|
| 439 | - } |
|
| 440 | - |
|
| 441 | - // Get node information |
|
| 442 | - $node = $this->getShare()->getNodeCacheEntry(); |
|
| 443 | - if ($node instanceof CacheEntry) { |
|
| 444 | - $storageId = $node->getData()['storage_string_id'] ?? null; |
|
| 445 | - // for shares from the home storage we can rely on the home storage to keep itself up to date |
|
| 446 | - // for other storages we need use the proper watcher |
|
| 447 | - if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) { |
|
| 448 | - $cache = $this->getCache(); |
|
| 449 | - $this->watcher = parent::getWatcher($path, $storage); |
|
| 450 | - if ($cache instanceof Cache) { |
|
| 451 | - $this->watcher->onUpdate($cache->markRootChanged(...)); |
|
| 452 | - } |
|
| 453 | - return $this->watcher; |
|
| 454 | - } |
|
| 455 | - } |
|
| 456 | - |
|
| 457 | - // cache updating is handled by the share source |
|
| 458 | - $this->watcher = new NullWatcher(); |
|
| 459 | - return $this->watcher; |
|
| 460 | - } |
|
| 461 | - |
|
| 462 | - /** |
|
| 463 | - * unshare complete storage, also the grouped shares |
|
| 464 | - * |
|
| 465 | - * @return bool |
|
| 466 | - */ |
|
| 467 | - public function unshareStorage(): bool { |
|
| 468 | - foreach ($this->groupedShares as $share) { |
|
| 469 | - Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user); |
|
| 470 | - } |
|
| 471 | - return true; |
|
| 472 | - } |
|
| 473 | - |
|
| 474 | - public function acquireLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 475 | - /** @var ILockingStorage $targetStorage */ |
|
| 476 | - [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 477 | - $targetStorage->acquireLock($targetInternalPath, $type, $provider); |
|
| 478 | - // lock the parent folders of the owner when locking the share as recipient |
|
| 479 | - if ($path === '') { |
|
| 480 | - $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); |
|
| 481 | - $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); |
|
| 482 | - } |
|
| 483 | - } |
|
| 484 | - |
|
| 485 | - public function releaseLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 486 | - /** @var ILockingStorage $targetStorage */ |
|
| 487 | - [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 488 | - $targetStorage->releaseLock($targetInternalPath, $type, $provider); |
|
| 489 | - // unlock the parent folders of the owner when unlocking the share as recipient |
|
| 490 | - if ($path === '') { |
|
| 491 | - $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); |
|
| 492 | - $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); |
|
| 493 | - } |
|
| 494 | - } |
|
| 495 | - |
|
| 496 | - public function changeLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 497 | - /** @var ILockingStorage $targetStorage */ |
|
| 498 | - [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 499 | - $targetStorage->changeLock($targetInternalPath, $type, $provider); |
|
| 500 | - } |
|
| 501 | - |
|
| 502 | - public function getAvailability(): array { |
|
| 503 | - // shares do not participate in availability logic |
|
| 504 | - return [ |
|
| 505 | - 'available' => true, |
|
| 506 | - 'last_checked' => 0, |
|
| 507 | - ]; |
|
| 508 | - } |
|
| 509 | - |
|
| 510 | - public function setAvailability(bool $isAvailable): void { |
|
| 511 | - // shares do not participate in availability logic |
|
| 512 | - } |
|
| 513 | - |
|
| 514 | - public function getSourceStorage() { |
|
| 515 | - $this->init(); |
|
| 516 | - return $this->nonMaskedStorage; |
|
| 517 | - } |
|
| 518 | - |
|
| 519 | - public function getWrapperStorage(): Storage { |
|
| 520 | - $this->init(); |
|
| 521 | - |
|
| 522 | - /** |
|
| 523 | - * @psalm-suppress DocblockTypeContradiction |
|
| 524 | - */ |
|
| 525 | - if (!$this->storage) { |
|
| 526 | - $message = 'no storage set after init for share ' . $this->getShareId(); |
|
| 527 | - $this->logger->error($message); |
|
| 528 | - $this->storage = new FailedStorage(['exception' => new \Exception($message)]); |
|
| 529 | - } |
|
| 530 | - |
|
| 531 | - return $this->storage; |
|
| 532 | - } |
|
| 533 | - |
|
| 534 | - public function file_get_contents(string $path): string|false { |
|
| 535 | - $info = [ |
|
| 536 | - 'target' => $this->getMountPoint() . '/' . $path, |
|
| 537 | - 'source' => $this->getUnjailedPath($path), |
|
| 538 | - ]; |
|
| 539 | - Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); |
|
| 540 | - return parent::file_get_contents($path); |
|
| 541 | - } |
|
| 542 | - |
|
| 543 | - public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 544 | - $info = [ |
|
| 545 | - 'target' => $this->getMountPoint() . '/' . $path, |
|
| 546 | - 'source' => $this->getUnjailedPath($path), |
|
| 547 | - ]; |
|
| 548 | - Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); |
|
| 549 | - return parent::file_put_contents($path, $data); |
|
| 550 | - } |
|
| 551 | - |
|
| 552 | - public function setMountOptions(array $options): void { |
|
| 553 | - /* Note: This value is never read */ |
|
| 554 | - $this->mountOptions = $options; |
|
| 555 | - } |
|
| 556 | - |
|
| 557 | - public function getUnjailedPath(string $path): string { |
|
| 558 | - $this->init(); |
|
| 559 | - return parent::getUnjailedPath($path); |
|
| 560 | - } |
|
| 561 | - |
|
| 562 | - #[Override] |
|
| 563 | - public function getDirectDownload(string $path): array|false { |
|
| 564 | - // disable direct download for shares |
|
| 565 | - return false; |
|
| 566 | - } |
|
| 567 | - |
|
| 568 | - #[Override] |
|
| 569 | - public function getDirectDownloadById(string $fileId): array|false { |
|
| 570 | - // disable direct download for shares |
|
| 571 | - return false; |
|
| 572 | - } |
|
| 50 | + /** @var IShare */ |
|
| 51 | + private $superShare; |
|
| 52 | + |
|
| 53 | + /** @var IShare[] */ |
|
| 54 | + private $groupedShares; |
|
| 55 | + |
|
| 56 | + /** |
|
| 57 | + * @var View |
|
| 58 | + */ |
|
| 59 | + private $ownerView; |
|
| 60 | + |
|
| 61 | + private $initialized = false; |
|
| 62 | + |
|
| 63 | + /** |
|
| 64 | + * @var ICacheEntry |
|
| 65 | + */ |
|
| 66 | + private $sourceRootInfo; |
|
| 67 | + |
|
| 68 | + /** @var string */ |
|
| 69 | + private $user; |
|
| 70 | + |
|
| 71 | + private LoggerInterface $logger; |
|
| 72 | + |
|
| 73 | + /** @var IStorage */ |
|
| 74 | + private $nonMaskedStorage; |
|
| 75 | + |
|
| 76 | + private array $mountOptions = []; |
|
| 77 | + |
|
| 78 | + /** @var boolean */ |
|
| 79 | + private $sharingDisabledForUser; |
|
| 80 | + |
|
| 81 | + /** @var ?Folder $ownerUserFolder */ |
|
| 82 | + private $ownerUserFolder = null; |
|
| 83 | + |
|
| 84 | + private string $sourcePath = ''; |
|
| 85 | + |
|
| 86 | + private static int $initDepth = 0; |
|
| 87 | + |
|
| 88 | + /** |
|
| 89 | + * @psalm-suppress NonInvariantDocblockPropertyType |
|
| 90 | + * @var ?Storage $storage |
|
| 91 | + */ |
|
| 92 | + protected $storage; |
|
| 93 | + |
|
| 94 | + public function __construct(array $parameters) { |
|
| 95 | + $this->ownerView = $parameters['ownerView']; |
|
| 96 | + $this->logger = Server::get(LoggerInterface::class); |
|
| 97 | + |
|
| 98 | + $this->superShare = $parameters['superShare']; |
|
| 99 | + $this->groupedShares = $parameters['groupedShares']; |
|
| 100 | + |
|
| 101 | + $this->user = $parameters['user']; |
|
| 102 | + if (isset($parameters['sharingDisabledForUser'])) { |
|
| 103 | + $this->sharingDisabledForUser = $parameters['sharingDisabledForUser']; |
|
| 104 | + } else { |
|
| 105 | + $this->sharingDisabledForUser = false; |
|
| 106 | + } |
|
| 107 | + |
|
| 108 | + parent::__construct([ |
|
| 109 | + 'storage' => null, |
|
| 110 | + 'root' => null, |
|
| 111 | + ]); |
|
| 112 | + } |
|
| 113 | + |
|
| 114 | + /** |
|
| 115 | + * @return ICacheEntry |
|
| 116 | + */ |
|
| 117 | + private function getSourceRootInfo() { |
|
| 118 | + if (is_null($this->sourceRootInfo)) { |
|
| 119 | + if (is_null($this->superShare->getNodeCacheEntry())) { |
|
| 120 | + $this->init(); |
|
| 121 | + $this->sourceRootInfo = $this->nonMaskedStorage->getCache()->get($this->rootPath); |
|
| 122 | + } else { |
|
| 123 | + $this->sourceRootInfo = $this->superShare->getNodeCacheEntry(); |
|
| 124 | + } |
|
| 125 | + } |
|
| 126 | + return $this->sourceRootInfo; |
|
| 127 | + } |
|
| 128 | + |
|
| 129 | + /** |
|
| 130 | + * @psalm-assert Storage $this->storage |
|
| 131 | + */ |
|
| 132 | + private function init() { |
|
| 133 | + if ($this->initialized) { |
|
| 134 | + if (!$this->storage) { |
|
| 135 | + // marked as initialized but no storage set |
|
| 136 | + // this is probably because some code path has caused recursion during the share setup |
|
| 137 | + // we setup a "failed storage" so `getWrapperStorage` doesn't return null. |
|
| 138 | + // If the share setup completes after this the "failed storage" will be overwritten by the correct one |
|
| 139 | + $ex = new \Exception('Possible share setup recursion detected for share ' . $this->superShare->getId()); |
|
| 140 | + $this->logger->warning($ex->getMessage(), ['exception' => $ex, 'app' => 'files_sharing']); |
|
| 141 | + $this->storage = new FailedStorage(['exception' => $ex]); |
|
| 142 | + $this->cache = new FailedCache(); |
|
| 143 | + $this->rootPath = ''; |
|
| 144 | + } |
|
| 145 | + return; |
|
| 146 | + } |
|
| 147 | + |
|
| 148 | + $this->initialized = true; |
|
| 149 | + self::$initDepth++; |
|
| 150 | + |
|
| 151 | + try { |
|
| 152 | + if (self::$initDepth > 10) { |
|
| 153 | + throw new \Exception('Maximum share depth reached'); |
|
| 154 | + } |
|
| 155 | + |
|
| 156 | + /** @var IRootFolder $rootFolder */ |
|
| 157 | + $rootFolder = Server::get(IRootFolder::class); |
|
| 158 | + $this->ownerUserFolder = $rootFolder->getUserFolder($this->superShare->getShareOwner()); |
|
| 159 | + $sourceId = $this->superShare->getNodeId(); |
|
| 160 | + $ownerNodes = $this->ownerUserFolder->getById($sourceId); |
|
| 161 | + |
|
| 162 | + if (count($ownerNodes) === 0) { |
|
| 163 | + $this->storage = new FailedStorage(['exception' => new NotFoundException("File by id $sourceId not found")]); |
|
| 164 | + $this->cache = new FailedCache(); |
|
| 165 | + $this->rootPath = ''; |
|
| 166 | + } else { |
|
| 167 | + foreach ($ownerNodes as $ownerNode) { |
|
| 168 | + $nonMaskedStorage = $ownerNode->getStorage(); |
|
| 169 | + |
|
| 170 | + // check if potential source node would lead to a recursive share setup |
|
| 171 | + if ($nonMaskedStorage instanceof Wrapper && $nonMaskedStorage->isWrapperOf($this)) { |
|
| 172 | + continue; |
|
| 173 | + } |
|
| 174 | + $this->nonMaskedStorage = $nonMaskedStorage; |
|
| 175 | + $this->sourcePath = $ownerNode->getPath(); |
|
| 176 | + $this->rootPath = $ownerNode->getInternalPath(); |
|
| 177 | + $this->cache = null; |
|
| 178 | + break; |
|
| 179 | + } |
|
| 180 | + if (!$this->nonMaskedStorage) { |
|
| 181 | + // all potential source nodes would have been recursive |
|
| 182 | + throw new \Exception('recursive share detected'); |
|
| 183 | + } |
|
| 184 | + $this->storage = new PermissionsMask([ |
|
| 185 | + 'storage' => $this->nonMaskedStorage, |
|
| 186 | + 'mask' => $this->superShare->getPermissions(), |
|
| 187 | + ]); |
|
| 188 | + } |
|
| 189 | + } catch (NotFoundException $e) { |
|
| 190 | + // original file not accessible or deleted, set FailedStorage |
|
| 191 | + $this->storage = new FailedStorage(['exception' => $e]); |
|
| 192 | + $this->cache = new FailedCache(); |
|
| 193 | + $this->rootPath = ''; |
|
| 194 | + } catch (NoUserException $e) { |
|
| 195 | + // sharer user deleted, set FailedStorage |
|
| 196 | + $this->storage = new FailedStorage(['exception' => $e]); |
|
| 197 | + $this->cache = new FailedCache(); |
|
| 198 | + $this->rootPath = ''; |
|
| 199 | + } catch (\Exception $e) { |
|
| 200 | + $this->storage = new FailedStorage(['exception' => $e]); |
|
| 201 | + $this->cache = new FailedCache(); |
|
| 202 | + $this->rootPath = ''; |
|
| 203 | + $this->logger->error($e->getMessage(), ['exception' => $e]); |
|
| 204 | + } |
|
| 205 | + |
|
| 206 | + if (!$this->nonMaskedStorage) { |
|
| 207 | + $this->nonMaskedStorage = $this->storage; |
|
| 208 | + } |
|
| 209 | + self::$initDepth--; |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + public function instanceOfStorage(string $class): bool { |
|
| 213 | + if ($class === '\OC\Files\Storage\Common' || $class == Common::class) { |
|
| 214 | + return true; |
|
| 215 | + } |
|
| 216 | + if (in_array($class, [ |
|
| 217 | + '\OC\Files\Storage\Home', |
|
| 218 | + '\OC\Files\ObjectStore\HomeObjectStoreStorage', |
|
| 219 | + '\OCP\Files\IHomeStorage', |
|
| 220 | + Home::class, |
|
| 221 | + HomeObjectStoreStorage::class, |
|
| 222 | + IHomeStorage::class |
|
| 223 | + ])) { |
|
| 224 | + return false; |
|
| 225 | + } |
|
| 226 | + return parent::instanceOfStorage($class); |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + /** |
|
| 230 | + * @return string |
|
| 231 | + */ |
|
| 232 | + public function getShareId() { |
|
| 233 | + return $this->superShare->getId(); |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + private function isValid(): bool { |
|
| 237 | + return $this->getSourceRootInfo() && ($this->getSourceRootInfo()->getPermissions() & Constants::PERMISSION_SHARE) === Constants::PERMISSION_SHARE; |
|
| 238 | + } |
|
| 239 | + |
|
| 240 | + public function getId(): string { |
|
| 241 | + return 'shared::' . $this->getMountPoint(); |
|
| 242 | + } |
|
| 243 | + |
|
| 244 | + public function getPermissions(string $path = ''): int { |
|
| 245 | + if (!$this->isValid()) { |
|
| 246 | + return 0; |
|
| 247 | + } |
|
| 248 | + $permissions = parent::getPermissions($path) & $this->superShare->getPermissions(); |
|
| 249 | + |
|
| 250 | + // part files and the mount point always have delete permissions |
|
| 251 | + if ($path === '' || pathinfo($path, PATHINFO_EXTENSION) === 'part') { |
|
| 252 | + $permissions |= Constants::PERMISSION_DELETE; |
|
| 253 | + } |
|
| 254 | + |
|
| 255 | + if ($this->sharingDisabledForUser) { |
|
| 256 | + $permissions &= ~Constants::PERMISSION_SHARE; |
|
| 257 | + } |
|
| 258 | + |
|
| 259 | + return $permissions; |
|
| 260 | + } |
|
| 261 | + |
|
| 262 | + public function isCreatable(string $path): bool { |
|
| 263 | + return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE); |
|
| 264 | + } |
|
| 265 | + |
|
| 266 | + public function isReadable(string $path): bool { |
|
| 267 | + if (!$this->isValid()) { |
|
| 268 | + return false; |
|
| 269 | + } |
|
| 270 | + if (!$this->file_exists($path)) { |
|
| 271 | + return false; |
|
| 272 | + } |
|
| 273 | + /** @var IStorage $storage */ |
|
| 274 | + /** @var string $internalPath */ |
|
| 275 | + [$storage, $internalPath] = $this->resolvePath($path); |
|
| 276 | + return $storage->isReadable($internalPath); |
|
| 277 | + } |
|
| 278 | + |
|
| 279 | + public function isUpdatable(string $path): bool { |
|
| 280 | + return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE); |
|
| 281 | + } |
|
| 282 | + |
|
| 283 | + public function isDeletable(string $path): bool { |
|
| 284 | + return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE); |
|
| 285 | + } |
|
| 286 | + |
|
| 287 | + public function isSharable(string $path): bool { |
|
| 288 | + if (Util::isSharingDisabledForUser() || !Share::isResharingAllowed()) { |
|
| 289 | + return false; |
|
| 290 | + } |
|
| 291 | + return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE); |
|
| 292 | + } |
|
| 293 | + |
|
| 294 | + public function fopen(string $path, string $mode) { |
|
| 295 | + $source = $this->getUnjailedPath($path); |
|
| 296 | + switch ($mode) { |
|
| 297 | + case 'r+': |
|
| 298 | + case 'rb+': |
|
| 299 | + case 'w+': |
|
| 300 | + case 'wb+': |
|
| 301 | + case 'x+': |
|
| 302 | + case 'xb+': |
|
| 303 | + case 'a+': |
|
| 304 | + case 'ab+': |
|
| 305 | + case 'w': |
|
| 306 | + case 'wb': |
|
| 307 | + case 'x': |
|
| 308 | + case 'xb': |
|
| 309 | + case 'a': |
|
| 310 | + case 'ab': |
|
| 311 | + $creatable = $this->isCreatable(dirname($path)); |
|
| 312 | + $updatable = $this->isUpdatable($path); |
|
| 313 | + // if neither permissions given, no need to continue |
|
| 314 | + if (!$creatable && !$updatable) { |
|
| 315 | + if (pathinfo($path, PATHINFO_EXTENSION) === 'part') { |
|
| 316 | + $updatable = $this->isUpdatable(dirname($path)); |
|
| 317 | + } |
|
| 318 | + |
|
| 319 | + if (!$updatable) { |
|
| 320 | + return false; |
|
| 321 | + } |
|
| 322 | + } |
|
| 323 | + |
|
| 324 | + $exists = $this->file_exists($path); |
|
| 325 | + // if a file exists, updatable permissions are required |
|
| 326 | + if ($exists && !$updatable) { |
|
| 327 | + return false; |
|
| 328 | + } |
|
| 329 | + |
|
| 330 | + // part file is allowed if !$creatable but the final file is $updatable |
|
| 331 | + if (pathinfo($path, PATHINFO_EXTENSION) !== 'part') { |
|
| 332 | + if (!$exists && !$creatable) { |
|
| 333 | + return false; |
|
| 334 | + } |
|
| 335 | + } |
|
| 336 | + } |
|
| 337 | + $info = [ |
|
| 338 | + 'target' => $this->getMountPoint() . '/' . $path, |
|
| 339 | + 'source' => $source, |
|
| 340 | + 'mode' => $mode, |
|
| 341 | + ]; |
|
| 342 | + Util::emitHook('\OC\Files\Storage\Shared', 'fopen', $info); |
|
| 343 | + return $this->nonMaskedStorage->fopen($this->getUnjailedPath($path), $mode); |
|
| 344 | + } |
|
| 345 | + |
|
| 346 | + public function rename(string $source, string $target): bool { |
|
| 347 | + $this->init(); |
|
| 348 | + $isPartFile = pathinfo($source, PATHINFO_EXTENSION) === 'part'; |
|
| 349 | + $targetExists = $this->file_exists($target); |
|
| 350 | + $sameFolder = dirname($source) === dirname($target); |
|
| 351 | + |
|
| 352 | + if ($targetExists || ($sameFolder && !$isPartFile)) { |
|
| 353 | + if (!$this->isUpdatable('')) { |
|
| 354 | + return false; |
|
| 355 | + } |
|
| 356 | + } else { |
|
| 357 | + if (!$this->isCreatable('')) { |
|
| 358 | + return false; |
|
| 359 | + } |
|
| 360 | + } |
|
| 361 | + |
|
| 362 | + return $this->nonMaskedStorage->rename($this->getUnjailedPath($source), $this->getUnjailedPath($target)); |
|
| 363 | + } |
|
| 364 | + |
|
| 365 | + /** |
|
| 366 | + * return mount point of share, relative to data/user/files |
|
| 367 | + * |
|
| 368 | + * @return string |
|
| 369 | + */ |
|
| 370 | + public function getMountPoint(): string { |
|
| 371 | + return $this->superShare->getTarget(); |
|
| 372 | + } |
|
| 373 | + |
|
| 374 | + public function setMountPoint(string $path): void { |
|
| 375 | + $this->superShare->setTarget($path); |
|
| 376 | + |
|
| 377 | + foreach ($this->groupedShares as $share) { |
|
| 378 | + $share->setTarget($path); |
|
| 379 | + } |
|
| 380 | + } |
|
| 381 | + |
|
| 382 | + /** |
|
| 383 | + * get the user who shared the file |
|
| 384 | + * |
|
| 385 | + * @return string |
|
| 386 | + */ |
|
| 387 | + public function getSharedFrom(): string { |
|
| 388 | + return $this->superShare->getShareOwner(); |
|
| 389 | + } |
|
| 390 | + |
|
| 391 | + public function getShare(): IShare { |
|
| 392 | + return $this->superShare; |
|
| 393 | + } |
|
| 394 | + |
|
| 395 | + /** |
|
| 396 | + * return share type, can be "file" or "folder" |
|
| 397 | + * |
|
| 398 | + * @return string |
|
| 399 | + */ |
|
| 400 | + public function getItemType(): string { |
|
| 401 | + return $this->superShare->getNodeType(); |
|
| 402 | + } |
|
| 403 | + |
|
| 404 | + public function getCache(string $path = '', ?IStorage $storage = null): ICache { |
|
| 405 | + if ($this->cache) { |
|
| 406 | + return $this->cache; |
|
| 407 | + } |
|
| 408 | + if (!$storage) { |
|
| 409 | + $storage = $this; |
|
| 410 | + } |
|
| 411 | + $sourceRoot = $this->getSourceRootInfo(); |
|
| 412 | + if ($this->storage instanceof FailedStorage) { |
|
| 413 | + return new FailedCache(); |
|
| 414 | + } |
|
| 415 | + |
|
| 416 | + $this->cache = new Cache( |
|
| 417 | + $storage, |
|
| 418 | + $sourceRoot, |
|
| 419 | + Server::get(CacheDependencies::class), |
|
| 420 | + $this->getShare() |
|
| 421 | + ); |
|
| 422 | + return $this->cache; |
|
| 423 | + } |
|
| 424 | + |
|
| 425 | + public function getScanner(string $path = '', ?IStorage $storage = null): IScanner { |
|
| 426 | + if (!$storage) { |
|
| 427 | + $storage = $this; |
|
| 428 | + } |
|
| 429 | + return new Scanner($storage); |
|
| 430 | + } |
|
| 431 | + |
|
| 432 | + public function getOwner(string $path): string|false { |
|
| 433 | + return $this->superShare->getShareOwner(); |
|
| 434 | + } |
|
| 435 | + |
|
| 436 | + public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher { |
|
| 437 | + if ($this->watcher) { |
|
| 438 | + return $this->watcher; |
|
| 439 | + } |
|
| 440 | + |
|
| 441 | + // Get node information |
|
| 442 | + $node = $this->getShare()->getNodeCacheEntry(); |
|
| 443 | + if ($node instanceof CacheEntry) { |
|
| 444 | + $storageId = $node->getData()['storage_string_id'] ?? null; |
|
| 445 | + // for shares from the home storage we can rely on the home storage to keep itself up to date |
|
| 446 | + // for other storages we need use the proper watcher |
|
| 447 | + if ($storageId !== null && !(str_starts_with($storageId, 'home::') || str_starts_with($storageId, 'object::user'))) { |
|
| 448 | + $cache = $this->getCache(); |
|
| 449 | + $this->watcher = parent::getWatcher($path, $storage); |
|
| 450 | + if ($cache instanceof Cache) { |
|
| 451 | + $this->watcher->onUpdate($cache->markRootChanged(...)); |
|
| 452 | + } |
|
| 453 | + return $this->watcher; |
|
| 454 | + } |
|
| 455 | + } |
|
| 456 | + |
|
| 457 | + // cache updating is handled by the share source |
|
| 458 | + $this->watcher = new NullWatcher(); |
|
| 459 | + return $this->watcher; |
|
| 460 | + } |
|
| 461 | + |
|
| 462 | + /** |
|
| 463 | + * unshare complete storage, also the grouped shares |
|
| 464 | + * |
|
| 465 | + * @return bool |
|
| 466 | + */ |
|
| 467 | + public function unshareStorage(): bool { |
|
| 468 | + foreach ($this->groupedShares as $share) { |
|
| 469 | + Server::get(\OCP\Share\IManager::class)->deleteFromSelf($share, $this->user); |
|
| 470 | + } |
|
| 471 | + return true; |
|
| 472 | + } |
|
| 473 | + |
|
| 474 | + public function acquireLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 475 | + /** @var ILockingStorage $targetStorage */ |
|
| 476 | + [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 477 | + $targetStorage->acquireLock($targetInternalPath, $type, $provider); |
|
| 478 | + // lock the parent folders of the owner when locking the share as recipient |
|
| 479 | + if ($path === '') { |
|
| 480 | + $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); |
|
| 481 | + $this->ownerView->lockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); |
|
| 482 | + } |
|
| 483 | + } |
|
| 484 | + |
|
| 485 | + public function releaseLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 486 | + /** @var ILockingStorage $targetStorage */ |
|
| 487 | + [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 488 | + $targetStorage->releaseLock($targetInternalPath, $type, $provider); |
|
| 489 | + // unlock the parent folders of the owner when unlocking the share as recipient |
|
| 490 | + if ($path === '') { |
|
| 491 | + $sourcePath = $this->ownerUserFolder->getRelativePath($this->sourcePath); |
|
| 492 | + $this->ownerView->unlockFile(dirname($sourcePath), ILockingProvider::LOCK_SHARED, true); |
|
| 493 | + } |
|
| 494 | + } |
|
| 495 | + |
|
| 496 | + public function changeLock(string $path, int $type, ILockingProvider $provider): void { |
|
| 497 | + /** @var ILockingStorage $targetStorage */ |
|
| 498 | + [$targetStorage, $targetInternalPath] = $this->resolvePath($path); |
|
| 499 | + $targetStorage->changeLock($targetInternalPath, $type, $provider); |
|
| 500 | + } |
|
| 501 | + |
|
| 502 | + public function getAvailability(): array { |
|
| 503 | + // shares do not participate in availability logic |
|
| 504 | + return [ |
|
| 505 | + 'available' => true, |
|
| 506 | + 'last_checked' => 0, |
|
| 507 | + ]; |
|
| 508 | + } |
|
| 509 | + |
|
| 510 | + public function setAvailability(bool $isAvailable): void { |
|
| 511 | + // shares do not participate in availability logic |
|
| 512 | + } |
|
| 513 | + |
|
| 514 | + public function getSourceStorage() { |
|
| 515 | + $this->init(); |
|
| 516 | + return $this->nonMaskedStorage; |
|
| 517 | + } |
|
| 518 | + |
|
| 519 | + public function getWrapperStorage(): Storage { |
|
| 520 | + $this->init(); |
|
| 521 | + |
|
| 522 | + /** |
|
| 523 | + * @psalm-suppress DocblockTypeContradiction |
|
| 524 | + */ |
|
| 525 | + if (!$this->storage) { |
|
| 526 | + $message = 'no storage set after init for share ' . $this->getShareId(); |
|
| 527 | + $this->logger->error($message); |
|
| 528 | + $this->storage = new FailedStorage(['exception' => new \Exception($message)]); |
|
| 529 | + } |
|
| 530 | + |
|
| 531 | + return $this->storage; |
|
| 532 | + } |
|
| 533 | + |
|
| 534 | + public function file_get_contents(string $path): string|false { |
|
| 535 | + $info = [ |
|
| 536 | + 'target' => $this->getMountPoint() . '/' . $path, |
|
| 537 | + 'source' => $this->getUnjailedPath($path), |
|
| 538 | + ]; |
|
| 539 | + Util::emitHook('\OC\Files\Storage\Shared', 'file_get_contents', $info); |
|
| 540 | + return parent::file_get_contents($path); |
|
| 541 | + } |
|
| 542 | + |
|
| 543 | + public function file_put_contents(string $path, mixed $data): int|float|false { |
|
| 544 | + $info = [ |
|
| 545 | + 'target' => $this->getMountPoint() . '/' . $path, |
|
| 546 | + 'source' => $this->getUnjailedPath($path), |
|
| 547 | + ]; |
|
| 548 | + Util::emitHook('\OC\Files\Storage\Shared', 'file_put_contents', $info); |
|
| 549 | + return parent::file_put_contents($path, $data); |
|
| 550 | + } |
|
| 551 | + |
|
| 552 | + public function setMountOptions(array $options): void { |
|
| 553 | + /* Note: This value is never read */ |
|
| 554 | + $this->mountOptions = $options; |
|
| 555 | + } |
|
| 556 | + |
|
| 557 | + public function getUnjailedPath(string $path): string { |
|
| 558 | + $this->init(); |
|
| 559 | + return parent::getUnjailedPath($path); |
|
| 560 | + } |
|
| 561 | + |
|
| 562 | + #[Override] |
|
| 563 | + public function getDirectDownload(string $path): array|false { |
|
| 564 | + // disable direct download for shares |
|
| 565 | + return false; |
|
| 566 | + } |
|
| 567 | + |
|
| 568 | + #[Override] |
|
| 569 | + public function getDirectDownloadById(string $fileId): array|false { |
|
| 570 | + // disable direct download for shares |
|
| 571 | + return false; |
|
| 572 | + } |
|
| 573 | 573 | } |
@@ -50,591 +50,591 @@ |
||
| 50 | 50 | use Sabre\DAV\IFile; |
| 51 | 51 | |
| 52 | 52 | class File extends Node implements IFile { |
| 53 | - protected IRequest $request; |
|
| 54 | - protected IL10N $l10n; |
|
| 55 | - |
|
| 56 | - /** |
|
| 57 | - * Sets up the node, expects a full path name |
|
| 58 | - * |
|
| 59 | - * @param View $view |
|
| 60 | - * @param FileInfo $info |
|
| 61 | - * @param ?\OCP\Share\IManager $shareManager |
|
| 62 | - * @param ?IRequest $request |
|
| 63 | - * @param ?IL10N $l10n |
|
| 64 | - */ |
|
| 65 | - public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) { |
|
| 66 | - parent::__construct($view, $info, $shareManager); |
|
| 67 | - |
|
| 68 | - if ($l10n) { |
|
| 69 | - $this->l10n = $l10n; |
|
| 70 | - } else { |
|
| 71 | - // Querying IL10N directly results in a dependency loop |
|
| 72 | - /** @var IL10NFactory $l10nFactory */ |
|
| 73 | - $l10nFactory = Server::get(IL10NFactory::class); |
|
| 74 | - $this->l10n = $l10nFactory->get(Application::APP_ID); |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - if (isset($request)) { |
|
| 78 | - $this->request = $request; |
|
| 79 | - } else { |
|
| 80 | - $this->request = Server::get(IRequest::class); |
|
| 81 | - } |
|
| 82 | - } |
|
| 83 | - |
|
| 84 | - /** |
|
| 85 | - * Updates the data |
|
| 86 | - * |
|
| 87 | - * The data argument is a readable stream resource. |
|
| 88 | - * |
|
| 89 | - * After a successful put operation, you may choose to return an ETag. The |
|
| 90 | - * etag must always be surrounded by double-quotes. These quotes must |
|
| 91 | - * appear in the actual string you're returning. |
|
| 92 | - * |
|
| 93 | - * Clients may use the ETag from a PUT request to later on make sure that |
|
| 94 | - * when they update the file, the contents haven't changed in the mean |
|
| 95 | - * time. |
|
| 96 | - * |
|
| 97 | - * If you don't plan to store the file byte-by-byte, and you return a |
|
| 98 | - * different object on a subsequent GET you are strongly recommended to not |
|
| 99 | - * return an ETag, and just return null. |
|
| 100 | - * |
|
| 101 | - * @param resource|string $data |
|
| 102 | - * |
|
| 103 | - * @throws Forbidden |
|
| 104 | - * @throws UnsupportedMediaType |
|
| 105 | - * @throws BadRequest |
|
| 106 | - * @throws Exception |
|
| 107 | - * @throws EntityTooLarge |
|
| 108 | - * @throws ServiceUnavailable |
|
| 109 | - * @throws FileLocked |
|
| 110 | - * @return string|null |
|
| 111 | - */ |
|
| 112 | - public function put($data) { |
|
| 113 | - try { |
|
| 114 | - $exists = $this->fileView->file_exists($this->path); |
|
| 115 | - if ($exists && !$this->info->isUpdateable()) { |
|
| 116 | - throw new Forbidden(); |
|
| 117 | - } |
|
| 118 | - } catch (StorageNotAvailableException $e) { |
|
| 119 | - throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()])); |
|
| 120 | - } |
|
| 121 | - |
|
| 122 | - // verify path of the target |
|
| 123 | - $this->verifyPath(); |
|
| 124 | - |
|
| 125 | - [$partStorage] = $this->fileView->resolvePath($this->path); |
|
| 126 | - if ($partStorage === null) { |
|
| 127 | - throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); |
|
| 128 | - } |
|
| 129 | - $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); |
|
| 130 | - |
|
| 131 | - $view = Filesystem::getView(); |
|
| 132 | - |
|
| 133 | - if ($needsPartFile) { |
|
| 134 | - $transferId = \rand(); |
|
| 135 | - // mark file as partial while uploading (ignored by the scanner) |
|
| 136 | - $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part'; |
|
| 137 | - |
|
| 138 | - if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
|
| 139 | - $needsPartFile = false; |
|
| 140 | - } |
|
| 141 | - } |
|
| 142 | - if (!$needsPartFile) { |
|
| 143 | - // upload file directly as the final path |
|
| 144 | - $partFilePath = $this->path; |
|
| 145 | - |
|
| 146 | - if ($view && !$this->emitPreHooks($exists)) { |
|
| 147 | - throw new Exception($this->l10n->t('Could not write to final file, canceled by hook')); |
|
| 148 | - } |
|
| 149 | - } |
|
| 150 | - |
|
| 151 | - // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
|
| 152 | - [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath); |
|
| 153 | - [$storage, $internalPath] = $this->fileView->resolvePath($this->path); |
|
| 154 | - if ($partStorage === null || $storage === null) { |
|
| 155 | - throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); |
|
| 156 | - } |
|
| 157 | - try { |
|
| 158 | - if (!$needsPartFile) { |
|
| 159 | - try { |
|
| 160 | - $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 161 | - } catch (LockedException $e) { |
|
| 162 | - // during very large uploads, the shared lock we got at the start might have been expired |
|
| 163 | - // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
| 164 | - // or because there is no existing shared lock to make exclusive |
|
| 165 | - // |
|
| 166 | - // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
| 167 | - // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
| 168 | - // the entire operation will be safe |
|
| 169 | - |
|
| 170 | - try { |
|
| 171 | - $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 172 | - } catch (LockedException $ex) { |
|
| 173 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 174 | - } |
|
| 175 | - } |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - if (!is_resource($data)) { |
|
| 179 | - $tmpData = fopen('php://temp', 'r+'); |
|
| 180 | - if ($data !== null) { |
|
| 181 | - fwrite($tmpData, $data); |
|
| 182 | - rewind($tmpData); |
|
| 183 | - } |
|
| 184 | - $data = $tmpData; |
|
| 185 | - } |
|
| 186 | - |
|
| 187 | - if ($this->request->getHeader('X-HASH') !== '') { |
|
| 188 | - $hash = $this->request->getHeader('X-HASH'); |
|
| 189 | - if ($hash === 'all' || $hash === 'md5') { |
|
| 190 | - $data = HashWrapper::wrap($data, 'md5', function ($hash): void { |
|
| 191 | - $this->header('X-Hash-MD5: ' . $hash); |
|
| 192 | - }); |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - if ($hash === 'all' || $hash === 'sha1') { |
|
| 196 | - $data = HashWrapper::wrap($data, 'sha1', function ($hash): void { |
|
| 197 | - $this->header('X-Hash-SHA1: ' . $hash); |
|
| 198 | - }); |
|
| 199 | - } |
|
| 200 | - |
|
| 201 | - if ($hash === 'all' || $hash === 'sha256') { |
|
| 202 | - $data = HashWrapper::wrap($data, 'sha256', function ($hash): void { |
|
| 203 | - $this->header('X-Hash-SHA256: ' . $hash); |
|
| 204 | - }); |
|
| 205 | - } |
|
| 206 | - } |
|
| 207 | - |
|
| 208 | - $lengthHeader = $this->request->getHeader('content-length'); |
|
| 209 | - $expected = $lengthHeader !== '' ? (int)$lengthHeader : null; |
|
| 210 | - |
|
| 211 | - if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) { |
|
| 212 | - $isEOF = false; |
|
| 213 | - $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void { |
|
| 214 | - $isEOF = feof($stream); |
|
| 215 | - }); |
|
| 216 | - |
|
| 217 | - $result = is_resource($wrappedData); |
|
| 218 | - if ($result) { |
|
| 219 | - $count = -1; |
|
| 220 | - try { |
|
| 221 | - /** @var IWriteStreamStorage $partStorage */ |
|
| 222 | - $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected); |
|
| 223 | - } catch (GenericFileException $e) { |
|
| 224 | - $logger = Server::get(LoggerInterface::class); |
|
| 225 | - $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']); |
|
| 226 | - $result = $isEOF; |
|
| 227 | - if (is_resource($wrappedData)) { |
|
| 228 | - $result = feof($wrappedData); |
|
| 229 | - } |
|
| 230 | - } |
|
| 231 | - } |
|
| 232 | - } else { |
|
| 233 | - $target = $partStorage->fopen($internalPartPath, 'wb'); |
|
| 234 | - if ($target === false) { |
|
| 235 | - Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); |
|
| 236 | - // because we have no clue about the cause we can only throw back a 500/Internal Server Error |
|
| 237 | - throw new Exception($this->l10n->t('Could not write file contents')); |
|
| 238 | - } |
|
| 239 | - [$count, $result] = Files::streamCopy($data, $target, true); |
|
| 240 | - fclose($target); |
|
| 241 | - } |
|
| 242 | - if ($result === false && $expected !== null) { |
|
| 243 | - throw new Exception( |
|
| 244 | - $this->l10n->t( |
|
| 245 | - 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', |
|
| 246 | - [ |
|
| 247 | - $this->l10n->n('%n byte', '%n bytes', $count), |
|
| 248 | - $this->l10n->n('%n byte', '%n bytes', $expected), |
|
| 249 | - ], |
|
| 250 | - ) |
|
| 251 | - ); |
|
| 252 | - } |
|
| 253 | - |
|
| 254 | - // if content length is sent by client: |
|
| 255 | - // double check if the file was fully received |
|
| 256 | - // compare expected and actual size |
|
| 257 | - if ($expected !== null |
|
| 258 | - && $expected !== $count |
|
| 259 | - && $this->request->getMethod() === 'PUT' |
|
| 260 | - ) { |
|
| 261 | - throw new BadRequest( |
|
| 262 | - $this->l10n->t( |
|
| 263 | - 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.', |
|
| 264 | - [ |
|
| 265 | - $this->l10n->n('%n byte', '%n bytes', $expected), |
|
| 266 | - $this->l10n->n('%n byte', '%n bytes', $count), |
|
| 267 | - ], |
|
| 268 | - ) |
|
| 269 | - ); |
|
| 270 | - } |
|
| 271 | - } catch (\Exception $e) { |
|
| 272 | - if ($e instanceof LockedException) { |
|
| 273 | - Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]); |
|
| 274 | - } else { |
|
| 275 | - Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - if ($needsPartFile) { |
|
| 279 | - $partStorage->unlink($internalPartPath); |
|
| 280 | - } |
|
| 281 | - $this->convertToSabreException($e); |
|
| 282 | - } |
|
| 283 | - |
|
| 284 | - try { |
|
| 285 | - if ($needsPartFile) { |
|
| 286 | - if ($view && !$this->emitPreHooks($exists)) { |
|
| 287 | - $partStorage->unlink($internalPartPath); |
|
| 288 | - throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook')); |
|
| 289 | - } |
|
| 290 | - try { |
|
| 291 | - $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 292 | - } catch (LockedException $e) { |
|
| 293 | - // during very large uploads, the shared lock we got at the start might have been expired |
|
| 294 | - // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
| 295 | - // or because there is no existing shared lock to make exclusive |
|
| 296 | - // |
|
| 297 | - // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
| 298 | - // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
| 299 | - // the entire operation will be safe |
|
| 300 | - |
|
| 301 | - try { |
|
| 302 | - $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 303 | - } catch (LockedException $ex) { |
|
| 304 | - if ($needsPartFile) { |
|
| 305 | - $partStorage->unlink($internalPartPath); |
|
| 306 | - } |
|
| 307 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 308 | - } |
|
| 309 | - } |
|
| 310 | - |
|
| 311 | - // rename to correct path |
|
| 312 | - try { |
|
| 313 | - $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
|
| 314 | - $fileExists = $storage->file_exists($internalPath); |
|
| 315 | - if ($renameOkay === false || $fileExists === false) { |
|
| 316 | - Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
|
| 317 | - throw new Exception($this->l10n->t('Could not rename part file to final file')); |
|
| 318 | - } |
|
| 319 | - } catch (ForbiddenException $ex) { |
|
| 320 | - if (!$ex->getRetry()) { |
|
| 321 | - $partStorage->unlink($internalPartPath); |
|
| 322 | - } |
|
| 323 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 324 | - } catch (\Exception $e) { |
|
| 325 | - $partStorage->unlink($internalPartPath); |
|
| 326 | - $this->convertToSabreException($e); |
|
| 327 | - } |
|
| 328 | - } |
|
| 329 | - |
|
| 330 | - // since we skipped the view we need to scan and emit the hooks ourselves |
|
| 331 | - $storage->getUpdater()->update($internalPath); |
|
| 332 | - |
|
| 333 | - try { |
|
| 334 | - $this->changeLock(ILockingProvider::LOCK_SHARED); |
|
| 335 | - } catch (LockedException $e) { |
|
| 336 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 337 | - } |
|
| 338 | - |
|
| 339 | - // allow sync clients to send the mtime along in a header |
|
| 340 | - $mtimeHeader = $this->request->getHeader('x-oc-mtime'); |
|
| 341 | - if ($mtimeHeader !== '') { |
|
| 342 | - $mtime = $this->sanitizeMtime($mtimeHeader); |
|
| 343 | - if ($this->fileView->touch($this->path, $mtime)) { |
|
| 344 | - $this->header('X-OC-MTime: accepted'); |
|
| 345 | - } |
|
| 346 | - } |
|
| 347 | - |
|
| 348 | - $fileInfoUpdate = [ |
|
| 349 | - 'upload_time' => time() |
|
| 350 | - ]; |
|
| 351 | - |
|
| 352 | - // allow sync clients to send the creation time along in a header |
|
| 353 | - $ctimeHeader = $this->request->getHeader('x-oc-ctime'); |
|
| 354 | - if ($ctimeHeader) { |
|
| 355 | - $ctime = $this->sanitizeMtime($ctimeHeader); |
|
| 356 | - $fileInfoUpdate['creation_time'] = $ctime; |
|
| 357 | - $this->header('X-OC-CTime: accepted'); |
|
| 358 | - } |
|
| 359 | - |
|
| 360 | - $this->fileView->putFileInfo($this->path, $fileInfoUpdate); |
|
| 361 | - |
|
| 362 | - if ($view) { |
|
| 363 | - $this->emitPostHooks($exists); |
|
| 364 | - } |
|
| 365 | - |
|
| 366 | - $this->refreshInfo(); |
|
| 367 | - |
|
| 368 | - $checksumHeader = $this->request->getHeader('oc-checksum'); |
|
| 369 | - if ($checksumHeader) { |
|
| 370 | - $checksum = trim($checksumHeader); |
|
| 371 | - $this->setChecksum($checksum); |
|
| 372 | - } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') { |
|
| 373 | - $this->setChecksum(''); |
|
| 374 | - } |
|
| 375 | - } catch (StorageNotAvailableException $e) { |
|
| 376 | - throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e); |
|
| 377 | - } |
|
| 378 | - |
|
| 379 | - return '"' . $this->info->getEtag() . '"'; |
|
| 380 | - } |
|
| 381 | - |
|
| 382 | - private function getPartFileBasePath($path) { |
|
| 383 | - $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true); |
|
| 384 | - if ($partFileInStorage) { |
|
| 385 | - $filename = basename($path); |
|
| 386 | - // hash does not need to be secure but fast and semi unique |
|
| 387 | - $hashedFilename = hash('xxh128', $filename); |
|
| 388 | - return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename; |
|
| 389 | - } else { |
|
| 390 | - // will place the .part file in the users root directory |
|
| 391 | - // therefor we need to make the name (semi) unique - hash does not need to be secure but fast. |
|
| 392 | - return hash('xxh128', $path); |
|
| 393 | - } |
|
| 394 | - } |
|
| 395 | - |
|
| 396 | - private function emitPreHooks(bool $exists, ?string $path = null): bool { |
|
| 397 | - if (is_null($path)) { |
|
| 398 | - $path = $this->path; |
|
| 399 | - } |
|
| 400 | - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
| 401 | - if ($hookPath === null) { |
|
| 402 | - // We only trigger hooks from inside default view |
|
| 403 | - return true; |
|
| 404 | - } |
|
| 405 | - $run = true; |
|
| 406 | - |
|
| 407 | - if (!$exists) { |
|
| 408 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [ |
|
| 409 | - Filesystem::signal_param_path => $hookPath, |
|
| 410 | - Filesystem::signal_param_run => &$run, |
|
| 411 | - ]); |
|
| 412 | - } else { |
|
| 413 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [ |
|
| 414 | - Filesystem::signal_param_path => $hookPath, |
|
| 415 | - Filesystem::signal_param_run => &$run, |
|
| 416 | - ]); |
|
| 417 | - } |
|
| 418 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [ |
|
| 419 | - Filesystem::signal_param_path => $hookPath, |
|
| 420 | - Filesystem::signal_param_run => &$run, |
|
| 421 | - ]); |
|
| 422 | - return $run; |
|
| 423 | - } |
|
| 424 | - |
|
| 425 | - private function emitPostHooks(bool $exists, ?string $path = null): void { |
|
| 426 | - if (is_null($path)) { |
|
| 427 | - $path = $this->path; |
|
| 428 | - } |
|
| 429 | - $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
| 430 | - if ($hookPath === null) { |
|
| 431 | - // We only trigger hooks from inside default view |
|
| 432 | - return; |
|
| 433 | - } |
|
| 434 | - if (!$exists) { |
|
| 435 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [ |
|
| 436 | - Filesystem::signal_param_path => $hookPath |
|
| 437 | - ]); |
|
| 438 | - } else { |
|
| 439 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [ |
|
| 440 | - Filesystem::signal_param_path => $hookPath |
|
| 441 | - ]); |
|
| 442 | - } |
|
| 443 | - \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [ |
|
| 444 | - Filesystem::signal_param_path => $hookPath |
|
| 445 | - ]); |
|
| 446 | - } |
|
| 447 | - |
|
| 448 | - /** |
|
| 449 | - * Returns the data |
|
| 450 | - * |
|
| 451 | - * @return resource |
|
| 452 | - * @throws Forbidden |
|
| 453 | - * @throws ServiceUnavailable |
|
| 454 | - */ |
|
| 455 | - public function get() { |
|
| 456 | - //throw exception if encryption is disabled but files are still encrypted |
|
| 457 | - try { |
|
| 458 | - if (!$this->info->isReadable()) { |
|
| 459 | - // do a if the file did not exist |
|
| 460 | - throw new NotFound(); |
|
| 461 | - } |
|
| 462 | - $path = ltrim($this->path, '/'); |
|
| 463 | - try { |
|
| 464 | - $res = $this->fileView->fopen($path, 'rb'); |
|
| 465 | - } catch (\Exception $e) { |
|
| 466 | - $this->convertToSabreException($e); |
|
| 467 | - } |
|
| 468 | - |
|
| 469 | - if ($res === false) { |
|
| 470 | - if ($this->fileView->file_exists($path)) { |
|
| 471 | - throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path])); |
|
| 472 | - } else { |
|
| 473 | - throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path])); |
|
| 474 | - } |
|
| 475 | - } |
|
| 476 | - |
|
| 477 | - // comparing current file size with the one in DB |
|
| 478 | - // if different, fix DB and refresh cache. |
|
| 479 | - if ($this->getSize() !== $this->fileView->filesize($this->getPath())) { |
|
| 480 | - $logger = Server::get(LoggerInterface::class); |
|
| 481 | - $logger->warning('fixing cached size of file id=' . $this->getId()); |
|
| 482 | - |
|
| 483 | - $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath()); |
|
| 484 | - $this->refreshInfo(); |
|
| 485 | - } |
|
| 486 | - |
|
| 487 | - return $res; |
|
| 488 | - } catch (GenericEncryptionException $e) { |
|
| 489 | - // returning 503 will allow retry of the operation at a later point in time |
|
| 490 | - throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()])); |
|
| 491 | - } catch (StorageNotAvailableException $e) { |
|
| 492 | - throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()])); |
|
| 493 | - } catch (ForbiddenException $ex) { |
|
| 494 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 495 | - } catch (LockedException $e) { |
|
| 496 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 497 | - } |
|
| 498 | - } |
|
| 499 | - |
|
| 500 | - /** |
|
| 501 | - * Delete the current file |
|
| 502 | - * |
|
| 503 | - * @throws Forbidden |
|
| 504 | - * @throws ServiceUnavailable |
|
| 505 | - */ |
|
| 506 | - public function delete() { |
|
| 507 | - if (!$this->info->isDeletable()) { |
|
| 508 | - throw new Forbidden(); |
|
| 509 | - } |
|
| 510 | - |
|
| 511 | - try { |
|
| 512 | - if (!$this->fileView->unlink($this->path)) { |
|
| 513 | - // assume it wasn't possible to delete due to permissions |
|
| 514 | - throw new Forbidden(); |
|
| 515 | - } |
|
| 516 | - } catch (StorageNotAvailableException $e) { |
|
| 517 | - throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()])); |
|
| 518 | - } catch (ForbiddenException $ex) { |
|
| 519 | - throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 520 | - } catch (LockedException $e) { |
|
| 521 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 522 | - } |
|
| 523 | - } |
|
| 524 | - |
|
| 525 | - /** |
|
| 526 | - * Returns the mime-type for a file |
|
| 527 | - * |
|
| 528 | - * If null is returned, we'll assume application/octet-stream |
|
| 529 | - * |
|
| 530 | - * @return string |
|
| 531 | - */ |
|
| 532 | - public function getContentType() { |
|
| 533 | - $mimeType = $this->info->getMimetype(); |
|
| 534 | - |
|
| 535 | - // PROPFIND needs to return the correct mime type, for consistency with the web UI |
|
| 536 | - if ($this->request->getMethod() === 'PROPFIND') { |
|
| 537 | - return $mimeType; |
|
| 538 | - } |
|
| 539 | - return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType); |
|
| 540 | - } |
|
| 541 | - |
|
| 542 | - /** |
|
| 543 | - * @throws NotFoundException |
|
| 544 | - * @throws NotPermittedException |
|
| 545 | - */ |
|
| 546 | - public function getDirectDownload(): array|false { |
|
| 547 | - if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) { |
|
| 548 | - return false; |
|
| 549 | - } |
|
| 550 | - $node = $this->getNode(); |
|
| 551 | - $storage = $node->getStorage(); |
|
| 552 | - if (!$storage) { |
|
| 553 | - return false; |
|
| 554 | - } |
|
| 555 | - |
|
| 556 | - if (!($node->getPermissions() & Constants::PERMISSION_READ)) { |
|
| 557 | - return false; |
|
| 558 | - } |
|
| 559 | - |
|
| 560 | - return $storage->getDirectDownloadById((string)$node->getId()); |
|
| 561 | - } |
|
| 562 | - |
|
| 563 | - /** |
|
| 564 | - * Convert the given exception to a SabreException instance |
|
| 565 | - * |
|
| 566 | - * @param \Exception $e |
|
| 567 | - * |
|
| 568 | - * @throws \Sabre\DAV\Exception |
|
| 569 | - */ |
|
| 570 | - private function convertToSabreException(\Exception $e) { |
|
| 571 | - if ($e instanceof \Sabre\DAV\Exception) { |
|
| 572 | - throw $e; |
|
| 573 | - } |
|
| 574 | - if ($e instanceof NotPermittedException) { |
|
| 575 | - // a more general case - due to whatever reason the content could not be written |
|
| 576 | - throw new Forbidden($e->getMessage(), 0, $e); |
|
| 577 | - } |
|
| 578 | - if ($e instanceof ForbiddenException) { |
|
| 579 | - // the path for the file was forbidden |
|
| 580 | - throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
|
| 581 | - } |
|
| 582 | - if ($e instanceof EntityTooLargeException) { |
|
| 583 | - // the file is too big to be stored |
|
| 584 | - throw new EntityTooLarge($e->getMessage(), 0, $e); |
|
| 585 | - } |
|
| 586 | - if ($e instanceof InvalidContentException) { |
|
| 587 | - // the file content is not permitted |
|
| 588 | - throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
|
| 589 | - } |
|
| 590 | - if ($e instanceof InvalidPathException) { |
|
| 591 | - // the path for the file was not valid |
|
| 592 | - // TODO: find proper http status code for this case |
|
| 593 | - throw new Forbidden($e->getMessage(), 0, $e); |
|
| 594 | - } |
|
| 595 | - if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
|
| 596 | - // the file is currently being written to by another process |
|
| 597 | - throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 598 | - } |
|
| 599 | - if ($e instanceof GenericEncryptionException) { |
|
| 600 | - // returning 503 will allow retry of the operation at a later point in time |
|
| 601 | - throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e); |
|
| 602 | - } |
|
| 603 | - if ($e instanceof StorageNotAvailableException) { |
|
| 604 | - throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e); |
|
| 605 | - } |
|
| 606 | - if ($e instanceof NotFoundException) { |
|
| 607 | - throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e); |
|
| 608 | - } |
|
| 609 | - |
|
| 610 | - throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
|
| 611 | - } |
|
| 612 | - |
|
| 613 | - /** |
|
| 614 | - * Get the checksum for this file |
|
| 615 | - * |
|
| 616 | - * @return string|null |
|
| 617 | - */ |
|
| 618 | - public function getChecksum() { |
|
| 619 | - return $this->info->getChecksum(); |
|
| 620 | - } |
|
| 621 | - |
|
| 622 | - public function setChecksum(string $checksum) { |
|
| 623 | - $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); |
|
| 624 | - $this->refreshInfo(); |
|
| 625 | - } |
|
| 626 | - |
|
| 627 | - protected function header($string) { |
|
| 628 | - if (!\OC::$CLI) { |
|
| 629 | - \header($string); |
|
| 630 | - } |
|
| 631 | - } |
|
| 632 | - |
|
| 633 | - public function hash(string $type) { |
|
| 634 | - return $this->fileView->hash($type, $this->path); |
|
| 635 | - } |
|
| 636 | - |
|
| 637 | - public function getNode(): \OCP\Files\File { |
|
| 638 | - return $this->node; |
|
| 639 | - } |
|
| 53 | + protected IRequest $request; |
|
| 54 | + protected IL10N $l10n; |
|
| 55 | + |
|
| 56 | + /** |
|
| 57 | + * Sets up the node, expects a full path name |
|
| 58 | + * |
|
| 59 | + * @param View $view |
|
| 60 | + * @param FileInfo $info |
|
| 61 | + * @param ?\OCP\Share\IManager $shareManager |
|
| 62 | + * @param ?IRequest $request |
|
| 63 | + * @param ?IL10N $l10n |
|
| 64 | + */ |
|
| 65 | + public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) { |
|
| 66 | + parent::__construct($view, $info, $shareManager); |
|
| 67 | + |
|
| 68 | + if ($l10n) { |
|
| 69 | + $this->l10n = $l10n; |
|
| 70 | + } else { |
|
| 71 | + // Querying IL10N directly results in a dependency loop |
|
| 72 | + /** @var IL10NFactory $l10nFactory */ |
|
| 73 | + $l10nFactory = Server::get(IL10NFactory::class); |
|
| 74 | + $this->l10n = $l10nFactory->get(Application::APP_ID); |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + if (isset($request)) { |
|
| 78 | + $this->request = $request; |
|
| 79 | + } else { |
|
| 80 | + $this->request = Server::get(IRequest::class); |
|
| 81 | + } |
|
| 82 | + } |
|
| 83 | + |
|
| 84 | + /** |
|
| 85 | + * Updates the data |
|
| 86 | + * |
|
| 87 | + * The data argument is a readable stream resource. |
|
| 88 | + * |
|
| 89 | + * After a successful put operation, you may choose to return an ETag. The |
|
| 90 | + * etag must always be surrounded by double-quotes. These quotes must |
|
| 91 | + * appear in the actual string you're returning. |
|
| 92 | + * |
|
| 93 | + * Clients may use the ETag from a PUT request to later on make sure that |
|
| 94 | + * when they update the file, the contents haven't changed in the mean |
|
| 95 | + * time. |
|
| 96 | + * |
|
| 97 | + * If you don't plan to store the file byte-by-byte, and you return a |
|
| 98 | + * different object on a subsequent GET you are strongly recommended to not |
|
| 99 | + * return an ETag, and just return null. |
|
| 100 | + * |
|
| 101 | + * @param resource|string $data |
|
| 102 | + * |
|
| 103 | + * @throws Forbidden |
|
| 104 | + * @throws UnsupportedMediaType |
|
| 105 | + * @throws BadRequest |
|
| 106 | + * @throws Exception |
|
| 107 | + * @throws EntityTooLarge |
|
| 108 | + * @throws ServiceUnavailable |
|
| 109 | + * @throws FileLocked |
|
| 110 | + * @return string|null |
|
| 111 | + */ |
|
| 112 | + public function put($data) { |
|
| 113 | + try { |
|
| 114 | + $exists = $this->fileView->file_exists($this->path); |
|
| 115 | + if ($exists && !$this->info->isUpdateable()) { |
|
| 116 | + throw new Forbidden(); |
|
| 117 | + } |
|
| 118 | + } catch (StorageNotAvailableException $e) { |
|
| 119 | + throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()])); |
|
| 120 | + } |
|
| 121 | + |
|
| 122 | + // verify path of the target |
|
| 123 | + $this->verifyPath(); |
|
| 124 | + |
|
| 125 | + [$partStorage] = $this->fileView->resolvePath($this->path); |
|
| 126 | + if ($partStorage === null) { |
|
| 127 | + throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); |
|
| 128 | + } |
|
| 129 | + $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); |
|
| 130 | + |
|
| 131 | + $view = Filesystem::getView(); |
|
| 132 | + |
|
| 133 | + if ($needsPartFile) { |
|
| 134 | + $transferId = \rand(); |
|
| 135 | + // mark file as partial while uploading (ignored by the scanner) |
|
| 136 | + $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part'; |
|
| 137 | + |
|
| 138 | + if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
|
| 139 | + $needsPartFile = false; |
|
| 140 | + } |
|
| 141 | + } |
|
| 142 | + if (!$needsPartFile) { |
|
| 143 | + // upload file directly as the final path |
|
| 144 | + $partFilePath = $this->path; |
|
| 145 | + |
|
| 146 | + if ($view && !$this->emitPreHooks($exists)) { |
|
| 147 | + throw new Exception($this->l10n->t('Could not write to final file, canceled by hook')); |
|
| 148 | + } |
|
| 149 | + } |
|
| 150 | + |
|
| 151 | + // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
|
| 152 | + [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath); |
|
| 153 | + [$storage, $internalPath] = $this->fileView->resolvePath($this->path); |
|
| 154 | + if ($partStorage === null || $storage === null) { |
|
| 155 | + throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file')); |
|
| 156 | + } |
|
| 157 | + try { |
|
| 158 | + if (!$needsPartFile) { |
|
| 159 | + try { |
|
| 160 | + $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 161 | + } catch (LockedException $e) { |
|
| 162 | + // during very large uploads, the shared lock we got at the start might have been expired |
|
| 163 | + // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
| 164 | + // or because there is no existing shared lock to make exclusive |
|
| 165 | + // |
|
| 166 | + // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
| 167 | + // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
| 168 | + // the entire operation will be safe |
|
| 169 | + |
|
| 170 | + try { |
|
| 171 | + $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 172 | + } catch (LockedException $ex) { |
|
| 173 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 174 | + } |
|
| 175 | + } |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + if (!is_resource($data)) { |
|
| 179 | + $tmpData = fopen('php://temp', 'r+'); |
|
| 180 | + if ($data !== null) { |
|
| 181 | + fwrite($tmpData, $data); |
|
| 182 | + rewind($tmpData); |
|
| 183 | + } |
|
| 184 | + $data = $tmpData; |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + if ($this->request->getHeader('X-HASH') !== '') { |
|
| 188 | + $hash = $this->request->getHeader('X-HASH'); |
|
| 189 | + if ($hash === 'all' || $hash === 'md5') { |
|
| 190 | + $data = HashWrapper::wrap($data, 'md5', function ($hash): void { |
|
| 191 | + $this->header('X-Hash-MD5: ' . $hash); |
|
| 192 | + }); |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + if ($hash === 'all' || $hash === 'sha1') { |
|
| 196 | + $data = HashWrapper::wrap($data, 'sha1', function ($hash): void { |
|
| 197 | + $this->header('X-Hash-SHA1: ' . $hash); |
|
| 198 | + }); |
|
| 199 | + } |
|
| 200 | + |
|
| 201 | + if ($hash === 'all' || $hash === 'sha256') { |
|
| 202 | + $data = HashWrapper::wrap($data, 'sha256', function ($hash): void { |
|
| 203 | + $this->header('X-Hash-SHA256: ' . $hash); |
|
| 204 | + }); |
|
| 205 | + } |
|
| 206 | + } |
|
| 207 | + |
|
| 208 | + $lengthHeader = $this->request->getHeader('content-length'); |
|
| 209 | + $expected = $lengthHeader !== '' ? (int)$lengthHeader : null; |
|
| 210 | + |
|
| 211 | + if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) { |
|
| 212 | + $isEOF = false; |
|
| 213 | + $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void { |
|
| 214 | + $isEOF = feof($stream); |
|
| 215 | + }); |
|
| 216 | + |
|
| 217 | + $result = is_resource($wrappedData); |
|
| 218 | + if ($result) { |
|
| 219 | + $count = -1; |
|
| 220 | + try { |
|
| 221 | + /** @var IWriteStreamStorage $partStorage */ |
|
| 222 | + $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected); |
|
| 223 | + } catch (GenericFileException $e) { |
|
| 224 | + $logger = Server::get(LoggerInterface::class); |
|
| 225 | + $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']); |
|
| 226 | + $result = $isEOF; |
|
| 227 | + if (is_resource($wrappedData)) { |
|
| 228 | + $result = feof($wrappedData); |
|
| 229 | + } |
|
| 230 | + } |
|
| 231 | + } |
|
| 232 | + } else { |
|
| 233 | + $target = $partStorage->fopen($internalPartPath, 'wb'); |
|
| 234 | + if ($target === false) { |
|
| 235 | + Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); |
|
| 236 | + // because we have no clue about the cause we can only throw back a 500/Internal Server Error |
|
| 237 | + throw new Exception($this->l10n->t('Could not write file contents')); |
|
| 238 | + } |
|
| 239 | + [$count, $result] = Files::streamCopy($data, $target, true); |
|
| 240 | + fclose($target); |
|
| 241 | + } |
|
| 242 | + if ($result === false && $expected !== null) { |
|
| 243 | + throw new Exception( |
|
| 244 | + $this->l10n->t( |
|
| 245 | + 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', |
|
| 246 | + [ |
|
| 247 | + $this->l10n->n('%n byte', '%n bytes', $count), |
|
| 248 | + $this->l10n->n('%n byte', '%n bytes', $expected), |
|
| 249 | + ], |
|
| 250 | + ) |
|
| 251 | + ); |
|
| 252 | + } |
|
| 253 | + |
|
| 254 | + // if content length is sent by client: |
|
| 255 | + // double check if the file was fully received |
|
| 256 | + // compare expected and actual size |
|
| 257 | + if ($expected !== null |
|
| 258 | + && $expected !== $count |
|
| 259 | + && $this->request->getMethod() === 'PUT' |
|
| 260 | + ) { |
|
| 261 | + throw new BadRequest( |
|
| 262 | + $this->l10n->t( |
|
| 263 | + 'Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side.', |
|
| 264 | + [ |
|
| 265 | + $this->l10n->n('%n byte', '%n bytes', $expected), |
|
| 266 | + $this->l10n->n('%n byte', '%n bytes', $count), |
|
| 267 | + ], |
|
| 268 | + ) |
|
| 269 | + ); |
|
| 270 | + } |
|
| 271 | + } catch (\Exception $e) { |
|
| 272 | + if ($e instanceof LockedException) { |
|
| 273 | + Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]); |
|
| 274 | + } else { |
|
| 275 | + Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + if ($needsPartFile) { |
|
| 279 | + $partStorage->unlink($internalPartPath); |
|
| 280 | + } |
|
| 281 | + $this->convertToSabreException($e); |
|
| 282 | + } |
|
| 283 | + |
|
| 284 | + try { |
|
| 285 | + if ($needsPartFile) { |
|
| 286 | + if ($view && !$this->emitPreHooks($exists)) { |
|
| 287 | + $partStorage->unlink($internalPartPath); |
|
| 288 | + throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook')); |
|
| 289 | + } |
|
| 290 | + try { |
|
| 291 | + $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 292 | + } catch (LockedException $e) { |
|
| 293 | + // during very large uploads, the shared lock we got at the start might have been expired |
|
| 294 | + // meaning that the above lock can fail not just only because somebody else got a shared lock |
|
| 295 | + // or because there is no existing shared lock to make exclusive |
|
| 296 | + // |
|
| 297 | + // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
|
| 298 | + // lock this will still fail, if our original shared lock expired the new lock will be successful and |
|
| 299 | + // the entire operation will be safe |
|
| 300 | + |
|
| 301 | + try { |
|
| 302 | + $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
|
| 303 | + } catch (LockedException $ex) { |
|
| 304 | + if ($needsPartFile) { |
|
| 305 | + $partStorage->unlink($internalPartPath); |
|
| 306 | + } |
|
| 307 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 308 | + } |
|
| 309 | + } |
|
| 310 | + |
|
| 311 | + // rename to correct path |
|
| 312 | + try { |
|
| 313 | + $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
|
| 314 | + $fileExists = $storage->file_exists($internalPath); |
|
| 315 | + if ($renameOkay === false || $fileExists === false) { |
|
| 316 | + Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
|
| 317 | + throw new Exception($this->l10n->t('Could not rename part file to final file')); |
|
| 318 | + } |
|
| 319 | + } catch (ForbiddenException $ex) { |
|
| 320 | + if (!$ex->getRetry()) { |
|
| 321 | + $partStorage->unlink($internalPartPath); |
|
| 322 | + } |
|
| 323 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 324 | + } catch (\Exception $e) { |
|
| 325 | + $partStorage->unlink($internalPartPath); |
|
| 326 | + $this->convertToSabreException($e); |
|
| 327 | + } |
|
| 328 | + } |
|
| 329 | + |
|
| 330 | + // since we skipped the view we need to scan and emit the hooks ourselves |
|
| 331 | + $storage->getUpdater()->update($internalPath); |
|
| 332 | + |
|
| 333 | + try { |
|
| 334 | + $this->changeLock(ILockingProvider::LOCK_SHARED); |
|
| 335 | + } catch (LockedException $e) { |
|
| 336 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 337 | + } |
|
| 338 | + |
|
| 339 | + // allow sync clients to send the mtime along in a header |
|
| 340 | + $mtimeHeader = $this->request->getHeader('x-oc-mtime'); |
|
| 341 | + if ($mtimeHeader !== '') { |
|
| 342 | + $mtime = $this->sanitizeMtime($mtimeHeader); |
|
| 343 | + if ($this->fileView->touch($this->path, $mtime)) { |
|
| 344 | + $this->header('X-OC-MTime: accepted'); |
|
| 345 | + } |
|
| 346 | + } |
|
| 347 | + |
|
| 348 | + $fileInfoUpdate = [ |
|
| 349 | + 'upload_time' => time() |
|
| 350 | + ]; |
|
| 351 | + |
|
| 352 | + // allow sync clients to send the creation time along in a header |
|
| 353 | + $ctimeHeader = $this->request->getHeader('x-oc-ctime'); |
|
| 354 | + if ($ctimeHeader) { |
|
| 355 | + $ctime = $this->sanitizeMtime($ctimeHeader); |
|
| 356 | + $fileInfoUpdate['creation_time'] = $ctime; |
|
| 357 | + $this->header('X-OC-CTime: accepted'); |
|
| 358 | + } |
|
| 359 | + |
|
| 360 | + $this->fileView->putFileInfo($this->path, $fileInfoUpdate); |
|
| 361 | + |
|
| 362 | + if ($view) { |
|
| 363 | + $this->emitPostHooks($exists); |
|
| 364 | + } |
|
| 365 | + |
|
| 366 | + $this->refreshInfo(); |
|
| 367 | + |
|
| 368 | + $checksumHeader = $this->request->getHeader('oc-checksum'); |
|
| 369 | + if ($checksumHeader) { |
|
| 370 | + $checksum = trim($checksumHeader); |
|
| 371 | + $this->setChecksum($checksum); |
|
| 372 | + } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') { |
|
| 373 | + $this->setChecksum(''); |
|
| 374 | + } |
|
| 375 | + } catch (StorageNotAvailableException $e) { |
|
| 376 | + throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e); |
|
| 377 | + } |
|
| 378 | + |
|
| 379 | + return '"' . $this->info->getEtag() . '"'; |
|
| 380 | + } |
|
| 381 | + |
|
| 382 | + private function getPartFileBasePath($path) { |
|
| 383 | + $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true); |
|
| 384 | + if ($partFileInStorage) { |
|
| 385 | + $filename = basename($path); |
|
| 386 | + // hash does not need to be secure but fast and semi unique |
|
| 387 | + $hashedFilename = hash('xxh128', $filename); |
|
| 388 | + return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename; |
|
| 389 | + } else { |
|
| 390 | + // will place the .part file in the users root directory |
|
| 391 | + // therefor we need to make the name (semi) unique - hash does not need to be secure but fast. |
|
| 392 | + return hash('xxh128', $path); |
|
| 393 | + } |
|
| 394 | + } |
|
| 395 | + |
|
| 396 | + private function emitPreHooks(bool $exists, ?string $path = null): bool { |
|
| 397 | + if (is_null($path)) { |
|
| 398 | + $path = $this->path; |
|
| 399 | + } |
|
| 400 | + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
| 401 | + if ($hookPath === null) { |
|
| 402 | + // We only trigger hooks from inside default view |
|
| 403 | + return true; |
|
| 404 | + } |
|
| 405 | + $run = true; |
|
| 406 | + |
|
| 407 | + if (!$exists) { |
|
| 408 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [ |
|
| 409 | + Filesystem::signal_param_path => $hookPath, |
|
| 410 | + Filesystem::signal_param_run => &$run, |
|
| 411 | + ]); |
|
| 412 | + } else { |
|
| 413 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [ |
|
| 414 | + Filesystem::signal_param_path => $hookPath, |
|
| 415 | + Filesystem::signal_param_run => &$run, |
|
| 416 | + ]); |
|
| 417 | + } |
|
| 418 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [ |
|
| 419 | + Filesystem::signal_param_path => $hookPath, |
|
| 420 | + Filesystem::signal_param_run => &$run, |
|
| 421 | + ]); |
|
| 422 | + return $run; |
|
| 423 | + } |
|
| 424 | + |
|
| 425 | + private function emitPostHooks(bool $exists, ?string $path = null): void { |
|
| 426 | + if (is_null($path)) { |
|
| 427 | + $path = $this->path; |
|
| 428 | + } |
|
| 429 | + $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
|
| 430 | + if ($hookPath === null) { |
|
| 431 | + // We only trigger hooks from inside default view |
|
| 432 | + return; |
|
| 433 | + } |
|
| 434 | + if (!$exists) { |
|
| 435 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [ |
|
| 436 | + Filesystem::signal_param_path => $hookPath |
|
| 437 | + ]); |
|
| 438 | + } else { |
|
| 439 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [ |
|
| 440 | + Filesystem::signal_param_path => $hookPath |
|
| 441 | + ]); |
|
| 442 | + } |
|
| 443 | + \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [ |
|
| 444 | + Filesystem::signal_param_path => $hookPath |
|
| 445 | + ]); |
|
| 446 | + } |
|
| 447 | + |
|
| 448 | + /** |
|
| 449 | + * Returns the data |
|
| 450 | + * |
|
| 451 | + * @return resource |
|
| 452 | + * @throws Forbidden |
|
| 453 | + * @throws ServiceUnavailable |
|
| 454 | + */ |
|
| 455 | + public function get() { |
|
| 456 | + //throw exception if encryption is disabled but files are still encrypted |
|
| 457 | + try { |
|
| 458 | + if (!$this->info->isReadable()) { |
|
| 459 | + // do a if the file did not exist |
|
| 460 | + throw new NotFound(); |
|
| 461 | + } |
|
| 462 | + $path = ltrim($this->path, '/'); |
|
| 463 | + try { |
|
| 464 | + $res = $this->fileView->fopen($path, 'rb'); |
|
| 465 | + } catch (\Exception $e) { |
|
| 466 | + $this->convertToSabreException($e); |
|
| 467 | + } |
|
| 468 | + |
|
| 469 | + if ($res === false) { |
|
| 470 | + if ($this->fileView->file_exists($path)) { |
|
| 471 | + throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path])); |
|
| 472 | + } else { |
|
| 473 | + throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path])); |
|
| 474 | + } |
|
| 475 | + } |
|
| 476 | + |
|
| 477 | + // comparing current file size with the one in DB |
|
| 478 | + // if different, fix DB and refresh cache. |
|
| 479 | + if ($this->getSize() !== $this->fileView->filesize($this->getPath())) { |
|
| 480 | + $logger = Server::get(LoggerInterface::class); |
|
| 481 | + $logger->warning('fixing cached size of file id=' . $this->getId()); |
|
| 482 | + |
|
| 483 | + $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath()); |
|
| 484 | + $this->refreshInfo(); |
|
| 485 | + } |
|
| 486 | + |
|
| 487 | + return $res; |
|
| 488 | + } catch (GenericEncryptionException $e) { |
|
| 489 | + // returning 503 will allow retry of the operation at a later point in time |
|
| 490 | + throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()])); |
|
| 491 | + } catch (StorageNotAvailableException $e) { |
|
| 492 | + throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()])); |
|
| 493 | + } catch (ForbiddenException $ex) { |
|
| 494 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 495 | + } catch (LockedException $e) { |
|
| 496 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 497 | + } |
|
| 498 | + } |
|
| 499 | + |
|
| 500 | + /** |
|
| 501 | + * Delete the current file |
|
| 502 | + * |
|
| 503 | + * @throws Forbidden |
|
| 504 | + * @throws ServiceUnavailable |
|
| 505 | + */ |
|
| 506 | + public function delete() { |
|
| 507 | + if (!$this->info->isDeletable()) { |
|
| 508 | + throw new Forbidden(); |
|
| 509 | + } |
|
| 510 | + |
|
| 511 | + try { |
|
| 512 | + if (!$this->fileView->unlink($this->path)) { |
|
| 513 | + // assume it wasn't possible to delete due to permissions |
|
| 514 | + throw new Forbidden(); |
|
| 515 | + } |
|
| 516 | + } catch (StorageNotAvailableException $e) { |
|
| 517 | + throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()])); |
|
| 518 | + } catch (ForbiddenException $ex) { |
|
| 519 | + throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
|
| 520 | + } catch (LockedException $e) { |
|
| 521 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 522 | + } |
|
| 523 | + } |
|
| 524 | + |
|
| 525 | + /** |
|
| 526 | + * Returns the mime-type for a file |
|
| 527 | + * |
|
| 528 | + * If null is returned, we'll assume application/octet-stream |
|
| 529 | + * |
|
| 530 | + * @return string |
|
| 531 | + */ |
|
| 532 | + public function getContentType() { |
|
| 533 | + $mimeType = $this->info->getMimetype(); |
|
| 534 | + |
|
| 535 | + // PROPFIND needs to return the correct mime type, for consistency with the web UI |
|
| 536 | + if ($this->request->getMethod() === 'PROPFIND') { |
|
| 537 | + return $mimeType; |
|
| 538 | + } |
|
| 539 | + return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType); |
|
| 540 | + } |
|
| 541 | + |
|
| 542 | + /** |
|
| 543 | + * @throws NotFoundException |
|
| 544 | + * @throws NotPermittedException |
|
| 545 | + */ |
|
| 546 | + public function getDirectDownload(): array|false { |
|
| 547 | + if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) { |
|
| 548 | + return false; |
|
| 549 | + } |
|
| 550 | + $node = $this->getNode(); |
|
| 551 | + $storage = $node->getStorage(); |
|
| 552 | + if (!$storage) { |
|
| 553 | + return false; |
|
| 554 | + } |
|
| 555 | + |
|
| 556 | + if (!($node->getPermissions() & Constants::PERMISSION_READ)) { |
|
| 557 | + return false; |
|
| 558 | + } |
|
| 559 | + |
|
| 560 | + return $storage->getDirectDownloadById((string)$node->getId()); |
|
| 561 | + } |
|
| 562 | + |
|
| 563 | + /** |
|
| 564 | + * Convert the given exception to a SabreException instance |
|
| 565 | + * |
|
| 566 | + * @param \Exception $e |
|
| 567 | + * |
|
| 568 | + * @throws \Sabre\DAV\Exception |
|
| 569 | + */ |
|
| 570 | + private function convertToSabreException(\Exception $e) { |
|
| 571 | + if ($e instanceof \Sabre\DAV\Exception) { |
|
| 572 | + throw $e; |
|
| 573 | + } |
|
| 574 | + if ($e instanceof NotPermittedException) { |
|
| 575 | + // a more general case - due to whatever reason the content could not be written |
|
| 576 | + throw new Forbidden($e->getMessage(), 0, $e); |
|
| 577 | + } |
|
| 578 | + if ($e instanceof ForbiddenException) { |
|
| 579 | + // the path for the file was forbidden |
|
| 580 | + throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
|
| 581 | + } |
|
| 582 | + if ($e instanceof EntityTooLargeException) { |
|
| 583 | + // the file is too big to be stored |
|
| 584 | + throw new EntityTooLarge($e->getMessage(), 0, $e); |
|
| 585 | + } |
|
| 586 | + if ($e instanceof InvalidContentException) { |
|
| 587 | + // the file content is not permitted |
|
| 588 | + throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
|
| 589 | + } |
|
| 590 | + if ($e instanceof InvalidPathException) { |
|
| 591 | + // the path for the file was not valid |
|
| 592 | + // TODO: find proper http status code for this case |
|
| 593 | + throw new Forbidden($e->getMessage(), 0, $e); |
|
| 594 | + } |
|
| 595 | + if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
|
| 596 | + // the file is currently being written to by another process |
|
| 597 | + throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
|
| 598 | + } |
|
| 599 | + if ($e instanceof GenericEncryptionException) { |
|
| 600 | + // returning 503 will allow retry of the operation at a later point in time |
|
| 601 | + throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e); |
|
| 602 | + } |
|
| 603 | + if ($e instanceof StorageNotAvailableException) { |
|
| 604 | + throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e); |
|
| 605 | + } |
|
| 606 | + if ($e instanceof NotFoundException) { |
|
| 607 | + throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e); |
|
| 608 | + } |
|
| 609 | + |
|
| 610 | + throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
|
| 611 | + } |
|
| 612 | + |
|
| 613 | + /** |
|
| 614 | + * Get the checksum for this file |
|
| 615 | + * |
|
| 616 | + * @return string|null |
|
| 617 | + */ |
|
| 618 | + public function getChecksum() { |
|
| 619 | + return $this->info->getChecksum(); |
|
| 620 | + } |
|
| 621 | + |
|
| 622 | + public function setChecksum(string $checksum) { |
|
| 623 | + $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); |
|
| 624 | + $this->refreshInfo(); |
|
| 625 | + } |
|
| 626 | + |
|
| 627 | + protected function header($string) { |
|
| 628 | + if (!\OC::$CLI) { |
|
| 629 | + \header($string); |
|
| 630 | + } |
|
| 631 | + } |
|
| 632 | + |
|
| 633 | + public function hash(string $type) { |
|
| 634 | + return $this->fileView->hash($type, $this->path); |
|
| 635 | + } |
|
| 636 | + |
|
| 637 | + public function getNode(): \OCP\Files\File { |
|
| 638 | + return $this->node; |
|
| 639 | + } |
|
| 640 | 640 | } |
@@ -40,715 +40,715 @@ |
||
| 40 | 40 | use Sabre\HTTP\ResponseInterface; |
| 41 | 41 | |
| 42 | 42 | class FilesPlugin extends ServerPlugin { |
| 43 | - // namespace |
|
| 44 | - public const NS_OWNCLOUD = 'http://owncloud.org/ns'; |
|
| 45 | - public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; |
|
| 46 | - public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; |
|
| 47 | - public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; |
|
| 48 | - public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; |
|
| 49 | - public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions'; |
|
| 50 | - public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions'; |
|
| 51 | - public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes'; |
|
| 52 | - public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; |
|
| 53 | - public const DOWNLOADURL_EXPIRATION_PROPERTYNAME = '{http://nextcloud.org/ns}download-url-expiration'; |
|
| 54 | - public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; |
|
| 55 | - public const GETETAG_PROPERTYNAME = '{DAV:}getetag'; |
|
| 56 | - public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; |
|
| 57 | - public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate'; |
|
| 58 | - public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname'; |
|
| 59 | - public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; |
|
| 60 | - public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; |
|
| 61 | - public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; |
|
| 62 | - public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint'; |
|
| 63 | - public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview'; |
|
| 64 | - public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type'; |
|
| 65 | - public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root'; |
|
| 66 | - public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated'; |
|
| 67 | - public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag'; |
|
| 68 | - public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time'; |
|
| 69 | - public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time'; |
|
| 70 | - public const SHARE_NOTE = '{http://nextcloud.org/ns}note'; |
|
| 71 | - public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download'; |
|
| 72 | - public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count'; |
|
| 73 | - public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count'; |
|
| 74 | - public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-'; |
|
| 75 | - public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden'; |
|
| 76 | - |
|
| 77 | - /** Reference to main server object */ |
|
| 78 | - private ?Server $server = null; |
|
| 79 | - |
|
| 80 | - /** |
|
| 81 | - * @param Tree $tree |
|
| 82 | - * @param IConfig $config |
|
| 83 | - * @param IRequest $request |
|
| 84 | - * @param IPreview $previewManager |
|
| 85 | - * @param IUserSession $userSession |
|
| 86 | - * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off. |
|
| 87 | - * @param bool $downloadAttachment |
|
| 88 | - * @return void |
|
| 89 | - */ |
|
| 90 | - public function __construct( |
|
| 91 | - private Tree $tree, |
|
| 92 | - private IConfig $config, |
|
| 93 | - private IRequest $request, |
|
| 94 | - private IPreview $previewManager, |
|
| 95 | - private IUserSession $userSession, |
|
| 96 | - private IFilenameValidator $validator, |
|
| 97 | - private IAccountManager $accountManager, |
|
| 98 | - private bool $isPublic = false, |
|
| 99 | - private bool $downloadAttachment = true, |
|
| 100 | - ) { |
|
| 101 | - } |
|
| 102 | - |
|
| 103 | - /** |
|
| 104 | - * This initializes the plugin. |
|
| 105 | - * |
|
| 106 | - * This function is called by \Sabre\DAV\Server, after |
|
| 107 | - * addPlugin is called. |
|
| 108 | - * |
|
| 109 | - * This method should set up the required event subscriptions. |
|
| 110 | - * |
|
| 111 | - * @return void |
|
| 112 | - */ |
|
| 113 | - public function initialize(Server $server) { |
|
| 114 | - $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; |
|
| 115 | - $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc'; |
|
| 116 | - $server->protectedProperties[] = self::FILEID_PROPERTYNAME; |
|
| 117 | - $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; |
|
| 118 | - $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; |
|
| 119 | - $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; |
|
| 120 | - $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME; |
|
| 121 | - $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME; |
|
| 122 | - $server->protectedProperties[] = self::SIZE_PROPERTYNAME; |
|
| 123 | - $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; |
|
| 124 | - $server->protectedProperties[] = self::DOWNLOADURL_EXPIRATION_PROPERTYNAME; |
|
| 125 | - $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; |
|
| 126 | - $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; |
|
| 127 | - $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; |
|
| 128 | - $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME; |
|
| 129 | - $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME; |
|
| 130 | - $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME; |
|
| 131 | - $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME; |
|
| 132 | - $server->protectedProperties[] = self::SHARE_NOTE; |
|
| 133 | - |
|
| 134 | - // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH |
|
| 135 | - $allowedProperties = ['{DAV:}getetag']; |
|
| 136 | - $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties); |
|
| 137 | - |
|
| 138 | - $this->server = $server; |
|
| 139 | - $this->server->on('propFind', [$this, 'handleGetProperties']); |
|
| 140 | - $this->server->on('propPatch', [$this, 'handleUpdateProperties']); |
|
| 141 | - $this->server->on('afterBind', [$this, 'sendFileIdHeader']); |
|
| 142 | - $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); |
|
| 143 | - $this->server->on('afterMethod:GET', [$this,'httpGet']); |
|
| 144 | - $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); |
|
| 145 | - $this->server->on('afterResponse', function ($request, ResponseInterface $response): void { |
|
| 146 | - $body = $response->getBody(); |
|
| 147 | - if (is_resource($body)) { |
|
| 148 | - fclose($body); |
|
| 149 | - } |
|
| 150 | - }); |
|
| 151 | - $this->server->on('beforeMove', [$this, 'checkMove']); |
|
| 152 | - $this->server->on('beforeCopy', [$this, 'checkCopy']); |
|
| 153 | - } |
|
| 154 | - |
|
| 155 | - /** |
|
| 156 | - * Plugin that checks if a copy can actually be performed. |
|
| 157 | - * |
|
| 158 | - * @param string $source source path |
|
| 159 | - * @param string $target target path |
|
| 160 | - * @throws NotFound If the source does not exist |
|
| 161 | - * @throws InvalidPath If the target is invalid |
|
| 162 | - */ |
|
| 163 | - public function checkCopy($source, $target): void { |
|
| 164 | - $sourceNode = $this->tree->getNodeForPath($source); |
|
| 165 | - if (!$sourceNode instanceof Node) { |
|
| 166 | - return; |
|
| 167 | - } |
|
| 168 | - |
|
| 169 | - // Ensure source exists |
|
| 170 | - $sourceNodeFileInfo = $sourceNode->getFileInfo(); |
|
| 171 | - if ($sourceNodeFileInfo === null) { |
|
| 172 | - throw new NotFound($source . ' does not exist'); |
|
| 173 | - } |
|
| 174 | - // Ensure the target name is valid |
|
| 175 | - try { |
|
| 176 | - [$targetPath, $targetName] = \Sabre\Uri\split($target); |
|
| 177 | - $this->validator->validateFilename($targetName); |
|
| 178 | - } catch (InvalidPathException $e) { |
|
| 179 | - throw new InvalidPath($e->getMessage(), false); |
|
| 180 | - } |
|
| 181 | - // Ensure the target path is valid |
|
| 182 | - $segments = array_slice(explode('/', $targetPath), 2); |
|
| 183 | - foreach ($segments as $segment) { |
|
| 184 | - if ($this->validator->isFilenameValid($segment) === false) { |
|
| 185 | - $l = \OCP\Server::get(IFactory::class)->get('dav'); |
|
| 186 | - throw new InvalidPath($l->t('Invalid target path')); |
|
| 187 | - } |
|
| 188 | - } |
|
| 189 | - } |
|
| 190 | - |
|
| 191 | - /** |
|
| 192 | - * Plugin that checks if a move can actually be performed. |
|
| 193 | - * |
|
| 194 | - * @param string $source source path |
|
| 195 | - * @param string $target target path |
|
| 196 | - * @throws Forbidden If the source is not deletable |
|
| 197 | - * @throws NotFound If the source does not exist |
|
| 198 | - * @throws InvalidPath If the target name is invalid |
|
| 199 | - */ |
|
| 200 | - public function checkMove(string $source, string $target): void { |
|
| 201 | - $sourceNode = $this->tree->getNodeForPath($source); |
|
| 202 | - if (!$sourceNode instanceof Node) { |
|
| 203 | - return; |
|
| 204 | - } |
|
| 205 | - |
|
| 206 | - // First check copyable (move only needs additional delete permission) |
|
| 207 | - $this->checkCopy($source, $target); |
|
| 208 | - |
|
| 209 | - // The source needs to be deletable for moving |
|
| 210 | - $sourceNodeFileInfo = $sourceNode->getFileInfo(); |
|
| 211 | - if (!$sourceNodeFileInfo->isDeletable()) { |
|
| 212 | - throw new Forbidden($source . ' cannot be deleted'); |
|
| 213 | - } |
|
| 214 | - |
|
| 215 | - // The source is not allowed to be the parent of the target |
|
| 216 | - if (str_starts_with($source, $target . '/')) { |
|
| 217 | - throw new Forbidden($source . ' cannot be moved to it\'s parent'); |
|
| 218 | - } |
|
| 219 | - } |
|
| 220 | - |
|
| 221 | - /** |
|
| 222 | - * This sets a cookie to be able to recognize the start of the download |
|
| 223 | - * the content must not be longer than 32 characters and must only contain |
|
| 224 | - * alphanumeric characters |
|
| 225 | - * |
|
| 226 | - * @param RequestInterface $request |
|
| 227 | - * @param ResponseInterface $response |
|
| 228 | - */ |
|
| 229 | - public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { |
|
| 230 | - $queryParams = $request->getQueryParameters(); |
|
| 231 | - |
|
| 232 | - /** |
|
| 233 | - * this sets a cookie to be able to recognize the start of the download |
|
| 234 | - * the content must not be longer than 32 characters and must only contain |
|
| 235 | - * alphanumeric characters |
|
| 236 | - */ |
|
| 237 | - if (isset($queryParams['downloadStartSecret'])) { |
|
| 238 | - $token = $queryParams['downloadStartSecret']; |
|
| 239 | - if (!isset($token[32]) |
|
| 240 | - && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { |
|
| 241 | - // FIXME: use $response->setHeader() instead |
|
| 242 | - setcookie('ocDownloadStarted', $token, time() + 20, '/'); |
|
| 243 | - } |
|
| 244 | - } |
|
| 245 | - } |
|
| 246 | - |
|
| 247 | - /** |
|
| 248 | - * Add headers to file download |
|
| 249 | - * |
|
| 250 | - * @param RequestInterface $request |
|
| 251 | - * @param ResponseInterface $response |
|
| 252 | - */ |
|
| 253 | - public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 254 | - // Only handle valid files |
|
| 255 | - $node = $this->tree->getNodeForPath($request->getPath()); |
|
| 256 | - if (!($node instanceof IFile)) { |
|
| 257 | - return; |
|
| 258 | - } |
|
| 259 | - |
|
| 260 | - // adds a 'Content-Disposition: attachment' header in case no disposition |
|
| 261 | - // header has been set before |
|
| 262 | - if ($this->downloadAttachment |
|
| 263 | - && $response->getHeader('Content-Disposition') === null) { |
|
| 264 | - $filename = $node->getName(); |
|
| 265 | - if ($this->request->isUserAgent( |
|
| 266 | - [ |
|
| 267 | - Request::USER_AGENT_IE, |
|
| 268 | - Request::USER_AGENT_ANDROID_MOBILE_CHROME, |
|
| 269 | - Request::USER_AGENT_FREEBOX, |
|
| 270 | - ])) { |
|
| 271 | - $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"'); |
|
| 272 | - } else { |
|
| 273 | - $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename) |
|
| 274 | - . '; filename="' . rawurlencode($filename) . '"'); |
|
| 275 | - } |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - if ($node instanceof File) { |
|
| 279 | - //Add OC-Checksum header |
|
| 280 | - $checksum = $node->getChecksum(); |
|
| 281 | - if ($checksum !== null && $checksum !== '') { |
|
| 282 | - $response->addHeader('OC-Checksum', $checksum); |
|
| 283 | - } |
|
| 284 | - } |
|
| 285 | - $response->addHeader('X-Accel-Buffering', 'no'); |
|
| 286 | - } |
|
| 287 | - |
|
| 288 | - /** |
|
| 289 | - * Adds all ownCloud-specific properties |
|
| 290 | - * |
|
| 291 | - * @param PropFind $propFind |
|
| 292 | - * @param \Sabre\DAV\INode $node |
|
| 293 | - * @return void |
|
| 294 | - */ |
|
| 295 | - public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { |
|
| 296 | - $httpRequest = $this->server->httpRequest; |
|
| 297 | - |
|
| 298 | - if ($node instanceof Node) { |
|
| 299 | - /** |
|
| 300 | - * This was disabled, because it made dir listing throw an exception, |
|
| 301 | - * so users were unable to navigate into folders where one subitem |
|
| 302 | - * is blocked by the files_accesscontrol app, see: |
|
| 303 | - * https://github.com/nextcloud/files_accesscontrol/issues/65 |
|
| 304 | - * if (!$node->getFileInfo()->isReadable()) { |
|
| 305 | - * // avoid detecting files through this means |
|
| 306 | - * throw new NotFound(); |
|
| 307 | - * } |
|
| 308 | - */ |
|
| 309 | - |
|
| 310 | - $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) { |
|
| 311 | - return $node->getFileId(); |
|
| 312 | - }); |
|
| 313 | - |
|
| 314 | - $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) { |
|
| 315 | - return $node->getInternalFileId(); |
|
| 316 | - }); |
|
| 317 | - |
|
| 318 | - $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) { |
|
| 319 | - $perms = $node->getDavPermissions(); |
|
| 320 | - if ($this->isPublic) { |
|
| 321 | - // remove mount information |
|
| 322 | - $perms = str_replace(['S', 'M'], '', $perms); |
|
| 323 | - } |
|
| 324 | - return $perms; |
|
| 325 | - }); |
|
| 326 | - |
|
| 327 | - $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) { |
|
| 328 | - $user = $this->userSession->getUser(); |
|
| 329 | - if ($user === null) { |
|
| 330 | - return null; |
|
| 331 | - } |
|
| 332 | - return $node->getSharePermissions( |
|
| 333 | - $user->getUID() |
|
| 334 | - ); |
|
| 335 | - }); |
|
| 336 | - |
|
| 337 | - $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string { |
|
| 338 | - $user = $this->userSession->getUser(); |
|
| 339 | - if ($user === null) { |
|
| 340 | - return null; |
|
| 341 | - } |
|
| 342 | - $ncPermissions = $node->getSharePermissions( |
|
| 343 | - $user->getUID() |
|
| 344 | - ); |
|
| 345 | - $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions); |
|
| 346 | - return json_encode($ocmPermissions, JSON_THROW_ON_ERROR); |
|
| 347 | - }); |
|
| 348 | - |
|
| 349 | - $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) { |
|
| 350 | - return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR); |
|
| 351 | - }); |
|
| 352 | - |
|
| 353 | - $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string { |
|
| 354 | - return $node->getETag(); |
|
| 355 | - }); |
|
| 356 | - |
|
| 357 | - $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string { |
|
| 358 | - $owner = $node->getOwner(); |
|
| 359 | - if (!$owner) { |
|
| 360 | - return null; |
|
| 361 | - } else { |
|
| 362 | - return $owner->getUID(); |
|
| 363 | - } |
|
| 364 | - }); |
|
| 365 | - $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string { |
|
| 366 | - $owner = $node->getOwner(); |
|
| 367 | - if (!$owner) { |
|
| 368 | - return null; |
|
| 369 | - } |
|
| 370 | - |
|
| 371 | - // Get current user to see if we're in a public share or not |
|
| 372 | - $user = $this->userSession->getUser(); |
|
| 373 | - |
|
| 374 | - // If the user is logged in, we can return the display name |
|
| 375 | - if ($user !== null) { |
|
| 376 | - return $owner->getDisplayName(); |
|
| 377 | - } |
|
| 378 | - |
|
| 379 | - // Check if the user published their display name |
|
| 380 | - try { |
|
| 381 | - $ownerAccount = $this->accountManager->getAccount($owner); |
|
| 382 | - } catch (NoUserException) { |
|
| 383 | - // do not lock process if owner is not local |
|
| 384 | - return null; |
|
| 385 | - } |
|
| 386 | - |
|
| 387 | - $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); |
|
| 388 | - |
|
| 389 | - // Since we are not logged in, we need to have at least the published scope |
|
| 390 | - if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { |
|
| 391 | - return $owner->getDisplayName(); |
|
| 392 | - } |
|
| 393 | - |
|
| 394 | - return null; |
|
| 395 | - }); |
|
| 396 | - |
|
| 397 | - $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) { |
|
| 398 | - return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR); |
|
| 399 | - }); |
|
| 400 | - $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float { |
|
| 401 | - return $node->getSize(); |
|
| 402 | - }); |
|
| 403 | - $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) { |
|
| 404 | - return $node->getFileInfo()->getMountPoint()->getMountType(); |
|
| 405 | - }); |
|
| 406 | - |
|
| 407 | - /** |
|
| 408 | - * This is a special property which is used to determine if a node |
|
| 409 | - * is a mount root or not, e.g. a shared folder. |
|
| 410 | - * If so, then the node can only be unshared and not deleted. |
|
| 411 | - * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698 |
|
| 412 | - */ |
|
| 413 | - $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) { |
|
| 414 | - return $node->getNode()->getInternalPath() === '' ? 'true' : 'false'; |
|
| 415 | - }); |
|
| 416 | - |
|
| 417 | - $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string { |
|
| 418 | - $user = $this->userSession->getUser(); |
|
| 419 | - return $node->getNoteFromShare( |
|
| 420 | - $user?->getUID() |
|
| 421 | - ); |
|
| 422 | - }); |
|
| 423 | - |
|
| 424 | - $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) { |
|
| 425 | - $storage = $node->getNode()->getStorage(); |
|
| 426 | - if ($storage->instanceOfStorage(ISharedStorage::class)) { |
|
| 427 | - /** @var ISharedStorage $storage */ |
|
| 428 | - return match($storage->getShare()->getHideDownload()) { |
|
| 429 | - true => 'true', |
|
| 430 | - false => 'false', |
|
| 431 | - }; |
|
| 432 | - } else { |
|
| 433 | - return null; |
|
| 434 | - } |
|
| 435 | - }); |
|
| 436 | - |
|
| 437 | - $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () { |
|
| 438 | - return $this->config->getSystemValue('data-fingerprint', ''); |
|
| 439 | - }); |
|
| 440 | - $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) { |
|
| 441 | - return (new \DateTimeImmutable()) |
|
| 442 | - ->setTimestamp($node->getFileInfo()->getCreationTime()) |
|
| 443 | - ->format(\DateTimeInterface::ATOM); |
|
| 444 | - }); |
|
| 445 | - $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) { |
|
| 446 | - return $node->getFileInfo()->getCreationTime(); |
|
| 447 | - }); |
|
| 448 | - |
|
| 449 | - foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) { |
|
| 450 | - $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue); |
|
| 451 | - } |
|
| 452 | - |
|
| 453 | - $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) { |
|
| 454 | - $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']); |
|
| 455 | - $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime'; |
|
| 456 | - return ($isLivePhoto && $isMovFile) ? 'true' : 'false'; |
|
| 457 | - }); |
|
| 458 | - |
|
| 459 | - /** |
|
| 460 | - * Return file/folder name as displayname. The primary reason to |
|
| 461 | - * implement it this way is to avoid costly fallback to |
|
| 462 | - * CustomPropertiesBackend (esp. visible when querying all files |
|
| 463 | - * in a folder). |
|
| 464 | - */ |
|
| 465 | - $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) { |
|
| 466 | - return $node->getName(); |
|
| 467 | - }); |
|
| 468 | - |
|
| 469 | - $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) { |
|
| 470 | - return $node->getFileInfo()->getMountPoint() |
|
| 471 | - instanceof SharingExternalMount; |
|
| 472 | - }); |
|
| 473 | - } |
|
| 474 | - |
|
| 475 | - if ($node instanceof File) { |
|
| 476 | - $requestProperties = $propFind->getRequestedProperties(); |
|
| 477 | - |
|
| 478 | - if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true) |
|
| 479 | - || in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) { |
|
| 480 | - try { |
|
| 481 | - $directDownloadUrl = $node->getDirectDownload(); |
|
| 482 | - } catch (StorageNotAvailableException|ForbiddenException) { |
|
| 483 | - $directDownloadUrl = null; |
|
| 484 | - } |
|
| 485 | - |
|
| 486 | - $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) { |
|
| 487 | - if ($directDownloadUrl && isset($directDownloadUrl['url'])) { |
|
| 488 | - return $directDownloadUrl['url']; |
|
| 489 | - } |
|
| 490 | - return false; |
|
| 491 | - }); |
|
| 492 | - |
|
| 493 | - $propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) { |
|
| 494 | - if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) { |
|
| 495 | - return $directDownloadUrl['expiration']; |
|
| 496 | - } |
|
| 497 | - return false; |
|
| 498 | - }); |
|
| 499 | - } |
|
| 500 | - |
|
| 501 | - $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) { |
|
| 502 | - $checksum = $node->getChecksum(); |
|
| 503 | - if ($checksum === null || $checksum === '') { |
|
| 504 | - return null; |
|
| 505 | - } |
|
| 506 | - |
|
| 507 | - return new ChecksumList($checksum); |
|
| 508 | - }); |
|
| 509 | - |
|
| 510 | - $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) { |
|
| 511 | - return $node->getFileInfo()->getUploadTime(); |
|
| 512 | - }); |
|
| 513 | - } |
|
| 514 | - |
|
| 515 | - if ($node instanceof Directory) { |
|
| 516 | - $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) { |
|
| 517 | - return $node->getSize(); |
|
| 518 | - }); |
|
| 519 | - |
|
| 520 | - $requestProperties = $propFind->getRequestedProperties(); |
|
| 521 | - |
|
| 522 | - if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true) |
|
| 523 | - || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) { |
|
| 524 | - $nbFiles = 0; |
|
| 525 | - $nbFolders = 0; |
|
| 526 | - foreach ($node->getChildren() as $child) { |
|
| 527 | - if ($child instanceof File) { |
|
| 528 | - $nbFiles++; |
|
| 529 | - } elseif ($child instanceof Directory) { |
|
| 530 | - $nbFolders++; |
|
| 531 | - } |
|
| 532 | - } |
|
| 533 | - |
|
| 534 | - $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles); |
|
| 535 | - $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders); |
|
| 536 | - } |
|
| 537 | - } |
|
| 538 | - } |
|
| 539 | - |
|
| 540 | - /** |
|
| 541 | - * translate Nextcloud permissions to OCM Permissions |
|
| 542 | - * |
|
| 543 | - * @param $ncPermissions |
|
| 544 | - * @return array |
|
| 545 | - */ |
|
| 546 | - protected function ncPermissions2ocmPermissions($ncPermissions) { |
|
| 547 | - $ocmPermissions = []; |
|
| 548 | - |
|
| 549 | - if ($ncPermissions & Constants::PERMISSION_SHARE) { |
|
| 550 | - $ocmPermissions[] = 'share'; |
|
| 551 | - } |
|
| 552 | - |
|
| 553 | - if ($ncPermissions & Constants::PERMISSION_READ) { |
|
| 554 | - $ocmPermissions[] = 'read'; |
|
| 555 | - } |
|
| 556 | - |
|
| 557 | - if (($ncPermissions & Constants::PERMISSION_CREATE) |
|
| 558 | - || ($ncPermissions & Constants::PERMISSION_UPDATE)) { |
|
| 559 | - $ocmPermissions[] = 'write'; |
|
| 560 | - } |
|
| 561 | - |
|
| 562 | - return $ocmPermissions; |
|
| 563 | - } |
|
| 564 | - |
|
| 565 | - /** |
|
| 566 | - * Update ownCloud-specific properties |
|
| 567 | - * |
|
| 568 | - * @param string $path |
|
| 569 | - * @param PropPatch $propPatch |
|
| 570 | - * |
|
| 571 | - * @return void |
|
| 572 | - */ |
|
| 573 | - public function handleUpdateProperties($path, PropPatch $propPatch) { |
|
| 574 | - $node = $this->tree->getNodeForPath($path); |
|
| 575 | - if (!($node instanceof Node)) { |
|
| 576 | - return; |
|
| 577 | - } |
|
| 578 | - |
|
| 579 | - $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) { |
|
| 580 | - if (empty($time)) { |
|
| 581 | - return false; |
|
| 582 | - } |
|
| 583 | - $node->touch($time); |
|
| 584 | - return true; |
|
| 585 | - }); |
|
| 586 | - $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) { |
|
| 587 | - if (empty($etag)) { |
|
| 588 | - return false; |
|
| 589 | - } |
|
| 590 | - return $node->setEtag($etag) !== -1; |
|
| 591 | - }); |
|
| 592 | - $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) { |
|
| 593 | - if (empty($time)) { |
|
| 594 | - return false; |
|
| 595 | - } |
|
| 596 | - $dateTime = new \DateTimeImmutable($time); |
|
| 597 | - $node->setCreationTime($dateTime->getTimestamp()); |
|
| 598 | - return true; |
|
| 599 | - }); |
|
| 600 | - $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) { |
|
| 601 | - if (empty($time)) { |
|
| 602 | - return false; |
|
| 603 | - } |
|
| 604 | - $node->setCreationTime((int)$time); |
|
| 605 | - return true; |
|
| 606 | - }); |
|
| 607 | - |
|
| 608 | - $this->handleUpdatePropertiesMetadata($propPatch, $node); |
|
| 609 | - |
|
| 610 | - /** |
|
| 611 | - * Disable modification of the displayname property for files and |
|
| 612 | - * folders via PROPPATCH. See PROPFIND for more information. |
|
| 613 | - */ |
|
| 614 | - $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) { |
|
| 615 | - return 403; |
|
| 616 | - }); |
|
| 617 | - } |
|
| 618 | - |
|
| 619 | - |
|
| 620 | - /** |
|
| 621 | - * handle the update of metadata from PROPPATCH requests |
|
| 622 | - * |
|
| 623 | - * @param PropPatch $propPatch |
|
| 624 | - * @param Node $node |
|
| 625 | - * |
|
| 626 | - * @throws FilesMetadataException |
|
| 627 | - */ |
|
| 628 | - private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void { |
|
| 629 | - $userId = $this->userSession->getUser()?->getUID(); |
|
| 630 | - if ($userId === null) { |
|
| 631 | - return; |
|
| 632 | - } |
|
| 633 | - |
|
| 634 | - $accessRight = $this->getMetadataFileAccessRight($node, $userId); |
|
| 635 | - $filesMetadataManager = $this->initFilesMetadataManager(); |
|
| 636 | - $knownMetadata = $filesMetadataManager->getKnownMetadata(); |
|
| 637 | - |
|
| 638 | - foreach ($propPatch->getRemainingMutations() as $mutation) { |
|
| 639 | - if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) { |
|
| 640 | - continue; |
|
| 641 | - } |
|
| 642 | - |
|
| 643 | - $propPatch->handle( |
|
| 644 | - $mutation, |
|
| 645 | - function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool { |
|
| 646 | - /** @var FilesMetadata $metadata */ |
|
| 647 | - $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true); |
|
| 648 | - $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId()); |
|
| 649 | - $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX)); |
|
| 650 | - |
|
| 651 | - // confirm metadata key is editable via PROPPATCH |
|
| 652 | - if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) { |
|
| 653 | - throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node'); |
|
| 654 | - } |
|
| 655 | - |
|
| 656 | - if ($value === null) { |
|
| 657 | - $metadata->unset($metadataKey); |
|
| 658 | - $filesMetadataManager->saveMetadata($metadata); |
|
| 659 | - return true; |
|
| 660 | - } |
|
| 661 | - |
|
| 662 | - // If the metadata is unknown, it defaults to string. |
|
| 663 | - try { |
|
| 664 | - $type = $knownMetadata->getType($metadataKey); |
|
| 665 | - } catch (FilesMetadataNotFoundException) { |
|
| 666 | - $type = IMetadataValueWrapper::TYPE_STRING; |
|
| 667 | - } |
|
| 668 | - |
|
| 669 | - switch ($type) { |
|
| 670 | - case IMetadataValueWrapper::TYPE_STRING: |
|
| 671 | - $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 672 | - break; |
|
| 673 | - case IMetadataValueWrapper::TYPE_INT: |
|
| 674 | - $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 675 | - break; |
|
| 676 | - case IMetadataValueWrapper::TYPE_FLOAT: |
|
| 677 | - $metadata->setFloat($metadataKey, $value); |
|
| 678 | - break; |
|
| 679 | - case IMetadataValueWrapper::TYPE_BOOL: |
|
| 680 | - $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 681 | - break; |
|
| 682 | - case IMetadataValueWrapper::TYPE_ARRAY: |
|
| 683 | - $metadata->setArray($metadataKey, $value); |
|
| 684 | - break; |
|
| 685 | - case IMetadataValueWrapper::TYPE_STRING_LIST: |
|
| 686 | - $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 687 | - break; |
|
| 688 | - case IMetadataValueWrapper::TYPE_INT_LIST: |
|
| 689 | - $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 690 | - break; |
|
| 691 | - } |
|
| 692 | - |
|
| 693 | - $filesMetadataManager->saveMetadata($metadata); |
|
| 694 | - |
|
| 695 | - return true; |
|
| 696 | - } |
|
| 697 | - ); |
|
| 698 | - } |
|
| 699 | - } |
|
| 700 | - |
|
| 701 | - /** |
|
| 702 | - * init default internal metadata |
|
| 703 | - * |
|
| 704 | - * @return IFilesMetadataManager |
|
| 705 | - */ |
|
| 706 | - private function initFilesMetadataManager(): IFilesMetadataManager { |
|
| 707 | - /** @var IFilesMetadataManager $manager */ |
|
| 708 | - $manager = \OCP\Server::get(IFilesMetadataManager::class); |
|
| 709 | - $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION); |
|
| 710 | - |
|
| 711 | - return $manager; |
|
| 712 | - } |
|
| 713 | - |
|
| 714 | - /** |
|
| 715 | - * based on owner and shares, returns the bottom limit to update related metadata |
|
| 716 | - * |
|
| 717 | - * @param Node $node |
|
| 718 | - * @param string $userId |
|
| 719 | - * |
|
| 720 | - * @return int |
|
| 721 | - */ |
|
| 722 | - private function getMetadataFileAccessRight(Node $node, string $userId): int { |
|
| 723 | - if ($node->getOwner()?->getUID() === $userId) { |
|
| 724 | - return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP; |
|
| 725 | - } else { |
|
| 726 | - $filePermissions = $node->getSharePermissions($userId); |
|
| 727 | - if ($filePermissions & Constants::PERMISSION_UPDATE) { |
|
| 728 | - return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION; |
|
| 729 | - } |
|
| 730 | - } |
|
| 731 | - |
|
| 732 | - return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION; |
|
| 733 | - } |
|
| 734 | - |
|
| 735 | - /** |
|
| 736 | - * @param string $filePath |
|
| 737 | - * @param ?\Sabre\DAV\INode $node |
|
| 738 | - * @return void |
|
| 739 | - * @throws \Sabre\DAV\Exception\BadRequest |
|
| 740 | - */ |
|
| 741 | - public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) { |
|
| 742 | - // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder |
|
| 743 | - try { |
|
| 744 | - $node = $this->server->tree->getNodeForPath($filePath); |
|
| 745 | - if ($node instanceof Node) { |
|
| 746 | - $fileId = $node->getFileId(); |
|
| 747 | - if (!is_null($fileId)) { |
|
| 748 | - $this->server->httpResponse->setHeader('OC-FileId', $fileId); |
|
| 749 | - } |
|
| 750 | - } |
|
| 751 | - } catch (NotFound) { |
|
| 752 | - } |
|
| 753 | - } |
|
| 43 | + // namespace |
|
| 44 | + public const NS_OWNCLOUD = 'http://owncloud.org/ns'; |
|
| 45 | + public const NS_NEXTCLOUD = 'http://nextcloud.org/ns'; |
|
| 46 | + public const FILEID_PROPERTYNAME = '{http://owncloud.org/ns}id'; |
|
| 47 | + public const INTERNAL_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}fileid'; |
|
| 48 | + public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; |
|
| 49 | + public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions'; |
|
| 50 | + public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions'; |
|
| 51 | + public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes'; |
|
| 52 | + public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; |
|
| 53 | + public const DOWNLOADURL_EXPIRATION_PROPERTYNAME = '{http://nextcloud.org/ns}download-url-expiration'; |
|
| 54 | + public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; |
|
| 55 | + public const GETETAG_PROPERTYNAME = '{DAV:}getetag'; |
|
| 56 | + public const LASTMODIFIED_PROPERTYNAME = '{DAV:}lastmodified'; |
|
| 57 | + public const CREATIONDATE_PROPERTYNAME = '{DAV:}creationdate'; |
|
| 58 | + public const DISPLAYNAME_PROPERTYNAME = '{DAV:}displayname'; |
|
| 59 | + public const OWNER_ID_PROPERTYNAME = '{http://owncloud.org/ns}owner-id'; |
|
| 60 | + public const OWNER_DISPLAY_NAME_PROPERTYNAME = '{http://owncloud.org/ns}owner-display-name'; |
|
| 61 | + public const CHECKSUMS_PROPERTYNAME = '{http://owncloud.org/ns}checksums'; |
|
| 62 | + public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint'; |
|
| 63 | + public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview'; |
|
| 64 | + public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type'; |
|
| 65 | + public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root'; |
|
| 66 | + public const IS_FEDERATED_PROPERTYNAME = '{http://nextcloud.org/ns}is-federated'; |
|
| 67 | + public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag'; |
|
| 68 | + public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time'; |
|
| 69 | + public const CREATION_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}creation_time'; |
|
| 70 | + public const SHARE_NOTE = '{http://nextcloud.org/ns}note'; |
|
| 71 | + public const SHARE_HIDE_DOWNLOAD_PROPERTYNAME = '{http://nextcloud.org/ns}hide-download'; |
|
| 72 | + public const SUBFOLDER_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-folder-count'; |
|
| 73 | + public const SUBFILE_COUNT_PROPERTYNAME = '{http://nextcloud.org/ns}contained-file-count'; |
|
| 74 | + public const FILE_METADATA_PREFIX = '{http://nextcloud.org/ns}metadata-'; |
|
| 75 | + public const HIDDEN_PROPERTYNAME = '{http://nextcloud.org/ns}hidden'; |
|
| 76 | + |
|
| 77 | + /** Reference to main server object */ |
|
| 78 | + private ?Server $server = null; |
|
| 79 | + |
|
| 80 | + /** |
|
| 81 | + * @param Tree $tree |
|
| 82 | + * @param IConfig $config |
|
| 83 | + * @param IRequest $request |
|
| 84 | + * @param IPreview $previewManager |
|
| 85 | + * @param IUserSession $userSession |
|
| 86 | + * @param bool $isPublic Whether this is public WebDAV. If true, some returned information will be stripped off. |
|
| 87 | + * @param bool $downloadAttachment |
|
| 88 | + * @return void |
|
| 89 | + */ |
|
| 90 | + public function __construct( |
|
| 91 | + private Tree $tree, |
|
| 92 | + private IConfig $config, |
|
| 93 | + private IRequest $request, |
|
| 94 | + private IPreview $previewManager, |
|
| 95 | + private IUserSession $userSession, |
|
| 96 | + private IFilenameValidator $validator, |
|
| 97 | + private IAccountManager $accountManager, |
|
| 98 | + private bool $isPublic = false, |
|
| 99 | + private bool $downloadAttachment = true, |
|
| 100 | + ) { |
|
| 101 | + } |
|
| 102 | + |
|
| 103 | + /** |
|
| 104 | + * This initializes the plugin. |
|
| 105 | + * |
|
| 106 | + * This function is called by \Sabre\DAV\Server, after |
|
| 107 | + * addPlugin is called. |
|
| 108 | + * |
|
| 109 | + * This method should set up the required event subscriptions. |
|
| 110 | + * |
|
| 111 | + * @return void |
|
| 112 | + */ |
|
| 113 | + public function initialize(Server $server) { |
|
| 114 | + $server->xml->namespaceMap[self::NS_OWNCLOUD] = 'oc'; |
|
| 115 | + $server->xml->namespaceMap[self::NS_NEXTCLOUD] = 'nc'; |
|
| 116 | + $server->protectedProperties[] = self::FILEID_PROPERTYNAME; |
|
| 117 | + $server->protectedProperties[] = self::INTERNAL_FILEID_PROPERTYNAME; |
|
| 118 | + $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; |
|
| 119 | + $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; |
|
| 120 | + $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME; |
|
| 121 | + $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME; |
|
| 122 | + $server->protectedProperties[] = self::SIZE_PROPERTYNAME; |
|
| 123 | + $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; |
|
| 124 | + $server->protectedProperties[] = self::DOWNLOADURL_EXPIRATION_PROPERTYNAME; |
|
| 125 | + $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; |
|
| 126 | + $server->protectedProperties[] = self::OWNER_DISPLAY_NAME_PROPERTYNAME; |
|
| 127 | + $server->protectedProperties[] = self::CHECKSUMS_PROPERTYNAME; |
|
| 128 | + $server->protectedProperties[] = self::DATA_FINGERPRINT_PROPERTYNAME; |
|
| 129 | + $server->protectedProperties[] = self::HAS_PREVIEW_PROPERTYNAME; |
|
| 130 | + $server->protectedProperties[] = self::MOUNT_TYPE_PROPERTYNAME; |
|
| 131 | + $server->protectedProperties[] = self::IS_FEDERATED_PROPERTYNAME; |
|
| 132 | + $server->protectedProperties[] = self::SHARE_NOTE; |
|
| 133 | + |
|
| 134 | + // normally these cannot be changed (RFC4918), but we want them modifiable through PROPPATCH |
|
| 135 | + $allowedProperties = ['{DAV:}getetag']; |
|
| 136 | + $server->protectedProperties = array_diff($server->protectedProperties, $allowedProperties); |
|
| 137 | + |
|
| 138 | + $this->server = $server; |
|
| 139 | + $this->server->on('propFind', [$this, 'handleGetProperties']); |
|
| 140 | + $this->server->on('propPatch', [$this, 'handleUpdateProperties']); |
|
| 141 | + $this->server->on('afterBind', [$this, 'sendFileIdHeader']); |
|
| 142 | + $this->server->on('afterWriteContent', [$this, 'sendFileIdHeader']); |
|
| 143 | + $this->server->on('afterMethod:GET', [$this,'httpGet']); |
|
| 144 | + $this->server->on('afterMethod:GET', [$this, 'handleDownloadToken']); |
|
| 145 | + $this->server->on('afterResponse', function ($request, ResponseInterface $response): void { |
|
| 146 | + $body = $response->getBody(); |
|
| 147 | + if (is_resource($body)) { |
|
| 148 | + fclose($body); |
|
| 149 | + } |
|
| 150 | + }); |
|
| 151 | + $this->server->on('beforeMove', [$this, 'checkMove']); |
|
| 152 | + $this->server->on('beforeCopy', [$this, 'checkCopy']); |
|
| 153 | + } |
|
| 154 | + |
|
| 155 | + /** |
|
| 156 | + * Plugin that checks if a copy can actually be performed. |
|
| 157 | + * |
|
| 158 | + * @param string $source source path |
|
| 159 | + * @param string $target target path |
|
| 160 | + * @throws NotFound If the source does not exist |
|
| 161 | + * @throws InvalidPath If the target is invalid |
|
| 162 | + */ |
|
| 163 | + public function checkCopy($source, $target): void { |
|
| 164 | + $sourceNode = $this->tree->getNodeForPath($source); |
|
| 165 | + if (!$sourceNode instanceof Node) { |
|
| 166 | + return; |
|
| 167 | + } |
|
| 168 | + |
|
| 169 | + // Ensure source exists |
|
| 170 | + $sourceNodeFileInfo = $sourceNode->getFileInfo(); |
|
| 171 | + if ($sourceNodeFileInfo === null) { |
|
| 172 | + throw new NotFound($source . ' does not exist'); |
|
| 173 | + } |
|
| 174 | + // Ensure the target name is valid |
|
| 175 | + try { |
|
| 176 | + [$targetPath, $targetName] = \Sabre\Uri\split($target); |
|
| 177 | + $this->validator->validateFilename($targetName); |
|
| 178 | + } catch (InvalidPathException $e) { |
|
| 179 | + throw new InvalidPath($e->getMessage(), false); |
|
| 180 | + } |
|
| 181 | + // Ensure the target path is valid |
|
| 182 | + $segments = array_slice(explode('/', $targetPath), 2); |
|
| 183 | + foreach ($segments as $segment) { |
|
| 184 | + if ($this->validator->isFilenameValid($segment) === false) { |
|
| 185 | + $l = \OCP\Server::get(IFactory::class)->get('dav'); |
|
| 186 | + throw new InvalidPath($l->t('Invalid target path')); |
|
| 187 | + } |
|
| 188 | + } |
|
| 189 | + } |
|
| 190 | + |
|
| 191 | + /** |
|
| 192 | + * Plugin that checks if a move can actually be performed. |
|
| 193 | + * |
|
| 194 | + * @param string $source source path |
|
| 195 | + * @param string $target target path |
|
| 196 | + * @throws Forbidden If the source is not deletable |
|
| 197 | + * @throws NotFound If the source does not exist |
|
| 198 | + * @throws InvalidPath If the target name is invalid |
|
| 199 | + */ |
|
| 200 | + public function checkMove(string $source, string $target): void { |
|
| 201 | + $sourceNode = $this->tree->getNodeForPath($source); |
|
| 202 | + if (!$sourceNode instanceof Node) { |
|
| 203 | + return; |
|
| 204 | + } |
|
| 205 | + |
|
| 206 | + // First check copyable (move only needs additional delete permission) |
|
| 207 | + $this->checkCopy($source, $target); |
|
| 208 | + |
|
| 209 | + // The source needs to be deletable for moving |
|
| 210 | + $sourceNodeFileInfo = $sourceNode->getFileInfo(); |
|
| 211 | + if (!$sourceNodeFileInfo->isDeletable()) { |
|
| 212 | + throw new Forbidden($source . ' cannot be deleted'); |
|
| 213 | + } |
|
| 214 | + |
|
| 215 | + // The source is not allowed to be the parent of the target |
|
| 216 | + if (str_starts_with($source, $target . '/')) { |
|
| 217 | + throw new Forbidden($source . ' cannot be moved to it\'s parent'); |
|
| 218 | + } |
|
| 219 | + } |
|
| 220 | + |
|
| 221 | + /** |
|
| 222 | + * This sets a cookie to be able to recognize the start of the download |
|
| 223 | + * the content must not be longer than 32 characters and must only contain |
|
| 224 | + * alphanumeric characters |
|
| 225 | + * |
|
| 226 | + * @param RequestInterface $request |
|
| 227 | + * @param ResponseInterface $response |
|
| 228 | + */ |
|
| 229 | + public function handleDownloadToken(RequestInterface $request, ResponseInterface $response) { |
|
| 230 | + $queryParams = $request->getQueryParameters(); |
|
| 231 | + |
|
| 232 | + /** |
|
| 233 | + * this sets a cookie to be able to recognize the start of the download |
|
| 234 | + * the content must not be longer than 32 characters and must only contain |
|
| 235 | + * alphanumeric characters |
|
| 236 | + */ |
|
| 237 | + if (isset($queryParams['downloadStartSecret'])) { |
|
| 238 | + $token = $queryParams['downloadStartSecret']; |
|
| 239 | + if (!isset($token[32]) |
|
| 240 | + && preg_match('!^[a-zA-Z0-9]+$!', $token) === 1) { |
|
| 241 | + // FIXME: use $response->setHeader() instead |
|
| 242 | + setcookie('ocDownloadStarted', $token, time() + 20, '/'); |
|
| 243 | + } |
|
| 244 | + } |
|
| 245 | + } |
|
| 246 | + |
|
| 247 | + /** |
|
| 248 | + * Add headers to file download |
|
| 249 | + * |
|
| 250 | + * @param RequestInterface $request |
|
| 251 | + * @param ResponseInterface $response |
|
| 252 | + */ |
|
| 253 | + public function httpGet(RequestInterface $request, ResponseInterface $response) { |
|
| 254 | + // Only handle valid files |
|
| 255 | + $node = $this->tree->getNodeForPath($request->getPath()); |
|
| 256 | + if (!($node instanceof IFile)) { |
|
| 257 | + return; |
|
| 258 | + } |
|
| 259 | + |
|
| 260 | + // adds a 'Content-Disposition: attachment' header in case no disposition |
|
| 261 | + // header has been set before |
|
| 262 | + if ($this->downloadAttachment |
|
| 263 | + && $response->getHeader('Content-Disposition') === null) { |
|
| 264 | + $filename = $node->getName(); |
|
| 265 | + if ($this->request->isUserAgent( |
|
| 266 | + [ |
|
| 267 | + Request::USER_AGENT_IE, |
|
| 268 | + Request::USER_AGENT_ANDROID_MOBILE_CHROME, |
|
| 269 | + Request::USER_AGENT_FREEBOX, |
|
| 270 | + ])) { |
|
| 271 | + $response->addHeader('Content-Disposition', 'attachment; filename="' . rawurlencode($filename) . '"'); |
|
| 272 | + } else { |
|
| 273 | + $response->addHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\'' . rawurlencode($filename) |
|
| 274 | + . '; filename="' . rawurlencode($filename) . '"'); |
|
| 275 | + } |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + if ($node instanceof File) { |
|
| 279 | + //Add OC-Checksum header |
|
| 280 | + $checksum = $node->getChecksum(); |
|
| 281 | + if ($checksum !== null && $checksum !== '') { |
|
| 282 | + $response->addHeader('OC-Checksum', $checksum); |
|
| 283 | + } |
|
| 284 | + } |
|
| 285 | + $response->addHeader('X-Accel-Buffering', 'no'); |
|
| 286 | + } |
|
| 287 | + |
|
| 288 | + /** |
|
| 289 | + * Adds all ownCloud-specific properties |
|
| 290 | + * |
|
| 291 | + * @param PropFind $propFind |
|
| 292 | + * @param \Sabre\DAV\INode $node |
|
| 293 | + * @return void |
|
| 294 | + */ |
|
| 295 | + public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node) { |
|
| 296 | + $httpRequest = $this->server->httpRequest; |
|
| 297 | + |
|
| 298 | + if ($node instanceof Node) { |
|
| 299 | + /** |
|
| 300 | + * This was disabled, because it made dir listing throw an exception, |
|
| 301 | + * so users were unable to navigate into folders where one subitem |
|
| 302 | + * is blocked by the files_accesscontrol app, see: |
|
| 303 | + * https://github.com/nextcloud/files_accesscontrol/issues/65 |
|
| 304 | + * if (!$node->getFileInfo()->isReadable()) { |
|
| 305 | + * // avoid detecting files through this means |
|
| 306 | + * throw new NotFound(); |
|
| 307 | + * } |
|
| 308 | + */ |
|
| 309 | + |
|
| 310 | + $propFind->handle(self::FILEID_PROPERTYNAME, function () use ($node) { |
|
| 311 | + return $node->getFileId(); |
|
| 312 | + }); |
|
| 313 | + |
|
| 314 | + $propFind->handle(self::INTERNAL_FILEID_PROPERTYNAME, function () use ($node) { |
|
| 315 | + return $node->getInternalFileId(); |
|
| 316 | + }); |
|
| 317 | + |
|
| 318 | + $propFind->handle(self::PERMISSIONS_PROPERTYNAME, function () use ($node) { |
|
| 319 | + $perms = $node->getDavPermissions(); |
|
| 320 | + if ($this->isPublic) { |
|
| 321 | + // remove mount information |
|
| 322 | + $perms = str_replace(['S', 'M'], '', $perms); |
|
| 323 | + } |
|
| 324 | + return $perms; |
|
| 325 | + }); |
|
| 326 | + |
|
| 327 | + $propFind->handle(self::SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest) { |
|
| 328 | + $user = $this->userSession->getUser(); |
|
| 329 | + if ($user === null) { |
|
| 330 | + return null; |
|
| 331 | + } |
|
| 332 | + return $node->getSharePermissions( |
|
| 333 | + $user->getUID() |
|
| 334 | + ); |
|
| 335 | + }); |
|
| 336 | + |
|
| 337 | + $propFind->handle(self::OCM_SHARE_PERMISSIONS_PROPERTYNAME, function () use ($node, $httpRequest): ?string { |
|
| 338 | + $user = $this->userSession->getUser(); |
|
| 339 | + if ($user === null) { |
|
| 340 | + return null; |
|
| 341 | + } |
|
| 342 | + $ncPermissions = $node->getSharePermissions( |
|
| 343 | + $user->getUID() |
|
| 344 | + ); |
|
| 345 | + $ocmPermissions = $this->ncPermissions2ocmPermissions($ncPermissions); |
|
| 346 | + return json_encode($ocmPermissions, JSON_THROW_ON_ERROR); |
|
| 347 | + }); |
|
| 348 | + |
|
| 349 | + $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) { |
|
| 350 | + return json_encode($node->getShareAttributes(), JSON_THROW_ON_ERROR); |
|
| 351 | + }); |
|
| 352 | + |
|
| 353 | + $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string { |
|
| 354 | + return $node->getETag(); |
|
| 355 | + }); |
|
| 356 | + |
|
| 357 | + $propFind->handle(self::OWNER_ID_PROPERTYNAME, function () use ($node): ?string { |
|
| 358 | + $owner = $node->getOwner(); |
|
| 359 | + if (!$owner) { |
|
| 360 | + return null; |
|
| 361 | + } else { |
|
| 362 | + return $owner->getUID(); |
|
| 363 | + } |
|
| 364 | + }); |
|
| 365 | + $propFind->handle(self::OWNER_DISPLAY_NAME_PROPERTYNAME, function () use ($node): ?string { |
|
| 366 | + $owner = $node->getOwner(); |
|
| 367 | + if (!$owner) { |
|
| 368 | + return null; |
|
| 369 | + } |
|
| 370 | + |
|
| 371 | + // Get current user to see if we're in a public share or not |
|
| 372 | + $user = $this->userSession->getUser(); |
|
| 373 | + |
|
| 374 | + // If the user is logged in, we can return the display name |
|
| 375 | + if ($user !== null) { |
|
| 376 | + return $owner->getDisplayName(); |
|
| 377 | + } |
|
| 378 | + |
|
| 379 | + // Check if the user published their display name |
|
| 380 | + try { |
|
| 381 | + $ownerAccount = $this->accountManager->getAccount($owner); |
|
| 382 | + } catch (NoUserException) { |
|
| 383 | + // do not lock process if owner is not local |
|
| 384 | + return null; |
|
| 385 | + } |
|
| 386 | + |
|
| 387 | + $ownerNameProperty = $ownerAccount->getProperty(IAccountManager::PROPERTY_DISPLAYNAME); |
|
| 388 | + |
|
| 389 | + // Since we are not logged in, we need to have at least the published scope |
|
| 390 | + if ($ownerNameProperty->getScope() === IAccountManager::SCOPE_PUBLISHED) { |
|
| 391 | + return $owner->getDisplayName(); |
|
| 392 | + } |
|
| 393 | + |
|
| 394 | + return null; |
|
| 395 | + }); |
|
| 396 | + |
|
| 397 | + $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) { |
|
| 398 | + return json_encode($this->previewManager->isAvailable($node->getFileInfo()), JSON_THROW_ON_ERROR); |
|
| 399 | + }); |
|
| 400 | + $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node): int|float { |
|
| 401 | + return $node->getSize(); |
|
| 402 | + }); |
|
| 403 | + $propFind->handle(self::MOUNT_TYPE_PROPERTYNAME, function () use ($node) { |
|
| 404 | + return $node->getFileInfo()->getMountPoint()->getMountType(); |
|
| 405 | + }); |
|
| 406 | + |
|
| 407 | + /** |
|
| 408 | + * This is a special property which is used to determine if a node |
|
| 409 | + * is a mount root or not, e.g. a shared folder. |
|
| 410 | + * If so, then the node can only be unshared and not deleted. |
|
| 411 | + * @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698 |
|
| 412 | + */ |
|
| 413 | + $propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) { |
|
| 414 | + return $node->getNode()->getInternalPath() === '' ? 'true' : 'false'; |
|
| 415 | + }); |
|
| 416 | + |
|
| 417 | + $propFind->handle(self::SHARE_NOTE, function () use ($node): ?string { |
|
| 418 | + $user = $this->userSession->getUser(); |
|
| 419 | + return $node->getNoteFromShare( |
|
| 420 | + $user?->getUID() |
|
| 421 | + ); |
|
| 422 | + }); |
|
| 423 | + |
|
| 424 | + $propFind->handle(self::SHARE_HIDE_DOWNLOAD_PROPERTYNAME, function () use ($node) { |
|
| 425 | + $storage = $node->getNode()->getStorage(); |
|
| 426 | + if ($storage->instanceOfStorage(ISharedStorage::class)) { |
|
| 427 | + /** @var ISharedStorage $storage */ |
|
| 428 | + return match($storage->getShare()->getHideDownload()) { |
|
| 429 | + true => 'true', |
|
| 430 | + false => 'false', |
|
| 431 | + }; |
|
| 432 | + } else { |
|
| 433 | + return null; |
|
| 434 | + } |
|
| 435 | + }); |
|
| 436 | + |
|
| 437 | + $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function () { |
|
| 438 | + return $this->config->getSystemValue('data-fingerprint', ''); |
|
| 439 | + }); |
|
| 440 | + $propFind->handle(self::CREATIONDATE_PROPERTYNAME, function () use ($node) { |
|
| 441 | + return (new \DateTimeImmutable()) |
|
| 442 | + ->setTimestamp($node->getFileInfo()->getCreationTime()) |
|
| 443 | + ->format(\DateTimeInterface::ATOM); |
|
| 444 | + }); |
|
| 445 | + $propFind->handle(self::CREATION_TIME_PROPERTYNAME, function () use ($node) { |
|
| 446 | + return $node->getFileInfo()->getCreationTime(); |
|
| 447 | + }); |
|
| 448 | + |
|
| 449 | + foreach ($node->getFileInfo()->getMetadata() as $metadataKey => $metadataValue) { |
|
| 450 | + $propFind->handle(self::FILE_METADATA_PREFIX . $metadataKey, $metadataValue); |
|
| 451 | + } |
|
| 452 | + |
|
| 453 | + $propFind->handle(self::HIDDEN_PROPERTYNAME, function () use ($node) { |
|
| 454 | + $isLivePhoto = isset($node->getFileInfo()->getMetadata()['files-live-photo']); |
|
| 455 | + $isMovFile = $node->getFileInfo()->getMimetype() === 'video/quicktime'; |
|
| 456 | + return ($isLivePhoto && $isMovFile) ? 'true' : 'false'; |
|
| 457 | + }); |
|
| 458 | + |
|
| 459 | + /** |
|
| 460 | + * Return file/folder name as displayname. The primary reason to |
|
| 461 | + * implement it this way is to avoid costly fallback to |
|
| 462 | + * CustomPropertiesBackend (esp. visible when querying all files |
|
| 463 | + * in a folder). |
|
| 464 | + */ |
|
| 465 | + $propFind->handle(self::DISPLAYNAME_PROPERTYNAME, function () use ($node) { |
|
| 466 | + return $node->getName(); |
|
| 467 | + }); |
|
| 468 | + |
|
| 469 | + $propFind->handle(self::IS_FEDERATED_PROPERTYNAME, function () use ($node) { |
|
| 470 | + return $node->getFileInfo()->getMountPoint() |
|
| 471 | + instanceof SharingExternalMount; |
|
| 472 | + }); |
|
| 473 | + } |
|
| 474 | + |
|
| 475 | + if ($node instanceof File) { |
|
| 476 | + $requestProperties = $propFind->getRequestedProperties(); |
|
| 477 | + |
|
| 478 | + if (in_array(self::DOWNLOADURL_PROPERTYNAME, $requestProperties, true) |
|
| 479 | + || in_array(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, $requestProperties, true)) { |
|
| 480 | + try { |
|
| 481 | + $directDownloadUrl = $node->getDirectDownload(); |
|
| 482 | + } catch (StorageNotAvailableException|ForbiddenException) { |
|
| 483 | + $directDownloadUrl = null; |
|
| 484 | + } |
|
| 485 | + |
|
| 486 | + $propFind->handle(self::DOWNLOADURL_PROPERTYNAME, function () use ($node, $directDownloadUrl) { |
|
| 487 | + if ($directDownloadUrl && isset($directDownloadUrl['url'])) { |
|
| 488 | + return $directDownloadUrl['url']; |
|
| 489 | + } |
|
| 490 | + return false; |
|
| 491 | + }); |
|
| 492 | + |
|
| 493 | + $propFind->handle(self::DOWNLOADURL_EXPIRATION_PROPERTYNAME, function () use ($node, $directDownloadUrl) { |
|
| 494 | + if ($directDownloadUrl && isset($directDownloadUrl['expiration'])) { |
|
| 495 | + return $directDownloadUrl['expiration']; |
|
| 496 | + } |
|
| 497 | + return false; |
|
| 498 | + }); |
|
| 499 | + } |
|
| 500 | + |
|
| 501 | + $propFind->handle(self::CHECKSUMS_PROPERTYNAME, function () use ($node) { |
|
| 502 | + $checksum = $node->getChecksum(); |
|
| 503 | + if ($checksum === null || $checksum === '') { |
|
| 504 | + return null; |
|
| 505 | + } |
|
| 506 | + |
|
| 507 | + return new ChecksumList($checksum); |
|
| 508 | + }); |
|
| 509 | + |
|
| 510 | + $propFind->handle(self::UPLOAD_TIME_PROPERTYNAME, function () use ($node) { |
|
| 511 | + return $node->getFileInfo()->getUploadTime(); |
|
| 512 | + }); |
|
| 513 | + } |
|
| 514 | + |
|
| 515 | + if ($node instanceof Directory) { |
|
| 516 | + $propFind->handle(self::SIZE_PROPERTYNAME, function () use ($node) { |
|
| 517 | + return $node->getSize(); |
|
| 518 | + }); |
|
| 519 | + |
|
| 520 | + $requestProperties = $propFind->getRequestedProperties(); |
|
| 521 | + |
|
| 522 | + if (in_array(self::SUBFILE_COUNT_PROPERTYNAME, $requestProperties, true) |
|
| 523 | + || in_array(self::SUBFOLDER_COUNT_PROPERTYNAME, $requestProperties, true)) { |
|
| 524 | + $nbFiles = 0; |
|
| 525 | + $nbFolders = 0; |
|
| 526 | + foreach ($node->getChildren() as $child) { |
|
| 527 | + if ($child instanceof File) { |
|
| 528 | + $nbFiles++; |
|
| 529 | + } elseif ($child instanceof Directory) { |
|
| 530 | + $nbFolders++; |
|
| 531 | + } |
|
| 532 | + } |
|
| 533 | + |
|
| 534 | + $propFind->handle(self::SUBFILE_COUNT_PROPERTYNAME, $nbFiles); |
|
| 535 | + $propFind->handle(self::SUBFOLDER_COUNT_PROPERTYNAME, $nbFolders); |
|
| 536 | + } |
|
| 537 | + } |
|
| 538 | + } |
|
| 539 | + |
|
| 540 | + /** |
|
| 541 | + * translate Nextcloud permissions to OCM Permissions |
|
| 542 | + * |
|
| 543 | + * @param $ncPermissions |
|
| 544 | + * @return array |
|
| 545 | + */ |
|
| 546 | + protected function ncPermissions2ocmPermissions($ncPermissions) { |
|
| 547 | + $ocmPermissions = []; |
|
| 548 | + |
|
| 549 | + if ($ncPermissions & Constants::PERMISSION_SHARE) { |
|
| 550 | + $ocmPermissions[] = 'share'; |
|
| 551 | + } |
|
| 552 | + |
|
| 553 | + if ($ncPermissions & Constants::PERMISSION_READ) { |
|
| 554 | + $ocmPermissions[] = 'read'; |
|
| 555 | + } |
|
| 556 | + |
|
| 557 | + if (($ncPermissions & Constants::PERMISSION_CREATE) |
|
| 558 | + || ($ncPermissions & Constants::PERMISSION_UPDATE)) { |
|
| 559 | + $ocmPermissions[] = 'write'; |
|
| 560 | + } |
|
| 561 | + |
|
| 562 | + return $ocmPermissions; |
|
| 563 | + } |
|
| 564 | + |
|
| 565 | + /** |
|
| 566 | + * Update ownCloud-specific properties |
|
| 567 | + * |
|
| 568 | + * @param string $path |
|
| 569 | + * @param PropPatch $propPatch |
|
| 570 | + * |
|
| 571 | + * @return void |
|
| 572 | + */ |
|
| 573 | + public function handleUpdateProperties($path, PropPatch $propPatch) { |
|
| 574 | + $node = $this->tree->getNodeForPath($path); |
|
| 575 | + if (!($node instanceof Node)) { |
|
| 576 | + return; |
|
| 577 | + } |
|
| 578 | + |
|
| 579 | + $propPatch->handle(self::LASTMODIFIED_PROPERTYNAME, function ($time) use ($node) { |
|
| 580 | + if (empty($time)) { |
|
| 581 | + return false; |
|
| 582 | + } |
|
| 583 | + $node->touch($time); |
|
| 584 | + return true; |
|
| 585 | + }); |
|
| 586 | + $propPatch->handle(self::GETETAG_PROPERTYNAME, function ($etag) use ($node) { |
|
| 587 | + if (empty($etag)) { |
|
| 588 | + return false; |
|
| 589 | + } |
|
| 590 | + return $node->setEtag($etag) !== -1; |
|
| 591 | + }); |
|
| 592 | + $propPatch->handle(self::CREATIONDATE_PROPERTYNAME, function ($time) use ($node) { |
|
| 593 | + if (empty($time)) { |
|
| 594 | + return false; |
|
| 595 | + } |
|
| 596 | + $dateTime = new \DateTimeImmutable($time); |
|
| 597 | + $node->setCreationTime($dateTime->getTimestamp()); |
|
| 598 | + return true; |
|
| 599 | + }); |
|
| 600 | + $propPatch->handle(self::CREATION_TIME_PROPERTYNAME, function ($time) use ($node) { |
|
| 601 | + if (empty($time)) { |
|
| 602 | + return false; |
|
| 603 | + } |
|
| 604 | + $node->setCreationTime((int)$time); |
|
| 605 | + return true; |
|
| 606 | + }); |
|
| 607 | + |
|
| 608 | + $this->handleUpdatePropertiesMetadata($propPatch, $node); |
|
| 609 | + |
|
| 610 | + /** |
|
| 611 | + * Disable modification of the displayname property for files and |
|
| 612 | + * folders via PROPPATCH. See PROPFIND for more information. |
|
| 613 | + */ |
|
| 614 | + $propPatch->handle(self::DISPLAYNAME_PROPERTYNAME, function ($displayName) { |
|
| 615 | + return 403; |
|
| 616 | + }); |
|
| 617 | + } |
|
| 618 | + |
|
| 619 | + |
|
| 620 | + /** |
|
| 621 | + * handle the update of metadata from PROPPATCH requests |
|
| 622 | + * |
|
| 623 | + * @param PropPatch $propPatch |
|
| 624 | + * @param Node $node |
|
| 625 | + * |
|
| 626 | + * @throws FilesMetadataException |
|
| 627 | + */ |
|
| 628 | + private function handleUpdatePropertiesMetadata(PropPatch $propPatch, Node $node): void { |
|
| 629 | + $userId = $this->userSession->getUser()?->getUID(); |
|
| 630 | + if ($userId === null) { |
|
| 631 | + return; |
|
| 632 | + } |
|
| 633 | + |
|
| 634 | + $accessRight = $this->getMetadataFileAccessRight($node, $userId); |
|
| 635 | + $filesMetadataManager = $this->initFilesMetadataManager(); |
|
| 636 | + $knownMetadata = $filesMetadataManager->getKnownMetadata(); |
|
| 637 | + |
|
| 638 | + foreach ($propPatch->getRemainingMutations() as $mutation) { |
|
| 639 | + if (!str_starts_with($mutation, self::FILE_METADATA_PREFIX)) { |
|
| 640 | + continue; |
|
| 641 | + } |
|
| 642 | + |
|
| 643 | + $propPatch->handle( |
|
| 644 | + $mutation, |
|
| 645 | + function (mixed $value) use ($accessRight, $knownMetadata, $node, $mutation, $filesMetadataManager): bool { |
|
| 646 | + /** @var FilesMetadata $metadata */ |
|
| 647 | + $metadata = $filesMetadataManager->getMetadata((int)$node->getFileId(), true); |
|
| 648 | + $metadata->setStorageId($node->getNode()->getStorage()->getCache()->getNumericStorageId()); |
|
| 649 | + $metadataKey = substr($mutation, strlen(self::FILE_METADATA_PREFIX)); |
|
| 650 | + |
|
| 651 | + // confirm metadata key is editable via PROPPATCH |
|
| 652 | + if ($knownMetadata->getEditPermission($metadataKey) < $accessRight) { |
|
| 653 | + throw new FilesMetadataException('you do not have enough rights to update \'' . $metadataKey . '\' on this node'); |
|
| 654 | + } |
|
| 655 | + |
|
| 656 | + if ($value === null) { |
|
| 657 | + $metadata->unset($metadataKey); |
|
| 658 | + $filesMetadataManager->saveMetadata($metadata); |
|
| 659 | + return true; |
|
| 660 | + } |
|
| 661 | + |
|
| 662 | + // If the metadata is unknown, it defaults to string. |
|
| 663 | + try { |
|
| 664 | + $type = $knownMetadata->getType($metadataKey); |
|
| 665 | + } catch (FilesMetadataNotFoundException) { |
|
| 666 | + $type = IMetadataValueWrapper::TYPE_STRING; |
|
| 667 | + } |
|
| 668 | + |
|
| 669 | + switch ($type) { |
|
| 670 | + case IMetadataValueWrapper::TYPE_STRING: |
|
| 671 | + $metadata->setString($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 672 | + break; |
|
| 673 | + case IMetadataValueWrapper::TYPE_INT: |
|
| 674 | + $metadata->setInt($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 675 | + break; |
|
| 676 | + case IMetadataValueWrapper::TYPE_FLOAT: |
|
| 677 | + $metadata->setFloat($metadataKey, $value); |
|
| 678 | + break; |
|
| 679 | + case IMetadataValueWrapper::TYPE_BOOL: |
|
| 680 | + $metadata->setBool($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 681 | + break; |
|
| 682 | + case IMetadataValueWrapper::TYPE_ARRAY: |
|
| 683 | + $metadata->setArray($metadataKey, $value); |
|
| 684 | + break; |
|
| 685 | + case IMetadataValueWrapper::TYPE_STRING_LIST: |
|
| 686 | + $metadata->setStringList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 687 | + break; |
|
| 688 | + case IMetadataValueWrapper::TYPE_INT_LIST: |
|
| 689 | + $metadata->setIntList($metadataKey, $value, $knownMetadata->isIndex($metadataKey)); |
|
| 690 | + break; |
|
| 691 | + } |
|
| 692 | + |
|
| 693 | + $filesMetadataManager->saveMetadata($metadata); |
|
| 694 | + |
|
| 695 | + return true; |
|
| 696 | + } |
|
| 697 | + ); |
|
| 698 | + } |
|
| 699 | + } |
|
| 700 | + |
|
| 701 | + /** |
|
| 702 | + * init default internal metadata |
|
| 703 | + * |
|
| 704 | + * @return IFilesMetadataManager |
|
| 705 | + */ |
|
| 706 | + private function initFilesMetadataManager(): IFilesMetadataManager { |
|
| 707 | + /** @var IFilesMetadataManager $manager */ |
|
| 708 | + $manager = \OCP\Server::get(IFilesMetadataManager::class); |
|
| 709 | + $manager->initMetadata('files-live-photo', IMetadataValueWrapper::TYPE_STRING, false, IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION); |
|
| 710 | + |
|
| 711 | + return $manager; |
|
| 712 | + } |
|
| 713 | + |
|
| 714 | + /** |
|
| 715 | + * based on owner and shares, returns the bottom limit to update related metadata |
|
| 716 | + * |
|
| 717 | + * @param Node $node |
|
| 718 | + * @param string $userId |
|
| 719 | + * |
|
| 720 | + * @return int |
|
| 721 | + */ |
|
| 722 | + private function getMetadataFileAccessRight(Node $node, string $userId): int { |
|
| 723 | + if ($node->getOwner()?->getUID() === $userId) { |
|
| 724 | + return IMetadataValueWrapper::EDIT_REQ_OWNERSHIP; |
|
| 725 | + } else { |
|
| 726 | + $filePermissions = $node->getSharePermissions($userId); |
|
| 727 | + if ($filePermissions & Constants::PERMISSION_UPDATE) { |
|
| 728 | + return IMetadataValueWrapper::EDIT_REQ_WRITE_PERMISSION; |
|
| 729 | + } |
|
| 730 | + } |
|
| 731 | + |
|
| 732 | + return IMetadataValueWrapper::EDIT_REQ_READ_PERMISSION; |
|
| 733 | + } |
|
| 734 | + |
|
| 735 | + /** |
|
| 736 | + * @param string $filePath |
|
| 737 | + * @param ?\Sabre\DAV\INode $node |
|
| 738 | + * @return void |
|
| 739 | + * @throws \Sabre\DAV\Exception\BadRequest |
|
| 740 | + */ |
|
| 741 | + public function sendFileIdHeader($filePath, ?\Sabre\DAV\INode $node = null) { |
|
| 742 | + // we get the node for the given $filePath here because in case of afterCreateFile $node is the parent folder |
|
| 743 | + try { |
|
| 744 | + $node = $this->server->tree->getNodeForPath($filePath); |
|
| 745 | + if ($node instanceof Node) { |
|
| 746 | + $fileId = $node->getFileId(); |
|
| 747 | + if (!is_null($fileId)) { |
|
| 748 | + $this->server->httpResponse->setHeader('OC-FileId', $fileId); |
|
| 749 | + } |
|
| 750 | + } |
|
| 751 | + } catch (NotFound) { |
|
| 752 | + } |
|
| 753 | + } |
|
| 754 | 754 | } |
@@ -11,37 +11,37 @@ |
||
| 11 | 11 | use OCP\Files\ObjectStore\IObjectStore; |
| 12 | 12 | |
| 13 | 13 | class FailWriteObjectStore implements IObjectStore { |
| 14 | - public function __construct( |
|
| 15 | - private IObjectStore $objectStore, |
|
| 16 | - ) { |
|
| 17 | - } |
|
| 18 | - |
|
| 19 | - public function getStorageId() { |
|
| 20 | - return $this->objectStore->getStorageId(); |
|
| 21 | - } |
|
| 22 | - |
|
| 23 | - public function readObject($urn) { |
|
| 24 | - return $this->objectStore->readObject($urn); |
|
| 25 | - } |
|
| 26 | - |
|
| 27 | - public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 28 | - // emulate a failed write that didn't throw an error |
|
| 29 | - return true; |
|
| 30 | - } |
|
| 31 | - |
|
| 32 | - public function deleteObject($urn) { |
|
| 33 | - $this->objectStore->deleteObject($urn); |
|
| 34 | - } |
|
| 35 | - |
|
| 36 | - public function objectExists($urn) { |
|
| 37 | - return $this->objectStore->objectExists($urn); |
|
| 38 | - } |
|
| 39 | - |
|
| 40 | - public function copyObject($from, $to) { |
|
| 41 | - $this->objectStore->copyObject($from, $to); |
|
| 42 | - } |
|
| 43 | - |
|
| 44 | - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 45 | - return null; |
|
| 46 | - } |
|
| 14 | + public function __construct( |
|
| 15 | + private IObjectStore $objectStore, |
|
| 16 | + ) { |
|
| 17 | + } |
|
| 18 | + |
|
| 19 | + public function getStorageId() { |
|
| 20 | + return $this->objectStore->getStorageId(); |
|
| 21 | + } |
|
| 22 | + |
|
| 23 | + public function readObject($urn) { |
|
| 24 | + return $this->objectStore->readObject($urn); |
|
| 25 | + } |
|
| 26 | + |
|
| 27 | + public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 28 | + // emulate a failed write that didn't throw an error |
|
| 29 | + return true; |
|
| 30 | + } |
|
| 31 | + |
|
| 32 | + public function deleteObject($urn) { |
|
| 33 | + $this->objectStore->deleteObject($urn); |
|
| 34 | + } |
|
| 35 | + |
|
| 36 | + public function objectExists($urn) { |
|
| 37 | + return $this->objectStore->objectExists($urn); |
|
| 38 | + } |
|
| 39 | + |
|
| 40 | + public function copyObject($from, $to) { |
|
| 41 | + $this->objectStore->copyObject($from, $to); |
|
| 42 | + } |
|
| 43 | + |
|
| 44 | + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 45 | + return null; |
|
| 46 | + } |
|
| 47 | 47 | } |
@@ -11,36 +11,36 @@ |
||
| 11 | 11 | use OCP\Files\ObjectStore\IObjectStore; |
| 12 | 12 | |
| 13 | 13 | class FailDeleteObjectStore implements IObjectStore { |
| 14 | - public function __construct( |
|
| 15 | - private IObjectStore $objectStore, |
|
| 16 | - ) { |
|
| 17 | - } |
|
| 18 | - |
|
| 19 | - public function getStorageId() { |
|
| 20 | - return $this->objectStore->getStorageId(); |
|
| 21 | - } |
|
| 22 | - |
|
| 23 | - public function readObject($urn) { |
|
| 24 | - return $this->objectStore->readObject($urn); |
|
| 25 | - } |
|
| 26 | - |
|
| 27 | - public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 28 | - return $this->objectStore->writeObject($urn, $stream, $mimetype); |
|
| 29 | - } |
|
| 30 | - |
|
| 31 | - public function deleteObject($urn) { |
|
| 32 | - throw new \Exception(); |
|
| 33 | - } |
|
| 34 | - |
|
| 35 | - public function objectExists($urn) { |
|
| 36 | - return $this->objectStore->objectExists($urn); |
|
| 37 | - } |
|
| 38 | - |
|
| 39 | - public function copyObject($from, $to) { |
|
| 40 | - $this->objectStore->copyObject($from, $to); |
|
| 41 | - } |
|
| 42 | - |
|
| 43 | - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 44 | - return null; |
|
| 45 | - } |
|
| 14 | + public function __construct( |
|
| 15 | + private IObjectStore $objectStore, |
|
| 16 | + ) { |
|
| 17 | + } |
|
| 18 | + |
|
| 19 | + public function getStorageId() { |
|
| 20 | + return $this->objectStore->getStorageId(); |
|
| 21 | + } |
|
| 22 | + |
|
| 23 | + public function readObject($urn) { |
|
| 24 | + return $this->objectStore->readObject($urn); |
|
| 25 | + } |
|
| 26 | + |
|
| 27 | + public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 28 | + return $this->objectStore->writeObject($urn, $stream, $mimetype); |
|
| 29 | + } |
|
| 30 | + |
|
| 31 | + public function deleteObject($urn) { |
|
| 32 | + throw new \Exception(); |
|
| 33 | + } |
|
| 34 | + |
|
| 35 | + public function objectExists($urn) { |
|
| 36 | + return $this->objectStore->objectExists($urn); |
|
| 37 | + } |
|
| 38 | + |
|
| 39 | + public function copyObject($from, $to) { |
|
| 40 | + $this->objectStore->copyObject($from, $to); |
|
| 41 | + } |
|
| 42 | + |
|
| 43 | + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 44 | + return null; |
|
| 45 | + } |
|
| 46 | 46 | } |
@@ -16,260 +16,260 @@ |
||
| 16 | 16 | use OCP\IUser; |
| 17 | 17 | |
| 18 | 18 | class ObjectHomeMountProviderTest extends \Test\TestCase { |
| 19 | - /** @var ObjectHomeMountProvider */ |
|
| 20 | - protected $provider; |
|
| 21 | - |
|
| 22 | - /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 23 | - protected $config; |
|
| 24 | - |
|
| 25 | - /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 26 | - protected $user; |
|
| 27 | - |
|
| 28 | - /** @var IStorageFactory|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 29 | - protected $loader; |
|
| 30 | - |
|
| 31 | - protected function setUp(): void { |
|
| 32 | - parent::setUp(); |
|
| 33 | - |
|
| 34 | - $this->config = $this->createMock(IConfig::class); |
|
| 35 | - $this->user = $this->createMock(IUser::class); |
|
| 36 | - $this->loader = $this->createMock(IStorageFactory::class); |
|
| 37 | - |
|
| 38 | - $objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->createMock(IAppManager::class)); |
|
| 39 | - $this->provider = new ObjectHomeMountProvider($objectStoreConfig); |
|
| 40 | - } |
|
| 41 | - |
|
| 42 | - public function testSingleBucket(): void { |
|
| 43 | - $this->config->method('getSystemValue') |
|
| 44 | - ->willReturnCallback(function ($key, $default) { |
|
| 45 | - if ($key === 'objectstore') { |
|
| 46 | - return [ |
|
| 47 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 48 | - 'arguments' => [ |
|
| 49 | - 'foo' => 'bar' |
|
| 50 | - ], |
|
| 51 | - ]; |
|
| 52 | - } else { |
|
| 53 | - return $default; |
|
| 54 | - } |
|
| 55 | - }); |
|
| 56 | - |
|
| 57 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 58 | - $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 59 | - |
|
| 60 | - $objectStore = $arguments['objectstore']; |
|
| 61 | - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 62 | - $this->assertEquals(['foo' => 'bar', 'multibucket' => false], $objectStore->getArguments()); |
|
| 63 | - } |
|
| 64 | - |
|
| 65 | - public function testMultiBucket(): void { |
|
| 66 | - $this->config->method('getSystemValue') |
|
| 67 | - ->willReturnCallback(function ($key, $default) { |
|
| 68 | - if ($key === 'objectstore_multibucket') { |
|
| 69 | - return [ |
|
| 70 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 71 | - 'arguments' => [ |
|
| 72 | - 'foo' => 'bar' |
|
| 73 | - ], |
|
| 74 | - ]; |
|
| 75 | - } else { |
|
| 76 | - return $default; |
|
| 77 | - } |
|
| 78 | - }); |
|
| 79 | - |
|
| 80 | - $this->user->method('getUID') |
|
| 81 | - ->willReturn('uid'); |
|
| 82 | - $this->loader->expects($this->never())->method($this->anything()); |
|
| 83 | - |
|
| 84 | - $this->config->method('getUserValue') |
|
| 85 | - ->willReturn(null); |
|
| 86 | - |
|
| 87 | - $this->config |
|
| 88 | - ->method('setUserValue') |
|
| 89 | - ->with( |
|
| 90 | - $this->equalTo('uid'), |
|
| 91 | - $this->equalTo('homeobjectstore'), |
|
| 92 | - $this->equalTo('bucket'), |
|
| 93 | - $this->equalTo('49'), |
|
| 94 | - $this->equalTo(null) |
|
| 95 | - ); |
|
| 96 | - |
|
| 97 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 98 | - $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 99 | - |
|
| 100 | - $objectStore = $arguments['objectstore']; |
|
| 101 | - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 102 | - $this->assertEquals(['foo' => 'bar', 'bucket' => 49, 'multibucket' => true], $objectStore->getArguments()); |
|
| 103 | - } |
|
| 104 | - |
|
| 105 | - public function testMultiBucketWithPrefix(): void { |
|
| 106 | - $this->config->method('getSystemValue') |
|
| 107 | - ->willReturnCallback(function ($key, $default) { |
|
| 108 | - if ($key === 'objectstore_multibucket') { |
|
| 109 | - return [ |
|
| 110 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 111 | - 'arguments' => [ |
|
| 112 | - 'foo' => 'bar', |
|
| 113 | - 'bucket' => 'myBucketPrefix', |
|
| 114 | - ], |
|
| 115 | - ]; |
|
| 116 | - } else { |
|
| 117 | - return $default; |
|
| 118 | - } |
|
| 119 | - }); |
|
| 120 | - |
|
| 121 | - $this->user->method('getUID') |
|
| 122 | - ->willReturn('uid'); |
|
| 123 | - $this->loader->expects($this->never())->method($this->anything()); |
|
| 124 | - |
|
| 125 | - $this->config |
|
| 126 | - ->method('getUserValue') |
|
| 127 | - ->willReturn(null); |
|
| 128 | - |
|
| 129 | - $this->config->expects($this->once()) |
|
| 130 | - ->method('setUserValue') |
|
| 131 | - ->with( |
|
| 132 | - $this->equalTo('uid'), |
|
| 133 | - $this->equalTo('homeobjectstore'), |
|
| 134 | - $this->equalTo('bucket'), |
|
| 135 | - $this->equalTo('myBucketPrefix49'), |
|
| 136 | - $this->equalTo(null) |
|
| 137 | - ); |
|
| 138 | - |
|
| 139 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 140 | - $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 141 | - |
|
| 142 | - $objectStore = $arguments['objectstore']; |
|
| 143 | - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 144 | - $this->assertEquals(['foo' => 'bar', 'bucket' => 'myBucketPrefix49', 'multibucket' => true], $objectStore->getArguments()); |
|
| 145 | - } |
|
| 146 | - |
|
| 147 | - public function testMultiBucketBucketAlreadySet(): void { |
|
| 148 | - $this->config->method('getSystemValue') |
|
| 149 | - ->willReturnCallback(function ($key, $default) { |
|
| 150 | - if ($key === 'objectstore_multibucket') { |
|
| 151 | - return [ |
|
| 152 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 153 | - 'arguments' => [ |
|
| 154 | - 'foo' => 'bar', |
|
| 155 | - 'bucket' => 'myBucketPrefix', |
|
| 156 | - ], |
|
| 157 | - ]; |
|
| 158 | - } else { |
|
| 159 | - return $default; |
|
| 160 | - } |
|
| 161 | - }); |
|
| 162 | - |
|
| 163 | - $this->user->method('getUID') |
|
| 164 | - ->willReturn('uid'); |
|
| 165 | - $this->loader->expects($this->never())->method($this->anything()); |
|
| 166 | - |
|
| 167 | - $this->config |
|
| 168 | - ->method('getUserValue') |
|
| 169 | - ->willReturnCallback(function ($uid, $app, $key, $default) { |
|
| 170 | - if ($uid === 'uid' && $app === 'homeobjectstore' && $key === 'bucket') { |
|
| 171 | - return 'awesomeBucket1'; |
|
| 172 | - } else { |
|
| 173 | - return $default; |
|
| 174 | - } |
|
| 175 | - }); |
|
| 176 | - |
|
| 177 | - $this->config->expects($this->never()) |
|
| 178 | - ->method('setUserValue'); |
|
| 179 | - |
|
| 180 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 181 | - $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 182 | - |
|
| 183 | - $objectStore = $arguments['objectstore']; |
|
| 184 | - $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 185 | - $this->assertEquals(['foo' => 'bar', 'bucket' => 'awesomeBucket1', 'multibucket' => true], $objectStore->getArguments()); |
|
| 186 | - } |
|
| 187 | - |
|
| 188 | - public function testMultiBucketConfigFirst(): void { |
|
| 189 | - $this->config->method('getSystemValue') |
|
| 190 | - ->willReturnCallback(function ($key, $default) { |
|
| 191 | - if ($key === 'objectstore_multibucket') { |
|
| 192 | - return [ |
|
| 193 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 194 | - 'arguments' => [ |
|
| 195 | - 'foo' => 'bar', |
|
| 196 | - 'bucket' => 'myBucketPrefix', |
|
| 197 | - ], |
|
| 198 | - ]; |
|
| 199 | - } else { |
|
| 200 | - return $default; |
|
| 201 | - } |
|
| 202 | - }); |
|
| 203 | - |
|
| 204 | - $this->user->method('getUID') |
|
| 205 | - ->willReturn('uid'); |
|
| 206 | - $this->loader->expects($this->never())->method($this->anything()); |
|
| 207 | - |
|
| 208 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 209 | - $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); |
|
| 210 | - } |
|
| 211 | - |
|
| 212 | - public function testMultiBucketConfigFirstFallBackSingle(): void { |
|
| 213 | - $this->config |
|
| 214 | - ->method('getSystemValue')->willReturnMap([ |
|
| 215 | - ['objectstore_multibucket', null, null], |
|
| 216 | - ['objectstore', null, [ |
|
| 217 | - 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 218 | - 'arguments' => [ |
|
| 219 | - 'foo' => 'bar', |
|
| 220 | - 'bucket' => 'myBucketPrefix', |
|
| 221 | - ], |
|
| 222 | - ]], |
|
| 223 | - ]); |
|
| 224 | - |
|
| 225 | - $this->user->method('getUID') |
|
| 226 | - ->willReturn('uid'); |
|
| 227 | - $this->loader->expects($this->never())->method($this->anything()); |
|
| 228 | - |
|
| 229 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 230 | - $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); |
|
| 231 | - } |
|
| 232 | - |
|
| 233 | - public function testNoObjectStore(): void { |
|
| 234 | - $this->config->method('getSystemValue') |
|
| 235 | - ->willReturnCallback(function ($key, $default) { |
|
| 236 | - return $default; |
|
| 237 | - }); |
|
| 238 | - |
|
| 239 | - $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 240 | - $this->assertNull($mount); |
|
| 241 | - } |
|
| 19 | + /** @var ObjectHomeMountProvider */ |
|
| 20 | + protected $provider; |
|
| 21 | + |
|
| 22 | + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 23 | + protected $config; |
|
| 24 | + |
|
| 25 | + /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 26 | + protected $user; |
|
| 27 | + |
|
| 28 | + /** @var IStorageFactory|\PHPUnit\Framework\MockObject\MockObject */ |
|
| 29 | + protected $loader; |
|
| 30 | + |
|
| 31 | + protected function setUp(): void { |
|
| 32 | + parent::setUp(); |
|
| 33 | + |
|
| 34 | + $this->config = $this->createMock(IConfig::class); |
|
| 35 | + $this->user = $this->createMock(IUser::class); |
|
| 36 | + $this->loader = $this->createMock(IStorageFactory::class); |
|
| 37 | + |
|
| 38 | + $objectStoreConfig = new PrimaryObjectStoreConfig($this->config, $this->createMock(IAppManager::class)); |
|
| 39 | + $this->provider = new ObjectHomeMountProvider($objectStoreConfig); |
|
| 40 | + } |
|
| 41 | + |
|
| 42 | + public function testSingleBucket(): void { |
|
| 43 | + $this->config->method('getSystemValue') |
|
| 44 | + ->willReturnCallback(function ($key, $default) { |
|
| 45 | + if ($key === 'objectstore') { |
|
| 46 | + return [ |
|
| 47 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 48 | + 'arguments' => [ |
|
| 49 | + 'foo' => 'bar' |
|
| 50 | + ], |
|
| 51 | + ]; |
|
| 52 | + } else { |
|
| 53 | + return $default; |
|
| 54 | + } |
|
| 55 | + }); |
|
| 56 | + |
|
| 57 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 58 | + $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 59 | + |
|
| 60 | + $objectStore = $arguments['objectstore']; |
|
| 61 | + $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 62 | + $this->assertEquals(['foo' => 'bar', 'multibucket' => false], $objectStore->getArguments()); |
|
| 63 | + } |
|
| 64 | + |
|
| 65 | + public function testMultiBucket(): void { |
|
| 66 | + $this->config->method('getSystemValue') |
|
| 67 | + ->willReturnCallback(function ($key, $default) { |
|
| 68 | + if ($key === 'objectstore_multibucket') { |
|
| 69 | + return [ |
|
| 70 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 71 | + 'arguments' => [ |
|
| 72 | + 'foo' => 'bar' |
|
| 73 | + ], |
|
| 74 | + ]; |
|
| 75 | + } else { |
|
| 76 | + return $default; |
|
| 77 | + } |
|
| 78 | + }); |
|
| 79 | + |
|
| 80 | + $this->user->method('getUID') |
|
| 81 | + ->willReturn('uid'); |
|
| 82 | + $this->loader->expects($this->never())->method($this->anything()); |
|
| 83 | + |
|
| 84 | + $this->config->method('getUserValue') |
|
| 85 | + ->willReturn(null); |
|
| 86 | + |
|
| 87 | + $this->config |
|
| 88 | + ->method('setUserValue') |
|
| 89 | + ->with( |
|
| 90 | + $this->equalTo('uid'), |
|
| 91 | + $this->equalTo('homeobjectstore'), |
|
| 92 | + $this->equalTo('bucket'), |
|
| 93 | + $this->equalTo('49'), |
|
| 94 | + $this->equalTo(null) |
|
| 95 | + ); |
|
| 96 | + |
|
| 97 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 98 | + $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 99 | + |
|
| 100 | + $objectStore = $arguments['objectstore']; |
|
| 101 | + $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 102 | + $this->assertEquals(['foo' => 'bar', 'bucket' => 49, 'multibucket' => true], $objectStore->getArguments()); |
|
| 103 | + } |
|
| 104 | + |
|
| 105 | + public function testMultiBucketWithPrefix(): void { |
|
| 106 | + $this->config->method('getSystemValue') |
|
| 107 | + ->willReturnCallback(function ($key, $default) { |
|
| 108 | + if ($key === 'objectstore_multibucket') { |
|
| 109 | + return [ |
|
| 110 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 111 | + 'arguments' => [ |
|
| 112 | + 'foo' => 'bar', |
|
| 113 | + 'bucket' => 'myBucketPrefix', |
|
| 114 | + ], |
|
| 115 | + ]; |
|
| 116 | + } else { |
|
| 117 | + return $default; |
|
| 118 | + } |
|
| 119 | + }); |
|
| 120 | + |
|
| 121 | + $this->user->method('getUID') |
|
| 122 | + ->willReturn('uid'); |
|
| 123 | + $this->loader->expects($this->never())->method($this->anything()); |
|
| 124 | + |
|
| 125 | + $this->config |
|
| 126 | + ->method('getUserValue') |
|
| 127 | + ->willReturn(null); |
|
| 128 | + |
|
| 129 | + $this->config->expects($this->once()) |
|
| 130 | + ->method('setUserValue') |
|
| 131 | + ->with( |
|
| 132 | + $this->equalTo('uid'), |
|
| 133 | + $this->equalTo('homeobjectstore'), |
|
| 134 | + $this->equalTo('bucket'), |
|
| 135 | + $this->equalTo('myBucketPrefix49'), |
|
| 136 | + $this->equalTo(null) |
|
| 137 | + ); |
|
| 138 | + |
|
| 139 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 140 | + $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 141 | + |
|
| 142 | + $objectStore = $arguments['objectstore']; |
|
| 143 | + $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 144 | + $this->assertEquals(['foo' => 'bar', 'bucket' => 'myBucketPrefix49', 'multibucket' => true], $objectStore->getArguments()); |
|
| 145 | + } |
|
| 146 | + |
|
| 147 | + public function testMultiBucketBucketAlreadySet(): void { |
|
| 148 | + $this->config->method('getSystemValue') |
|
| 149 | + ->willReturnCallback(function ($key, $default) { |
|
| 150 | + if ($key === 'objectstore_multibucket') { |
|
| 151 | + return [ |
|
| 152 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 153 | + 'arguments' => [ |
|
| 154 | + 'foo' => 'bar', |
|
| 155 | + 'bucket' => 'myBucketPrefix', |
|
| 156 | + ], |
|
| 157 | + ]; |
|
| 158 | + } else { |
|
| 159 | + return $default; |
|
| 160 | + } |
|
| 161 | + }); |
|
| 162 | + |
|
| 163 | + $this->user->method('getUID') |
|
| 164 | + ->willReturn('uid'); |
|
| 165 | + $this->loader->expects($this->never())->method($this->anything()); |
|
| 166 | + |
|
| 167 | + $this->config |
|
| 168 | + ->method('getUserValue') |
|
| 169 | + ->willReturnCallback(function ($uid, $app, $key, $default) { |
|
| 170 | + if ($uid === 'uid' && $app === 'homeobjectstore' && $key === 'bucket') { |
|
| 171 | + return 'awesomeBucket1'; |
|
| 172 | + } else { |
|
| 173 | + return $default; |
|
| 174 | + } |
|
| 175 | + }); |
|
| 176 | + |
|
| 177 | + $this->config->expects($this->never()) |
|
| 178 | + ->method('setUserValue'); |
|
| 179 | + |
|
| 180 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 181 | + $arguments = $this->invokePrivate($mount, 'arguments'); |
|
| 182 | + |
|
| 183 | + $objectStore = $arguments['objectstore']; |
|
| 184 | + $this->assertInstanceOf(FakeObjectStore::class, $objectStore); |
|
| 185 | + $this->assertEquals(['foo' => 'bar', 'bucket' => 'awesomeBucket1', 'multibucket' => true], $objectStore->getArguments()); |
|
| 186 | + } |
|
| 187 | + |
|
| 188 | + public function testMultiBucketConfigFirst(): void { |
|
| 189 | + $this->config->method('getSystemValue') |
|
| 190 | + ->willReturnCallback(function ($key, $default) { |
|
| 191 | + if ($key === 'objectstore_multibucket') { |
|
| 192 | + return [ |
|
| 193 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 194 | + 'arguments' => [ |
|
| 195 | + 'foo' => 'bar', |
|
| 196 | + 'bucket' => 'myBucketPrefix', |
|
| 197 | + ], |
|
| 198 | + ]; |
|
| 199 | + } else { |
|
| 200 | + return $default; |
|
| 201 | + } |
|
| 202 | + }); |
|
| 203 | + |
|
| 204 | + $this->user->method('getUID') |
|
| 205 | + ->willReturn('uid'); |
|
| 206 | + $this->loader->expects($this->never())->method($this->anything()); |
|
| 207 | + |
|
| 208 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 209 | + $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + public function testMultiBucketConfigFirstFallBackSingle(): void { |
|
| 213 | + $this->config |
|
| 214 | + ->method('getSystemValue')->willReturnMap([ |
|
| 215 | + ['objectstore_multibucket', null, null], |
|
| 216 | + ['objectstore', null, [ |
|
| 217 | + 'class' => 'Test\Files\Mount\FakeObjectStore', |
|
| 218 | + 'arguments' => [ |
|
| 219 | + 'foo' => 'bar', |
|
| 220 | + 'bucket' => 'myBucketPrefix', |
|
| 221 | + ], |
|
| 222 | + ]], |
|
| 223 | + ]); |
|
| 224 | + |
|
| 225 | + $this->user->method('getUID') |
|
| 226 | + ->willReturn('uid'); |
|
| 227 | + $this->loader->expects($this->never())->method($this->anything()); |
|
| 228 | + |
|
| 229 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 230 | + $this->assertInstanceOf('OC\Files\Mount\MountPoint', $mount); |
|
| 231 | + } |
|
| 232 | + |
|
| 233 | + public function testNoObjectStore(): void { |
|
| 234 | + $this->config->method('getSystemValue') |
|
| 235 | + ->willReturnCallback(function ($key, $default) { |
|
| 236 | + return $default; |
|
| 237 | + }); |
|
| 238 | + |
|
| 239 | + $mount = $this->provider->getHomeMountForUser($this->user, $this->loader); |
|
| 240 | + $this->assertNull($mount); |
|
| 241 | + } |
|
| 242 | 242 | } |
| 243 | 243 | |
| 244 | 244 | class FakeObjectStore implements IObjectStore { |
| 245 | - public function __construct( |
|
| 246 | - private array $arguments, |
|
| 247 | - ) { |
|
| 248 | - } |
|
| 245 | + public function __construct( |
|
| 246 | + private array $arguments, |
|
| 247 | + ) { |
|
| 248 | + } |
|
| 249 | 249 | |
| 250 | - public function getArguments() { |
|
| 251 | - return $this->arguments; |
|
| 252 | - } |
|
| 250 | + public function getArguments() { |
|
| 251 | + return $this->arguments; |
|
| 252 | + } |
|
| 253 | 253 | |
| 254 | - public function getStorageId() { |
|
| 255 | - } |
|
| 254 | + public function getStorageId() { |
|
| 255 | + } |
|
| 256 | 256 | |
| 257 | - public function readObject($urn) { |
|
| 258 | - } |
|
| 257 | + public function readObject($urn) { |
|
| 258 | + } |
|
| 259 | 259 | |
| 260 | - public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 261 | - } |
|
| 260 | + public function writeObject($urn, $stream, ?string $mimetype = null) { |
|
| 261 | + } |
|
| 262 | 262 | |
| 263 | - public function deleteObject($urn) { |
|
| 264 | - } |
|
| 263 | + public function deleteObject($urn) { |
|
| 264 | + } |
|
| 265 | 265 | |
| 266 | - public function objectExists($urn) { |
|
| 267 | - } |
|
| 266 | + public function objectExists($urn) { |
|
| 267 | + } |
|
| 268 | 268 | |
| 269 | - public function copyObject($from, $to) { |
|
| 270 | - } |
|
| 269 | + public function copyObject($from, $to) { |
|
| 270 | + } |
|
| 271 | 271 | |
| 272 | - public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 273 | - return null; |
|
| 274 | - } |
|
| 272 | + public function preSignedUrl(string $urn, \DateTimeInterface $expiration): ?string { |
|
| 273 | + return null; |
|
| 274 | + } |
|
| 275 | 275 | } |