1 | <?php |
||||||
2 | /** |
||||||
3 | * @copyright Copyright (c) 2016, ownCloud, Inc. |
||||||
4 | * |
||||||
5 | * @author Bart Visscher <[email protected]> |
||||||
6 | * @author Björn Schießle <[email protected]> |
||||||
7 | * @author Christoph Wurst <[email protected]> |
||||||
8 | * @author Daniel Calviño Sánchez <[email protected]> |
||||||
9 | * @author Jakob Sack <[email protected]> |
||||||
10 | * @author Jan-Philipp Litza <[email protected]> |
||||||
11 | * @author Joas Schilling <[email protected]> |
||||||
12 | * @author Jörn Friedrich Dreyer <[email protected]> |
||||||
13 | * @author Julius Härtl <[email protected]> |
||||||
14 | * @author Lukas Reschke <[email protected]> |
||||||
15 | * @author Morris Jobke <[email protected]> |
||||||
16 | * @author Owen Winkler <[email protected]> |
||||||
17 | * @author Robin Appelman <[email protected]> |
||||||
18 | * @author Roeland Jago Douma <[email protected]> |
||||||
19 | * @author Semih Serhat Karakaya <[email protected]> |
||||||
20 | * @author Stefan Schneider <[email protected]> |
||||||
21 | * @author Thomas Müller <[email protected]> |
||||||
22 | * @author Vincent Petry <[email protected]> |
||||||
23 | * |
||||||
24 | * @license AGPL-3.0 |
||||||
25 | * |
||||||
26 | * This code is free software: you can redistribute it and/or modify |
||||||
27 | * it under the terms of the GNU Affero General Public License, version 3, |
||||||
28 | * as published by the Free Software Foundation. |
||||||
29 | * |
||||||
30 | * This program is distributed in the hope that it will be useful, |
||||||
31 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
32 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
33 | * GNU Affero General Public License for more details. |
||||||
34 | * |
||||||
35 | * You should have received a copy of the GNU Affero General Public License, version 3, |
||||||
36 | * along with this program. If not, see <http://www.gnu.org/licenses/> |
||||||
37 | * |
||||||
38 | */ |
||||||
39 | namespace OCA\DAV\Connector\Sabre; |
||||||
40 | |||||||
41 | use Icewind\Streams\CallbackWrapper; |
||||||
42 | use OC\AppFramework\Http\Request; |
||||||
43 | use OC\Files\Filesystem; |
||||||
44 | use OC\Files\Stream\HashWrapper; |
||||||
45 | use OC\Files\View; |
||||||
46 | use OC\Metadata\FileMetadata; |
||||||
47 | use OCA\DAV\AppInfo\Application; |
||||||
48 | use OCA\DAV\Connector\Sabre\Exception\BadGateway; |
||||||
49 | use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge; |
||||||
50 | use OCA\DAV\Connector\Sabre\Exception\FileLocked; |
||||||
51 | use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException; |
||||||
52 | use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType; |
||||||
53 | use OCP\Encryption\Exceptions\GenericEncryptionException; |
||||||
54 | use OCP\Files\EntityTooLargeException; |
||||||
55 | use OCP\Files\FileInfo; |
||||||
56 | use OCP\Files\ForbiddenException; |
||||||
57 | use OCP\Files\GenericFileException; |
||||||
58 | use OCP\Files\InvalidContentException; |
||||||
59 | use OCP\Files\InvalidPathException; |
||||||
60 | use OCP\Files\LockNotAcquiredException; |
||||||
61 | use OCP\Files\NotFoundException; |
||||||
62 | use OCP\Files\NotPermittedException; |
||||||
63 | use OCP\Files\Storage; |
||||||
64 | use OCP\Files\StorageNotAvailableException; |
||||||
65 | use OCP\IL10N; |
||||||
66 | use OCP\L10N\IFactory as IL10NFactory; |
||||||
67 | use OCP\Lock\ILockingProvider; |
||||||
68 | use OCP\Lock\LockedException; |
||||||
69 | use OCP\Share\IManager; |
||||||
70 | use Psr\Log\LoggerInterface; |
||||||
71 | use Sabre\DAV\Exception; |
||||||
72 | use Sabre\DAV\Exception\BadRequest; |
||||||
73 | use Sabre\DAV\Exception\Forbidden; |
||||||
74 | use Sabre\DAV\Exception\NotFound; |
||||||
75 | use Sabre\DAV\Exception\NotImplemented; |
||||||
76 | use Sabre\DAV\Exception\ServiceUnavailable; |
||||||
77 | use Sabre\DAV\IFile; |
||||||
78 | |||||||
79 | class File extends Node implements IFile { |
||||||
80 | protected $request; |
||||||
81 | |||||||
82 | protected IL10N $l10n; |
||||||
83 | |||||||
84 | /** @var array<string, FileMetadata> */ |
||||||
85 | private array $metadata = []; |
||||||
86 | |||||||
87 | /** |
||||||
88 | * Sets up the node, expects a full path name |
||||||
89 | * |
||||||
90 | * @param \OC\Files\View $view |
||||||
91 | * @param \OCP\Files\FileInfo $info |
||||||
92 | * @param \OCP\Share\IManager $shareManager |
||||||
93 | * @param \OC\AppFramework\Http\Request $request |
||||||
94 | */ |
||||||
95 | public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) { |
||||||
96 | parent::__construct($view, $info, $shareManager); |
||||||
97 | |||||||
98 | // Querying IL10N directly results in a dependency loop |
||||||
99 | /** @var IL10NFactory $l10nFactory */ |
||||||
100 | $l10nFactory = \OC::$server->get(IL10NFactory::class); |
||||||
101 | $this->l10n = $l10nFactory->get(Application::APP_ID); |
||||||
102 | |||||||
103 | if (isset($request)) { |
||||||
104 | $this->request = $request; |
||||||
105 | } else { |
||||||
106 | $this->request = \OC::$server->getRequest(); |
||||||
107 | } |
||||||
108 | } |
||||||
109 | |||||||
110 | /** |
||||||
111 | * Updates the data |
||||||
112 | * |
||||||
113 | * The data argument is a readable stream resource. |
||||||
114 | * |
||||||
115 | * After a successful put operation, you may choose to return an ETag. The |
||||||
116 | * etag must always be surrounded by double-quotes. These quotes must |
||||||
117 | * appear in the actual string you're returning. |
||||||
118 | * |
||||||
119 | * Clients may use the ETag from a PUT request to later on make sure that |
||||||
120 | * when they update the file, the contents haven't changed in the mean |
||||||
121 | * time. |
||||||
122 | * |
||||||
123 | * If you don't plan to store the file byte-by-byte, and you return a |
||||||
124 | * different object on a subsequent GET you are strongly recommended to not |
||||||
125 | * return an ETag, and just return null. |
||||||
126 | * |
||||||
127 | * @param resource $data |
||||||
128 | * |
||||||
129 | * @throws Forbidden |
||||||
130 | * @throws UnsupportedMediaType |
||||||
131 | * @throws BadRequest |
||||||
132 | * @throws Exception |
||||||
133 | * @throws EntityTooLarge |
||||||
134 | * @throws ServiceUnavailable |
||||||
135 | * @throws FileLocked |
||||||
136 | * @return string|null |
||||||
137 | */ |
||||||
138 | public function put($data) { |
||||||
139 | try { |
||||||
140 | $exists = $this->fileView->file_exists($this->path); |
||||||
141 | if ($exists && !$this->info->isUpdateable()) { |
||||||
142 | throw new Forbidden(); |
||||||
143 | } |
||||||
144 | } catch (StorageNotAvailableException $e) { |
||||||
145 | throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()])); |
||||||
146 | } |
||||||
147 | |||||||
148 | // verify path of the target |
||||||
149 | $this->verifyPath(); |
||||||
150 | |||||||
151 | // chunked handling |
||||||
152 | if (isset($_SERVER['HTTP_OC_CHUNKED'])) { |
||||||
153 | try { |
||||||
154 | return $this->createFileChunked($data); |
||||||
155 | } catch (\Exception $e) { |
||||||
156 | $this->convertToSabreException($e); |
||||||
157 | } |
||||||
158 | } |
||||||
159 | |||||||
160 | /** @var Storage $partStorage */ |
||||||
161 | [$partStorage] = $this->fileView->resolvePath($this->path); |
||||||
162 | $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1); |
||||||
163 | |||||||
164 | $view = \OC\Files\Filesystem::getView(); |
||||||
165 | |||||||
166 | if ($needsPartFile) { |
||||||
167 | // mark file as partial while uploading (ignored by the scanner) |
||||||
168 | $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part'; |
||||||
169 | |||||||
170 | if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) { |
||||||
0 ignored issues
–
show
|
|||||||
171 | $needsPartFile = false; |
||||||
172 | } |
||||||
173 | } |
||||||
174 | if (!$needsPartFile) { |
||||||
175 | // upload file directly as the final path |
||||||
176 | $partFilePath = $this->path; |
||||||
177 | |||||||
178 | if ($view && !$this->emitPreHooks($exists)) { |
||||||
179 | throw new Exception($this->l10n->t('Could not write to final file, canceled by hook')); |
||||||
180 | } |
||||||
181 | } |
||||||
182 | |||||||
183 | // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share) |
||||||
184 | /** @var \OC\Files\Storage\Storage $partStorage */ |
||||||
185 | [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath); |
||||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||||
186 | /** @var \OC\Files\Storage\Storage $storage */ |
||||||
187 | [$storage, $internalPath] = $this->fileView->resolvePath($this->path); |
||||||
188 | try { |
||||||
189 | if (!$needsPartFile) { |
||||||
190 | try { |
||||||
191 | $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
||||||
192 | } catch (LockedException $e) { |
||||||
193 | // during very large uploads, the shared lock we got at the start might have been expired |
||||||
194 | // meaning that the above lock can fail not just only because somebody else got a shared lock |
||||||
195 | // or because there is no existing shared lock to make exclusive |
||||||
196 | // |
||||||
197 | // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
||||||
198 | // lock this will still fail, if our original shared lock expired the new lock will be successful and |
||||||
199 | // the entire operation will be safe |
||||||
200 | |||||||
201 | try { |
||||||
202 | $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
||||||
203 | } catch (LockedException $ex) { |
||||||
204 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
205 | } |
||||||
206 | } |
||||||
207 | } |
||||||
208 | |||||||
209 | if (!is_resource($data)) { |
||||||
210 | $tmpData = fopen('php://temp', 'r+'); |
||||||
211 | if ($data !== null) { |
||||||
212 | fwrite($tmpData, $data); |
||||||
213 | rewind($tmpData); |
||||||
214 | } |
||||||
215 | $data = $tmpData; |
||||||
216 | } |
||||||
217 | |||||||
218 | if ($this->request->getHeader('X-HASH') !== '') { |
||||||
219 | $hash = $this->request->getHeader('X-HASH'); |
||||||
220 | if ($hash === 'all' || $hash === 'md5') { |
||||||
221 | $data = HashWrapper::wrap($data, 'md5', function ($hash) { |
||||||
222 | $this->header('X-Hash-MD5: ' . $hash); |
||||||
223 | }); |
||||||
224 | } |
||||||
225 | |||||||
226 | if ($hash === 'all' || $hash === 'sha1') { |
||||||
227 | $data = HashWrapper::wrap($data, 'sha1', function ($hash) { |
||||||
228 | $this->header('X-Hash-SHA1: ' . $hash); |
||||||
229 | }); |
||||||
230 | } |
||||||
231 | |||||||
232 | if ($hash === 'all' || $hash === 'sha256') { |
||||||
233 | $data = HashWrapper::wrap($data, 'sha256', function ($hash) { |
||||||
234 | $this->header('X-Hash-SHA256: ' . $hash); |
||||||
235 | }); |
||||||
236 | } |
||||||
237 | } |
||||||
238 | |||||||
239 | if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) { |
||||||
240 | $isEOF = false; |
||||||
241 | $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) { |
||||||
242 | $isEOF = feof($stream); |
||||||
243 | }); |
||||||
244 | |||||||
245 | $result = true; |
||||||
246 | $count = -1; |
||||||
247 | try { |
||||||
248 | $count = $partStorage->writeStream($internalPartPath, $wrappedData); |
||||||
0 ignored issues
–
show
The method
writeStream() does not exist on OC\Files\Storage\Storage . Since it exists in all sub-types, consider adding an abstract or default implementation to OC\Files\Storage\Storage .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
249 | } catch (GenericFileException $e) { |
||||||
250 | $result = false; |
||||||
251 | } catch (BadGateway $e) { |
||||||
252 | throw $e; |
||||||
253 | } |
||||||
254 | |||||||
255 | |||||||
256 | if ($result === false) { |
||||||
257 | $result = $isEOF; |
||||||
258 | if (is_resource($wrappedData)) { |
||||||
259 | $result = feof($wrappedData); |
||||||
260 | } |
||||||
261 | } |
||||||
262 | } else { |
||||||
263 | $target = $partStorage->fopen($internalPartPath, 'wb'); |
||||||
264 | if ($target === false) { |
||||||
265 | \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']); |
||||||
266 | // because we have no clue about the cause we can only throw back a 500/Internal Server Error |
||||||
267 | throw new Exception($this->l10n->t('Could not write file contents')); |
||||||
268 | } |
||||||
269 | [$count, $result] = \OC_Helper::streamCopy($data, $target); |
||||||
270 | fclose($target); |
||||||
271 | } |
||||||
272 | |||||||
273 | if ($result === false) { |
||||||
274 | $expected = -1; |
||||||
275 | if (isset($_SERVER['CONTENT_LENGTH'])) { |
||||||
276 | $expected = (int)$_SERVER['CONTENT_LENGTH']; |
||||||
277 | } |
||||||
278 | if ($expected !== 0) { |
||||||
279 | throw new Exception( |
||||||
280 | $this->l10n->t( |
||||||
281 | 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', |
||||||
282 | [ |
||||||
283 | $this->l10n->n('%n byte', '%n bytes', $count), |
||||||
284 | $this->l10n->n('%n byte', '%n bytes', $expected), |
||||||
285 | ], |
||||||
286 | ) |
||||||
287 | ); |
||||||
288 | } |
||||||
289 | } |
||||||
290 | |||||||
291 | // if content length is sent by client: |
||||||
292 | // double check if the file was fully received |
||||||
293 | // compare expected and actual size |
||||||
294 | if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
||||||
295 | $expected = (int)$_SERVER['CONTENT_LENGTH']; |
||||||
296 | if ($count !== $expected) { |
||||||
297 | throw new BadRequest( |
||||||
298 | $this->l10n->t( |
||||||
299 | '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.', |
||||||
300 | [ |
||||||
301 | $this->l10n->n('%n byte', '%n bytes', $expected), |
||||||
302 | $this->l10n->n('%n byte', '%n bytes', $count), |
||||||
303 | ], |
||||||
304 | ) |
||||||
305 | ); |
||||||
306 | } |
||||||
307 | } |
||||||
308 | } catch (\Exception $e) { |
||||||
309 | if ($e instanceof LockedException) { |
||||||
310 | \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]); |
||||||
311 | } else { |
||||||
312 | \OC::$server->get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); |
||||||
313 | } |
||||||
314 | |||||||
315 | if ($needsPartFile) { |
||||||
316 | $partStorage->unlink($internalPartPath); |
||||||
317 | } |
||||||
318 | $this->convertToSabreException($e); |
||||||
319 | } |
||||||
320 | |||||||
321 | try { |
||||||
322 | if ($needsPartFile) { |
||||||
323 | if ($view && !$this->emitPreHooks($exists)) { |
||||||
324 | $partStorage->unlink($internalPartPath); |
||||||
325 | throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook')); |
||||||
326 | } |
||||||
327 | try { |
||||||
328 | $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE); |
||||||
329 | } catch (LockedException $e) { |
||||||
330 | // during very large uploads, the shared lock we got at the start might have been expired |
||||||
331 | // meaning that the above lock can fail not just only because somebody else got a shared lock |
||||||
332 | // or because there is no existing shared lock to make exclusive |
||||||
333 | // |
||||||
334 | // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared |
||||||
335 | // lock this will still fail, if our original shared lock expired the new lock will be successful and |
||||||
336 | // the entire operation will be safe |
||||||
337 | |||||||
338 | try { |
||||||
339 | $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE); |
||||||
340 | } catch (LockedException $ex) { |
||||||
341 | if ($needsPartFile) { |
||||||
0 ignored issues
–
show
|
|||||||
342 | $partStorage->unlink($internalPartPath); |
||||||
343 | } |
||||||
344 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
345 | } |
||||||
346 | } |
||||||
347 | |||||||
348 | // rename to correct path |
||||||
349 | try { |
||||||
350 | $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath); |
||||||
351 | $fileExists = $storage->file_exists($internalPath); |
||||||
352 | if ($renameOkay === false || $fileExists === false) { |
||||||
353 | \OC::$server->get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']); |
||||||
354 | throw new Exception($this->l10n->t('Could not rename part file to final file')); |
||||||
355 | } |
||||||
356 | } catch (ForbiddenException $ex) { |
||||||
357 | if (!$ex->getRetry()) { |
||||||
358 | $partStorage->unlink($internalPartPath); |
||||||
359 | } |
||||||
360 | throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
||||||
361 | } catch (\Exception $e) { |
||||||
362 | $partStorage->unlink($internalPartPath); |
||||||
363 | $this->convertToSabreException($e); |
||||||
364 | } |
||||||
365 | } |
||||||
366 | |||||||
367 | // since we skipped the view we need to scan and emit the hooks ourselves |
||||||
368 | $storage->getUpdater()->update($internalPath); |
||||||
369 | |||||||
370 | try { |
||||||
371 | $this->changeLock(ILockingProvider::LOCK_SHARED); |
||||||
372 | } catch (LockedException $e) { |
||||||
373 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
374 | } |
||||||
375 | |||||||
376 | // allow sync clients to send the mtime along in a header |
||||||
377 | if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
||||||
378 | $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
||||||
379 | if ($this->fileView->touch($this->path, $mtime)) { |
||||||
380 | $this->header('X-OC-MTime: accepted'); |
||||||
381 | } |
||||||
382 | } |
||||||
383 | |||||||
384 | $fileInfoUpdate = [ |
||||||
385 | 'upload_time' => time() |
||||||
386 | ]; |
||||||
387 | |||||||
388 | // allow sync clients to send the creation time along in a header |
||||||
389 | if (isset($this->request->server['HTTP_X_OC_CTIME'])) { |
||||||
390 | $ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']); |
||||||
391 | $fileInfoUpdate['creation_time'] = $ctime; |
||||||
392 | $this->header('X-OC-CTime: accepted'); |
||||||
393 | } |
||||||
394 | |||||||
395 | $this->fileView->putFileInfo($this->path, $fileInfoUpdate); |
||||||
396 | |||||||
397 | if ($view) { |
||||||
398 | $this->emitPostHooks($exists); |
||||||
399 | } |
||||||
400 | |||||||
401 | $this->refreshInfo(); |
||||||
402 | |||||||
403 | if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
||||||
404 | $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
||||||
405 | $this->setChecksum($checksum); |
||||||
406 | } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') { |
||||||
407 | $this->setChecksum(''); |
||||||
408 | } |
||||||
409 | } catch (StorageNotAvailableException $e) { |
||||||
410 | throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e); |
||||||
411 | } |
||||||
412 | |||||||
413 | return '"' . $this->info->getEtag() . '"'; |
||||||
414 | } |
||||||
415 | |||||||
416 | private function getPartFileBasePath($path) { |
||||||
417 | $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true); |
||||||
418 | if ($partFileInStorage) { |
||||||
419 | return $path; |
||||||
420 | } else { |
||||||
421 | return md5($path); // will place it in the root of the view with a unique name |
||||||
422 | } |
||||||
423 | } |
||||||
424 | |||||||
425 | private function emitPreHooks(bool $exists, ?string $path = null): bool { |
||||||
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 true; |
||||||
433 | } |
||||||
434 | $run = true; |
||||||
435 | |||||||
436 | if (!$exists) { |
||||||
437 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [ |
||||||
438 | \OC\Files\Filesystem::signal_param_path => $hookPath, |
||||||
439 | \OC\Files\Filesystem::signal_param_run => &$run, |
||||||
440 | ]); |
||||||
441 | } else { |
||||||
442 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [ |
||||||
443 | \OC\Files\Filesystem::signal_param_path => $hookPath, |
||||||
444 | \OC\Files\Filesystem::signal_param_run => &$run, |
||||||
445 | ]); |
||||||
446 | } |
||||||
447 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [ |
||||||
448 | \OC\Files\Filesystem::signal_param_path => $hookPath, |
||||||
449 | \OC\Files\Filesystem::signal_param_run => &$run, |
||||||
450 | ]); |
||||||
451 | return $run; |
||||||
452 | } |
||||||
453 | |||||||
454 | private function emitPostHooks(bool $exists, ?string $path = null): void { |
||||||
455 | if (is_null($path)) { |
||||||
456 | $path = $this->path; |
||||||
457 | } |
||||||
458 | $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path)); |
||||||
459 | if ($hookPath === null) { |
||||||
460 | // We only trigger hooks from inside default view |
||||||
461 | return; |
||||||
462 | } |
||||||
463 | if (!$exists) { |
||||||
464 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [ |
||||||
465 | \OC\Files\Filesystem::signal_param_path => $hookPath |
||||||
466 | ]); |
||||||
467 | } else { |
||||||
468 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [ |
||||||
469 | \OC\Files\Filesystem::signal_param_path => $hookPath |
||||||
470 | ]); |
||||||
471 | } |
||||||
472 | \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [ |
||||||
473 | \OC\Files\Filesystem::signal_param_path => $hookPath |
||||||
474 | ]); |
||||||
475 | } |
||||||
476 | |||||||
477 | /** |
||||||
478 | * Returns the data |
||||||
479 | * |
||||||
480 | * @return resource |
||||||
481 | * @throws Forbidden |
||||||
482 | * @throws ServiceUnavailable |
||||||
483 | */ |
||||||
484 | public function get() { |
||||||
485 | //throw exception if encryption is disabled but files are still encrypted |
||||||
486 | try { |
||||||
487 | if (!$this->info->isReadable()) { |
||||||
488 | // do a if the file did not exist |
||||||
489 | throw new NotFound(); |
||||||
490 | } |
||||||
491 | try { |
||||||
492 | $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb'); |
||||||
493 | } catch (\Exception $e) { |
||||||
494 | $this->convertToSabreException($e); |
||||||
495 | } |
||||||
496 | |||||||
497 | if ($res === false) { |
||||||
498 | throw new ServiceUnavailable($this->l10n->t('Could not open file')); |
||||||
499 | } |
||||||
500 | |||||||
501 | // comparing current file size with the one in DB |
||||||
502 | // if different, fix DB and refresh cache. |
||||||
503 | if ($this->getSize() !== $this->fileView->filesize($this->getPath())) { |
||||||
504 | $logger = \OC::$server->get(LoggerInterface::class); |
||||||
505 | $logger->warning('fixing cached size of file id=' . $this->getId()); |
||||||
506 | |||||||
507 | $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath()); |
||||||
508 | $this->refreshInfo(); |
||||||
509 | } |
||||||
510 | |||||||
511 | return $res; |
||||||
512 | } catch (GenericEncryptionException $e) { |
||||||
513 | // returning 503 will allow retry of the operation at a later point in time |
||||||
514 | throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()])); |
||||||
515 | } catch (StorageNotAvailableException $e) { |
||||||
516 | throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()])); |
||||||
517 | } catch (ForbiddenException $ex) { |
||||||
518 | throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
||||||
519 | } catch (LockedException $e) { |
||||||
520 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
521 | } |
||||||
522 | } |
||||||
523 | |||||||
524 | /** |
||||||
525 | * Delete the current file |
||||||
526 | * |
||||||
527 | * @throws Forbidden |
||||||
528 | * @throws ServiceUnavailable |
||||||
529 | */ |
||||||
530 | public function delete() { |
||||||
531 | if (!$this->info->isDeletable()) { |
||||||
532 | throw new Forbidden(); |
||||||
533 | } |
||||||
534 | |||||||
535 | try { |
||||||
536 | if (!$this->fileView->unlink($this->path)) { |
||||||
537 | // assume it wasn't possible to delete due to permissions |
||||||
538 | throw new Forbidden(); |
||||||
539 | } |
||||||
540 | } catch (StorageNotAvailableException $e) { |
||||||
541 | throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()])); |
||||||
542 | } catch (ForbiddenException $ex) { |
||||||
543 | throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry()); |
||||||
544 | } catch (LockedException $e) { |
||||||
545 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
546 | } |
||||||
547 | } |
||||||
548 | |||||||
549 | /** |
||||||
550 | * Returns the mime-type for a file |
||||||
551 | * |
||||||
552 | * If null is returned, we'll assume application/octet-stream |
||||||
553 | * |
||||||
554 | * @return string |
||||||
555 | */ |
||||||
556 | public function getContentType() { |
||||||
557 | $mimeType = $this->info->getMimetype(); |
||||||
558 | |||||||
559 | // PROPFIND needs to return the correct mime type, for consistency with the web UI |
||||||
560 | if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { |
||||||
561 | return $mimeType; |
||||||
562 | } |
||||||
563 | return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType); |
||||||
564 | } |
||||||
565 | |||||||
566 | /** |
||||||
567 | * @return array|bool |
||||||
568 | */ |
||||||
569 | public function getDirectDownload() { |
||||||
570 | if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) { |
||||||
571 | return []; |
||||||
572 | } |
||||||
573 | /** @var \OCP\Files\Storage $storage */ |
||||||
574 | [$storage, $internalPath] = $this->fileView->resolvePath($this->path); |
||||||
575 | if (is_null($storage)) { |
||||||
576 | return []; |
||||||
577 | } |
||||||
578 | |||||||
579 | return $storage->getDirectDownload($internalPath); |
||||||
580 | } |
||||||
581 | |||||||
582 | /** |
||||||
583 | * @param resource $data |
||||||
584 | * @return null|string |
||||||
585 | * @throws Exception |
||||||
586 | * @throws BadRequest |
||||||
587 | * @throws NotImplemented |
||||||
588 | * @throws ServiceUnavailable |
||||||
589 | */ |
||||||
590 | private function createFileChunked($data) { |
||||||
591 | [$path, $name] = \Sabre\Uri\split($this->path); |
||||||
592 | |||||||
593 | $info = \OC_FileChunking::decodeName($name); |
||||||
594 | if (empty($info)) { |
||||||
595 | throw new NotImplemented($this->l10n->t('Invalid chunk name')); |
||||||
596 | } |
||||||
597 | |||||||
598 | $chunk_handler = new \OC_FileChunking($info); |
||||||
599 | $bytesWritten = $chunk_handler->store($info['index'], $data); |
||||||
600 | |||||||
601 | //detect aborted upload |
||||||
602 | if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') { |
||||||
603 | if (isset($_SERVER['CONTENT_LENGTH'])) { |
||||||
604 | $expected = (int)$_SERVER['CONTENT_LENGTH']; |
||||||
605 | if ($bytesWritten !== $expected) { |
||||||
606 | $chunk_handler->remove($info['index']); |
||||||
607 | throw new BadRequest( |
||||||
608 | $this->l10n->t( |
||||||
609 | '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.', |
||||||
610 | [ |
||||||
611 | $this->l10n->n('%n byte', '%n bytes', $expected), |
||||||
612 | $this->l10n->n('%n byte', '%n bytes', $bytesWritten), |
||||||
613 | ], |
||||||
614 | ) |
||||||
615 | ); |
||||||
616 | } |
||||||
617 | } |
||||||
618 | } |
||||||
619 | |||||||
620 | if ($chunk_handler->isComplete()) { |
||||||
621 | /** @var Storage $storage */ |
||||||
622 | [$storage,] = $this->fileView->resolvePath($path); |
||||||
623 | $needsPartFile = $storage->needsPartFile(); |
||||||
624 | $partFile = null; |
||||||
625 | |||||||
626 | $targetPath = $path . '/' . $info['name']; |
||||||
627 | /** @var \OC\Files\Storage\Storage $targetStorage */ |
||||||
628 | [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath); |
||||||
629 | |||||||
630 | $exists = $this->fileView->file_exists($targetPath); |
||||||
631 | |||||||
632 | try { |
||||||
633 | $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED); |
||||||
634 | |||||||
635 | $this->emitPreHooks($exists, $targetPath); |
||||||
636 | $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE); |
||||||
637 | /** @var \OC\Files\Storage\Storage $targetStorage */ |
||||||
638 | [$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath); |
||||||
639 | |||||||
640 | if ($needsPartFile) { |
||||||
641 | // we first assembly the target file as a part file |
||||||
642 | $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part'; |
||||||
643 | /** @var \OC\Files\Storage\Storage $targetStorage */ |
||||||
644 | [$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile); |
||||||
645 | |||||||
646 | |||||||
647 | $chunk_handler->file_assemble($partStorage, $partInternalPath); |
||||||
648 | |||||||
649 | // here is the final atomic rename |
||||||
650 | $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath); |
||||||
651 | $fileExists = $targetStorage->file_exists($targetInternalPath); |
||||||
652 | if ($renameOkay === false || $fileExists === false) { |
||||||
653 | \OC::$server->get(LoggerInterface::class)->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']); |
||||||
654 | // only delete if an error occurred and the target file was already created |
||||||
655 | if ($fileExists) { |
||||||
656 | // set to null to avoid double-deletion when handling exception |
||||||
657 | // stray part file |
||||||
658 | $partFile = null; |
||||||
659 | $targetStorage->unlink($targetInternalPath); |
||||||
660 | } |
||||||
661 | $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
||||||
662 | throw new Exception($this->l10n->t('Could not rename part file assembled from chunks')); |
||||||
663 | } |
||||||
664 | } else { |
||||||
665 | // assemble directly into the final file |
||||||
666 | $chunk_handler->file_assemble($targetStorage, $targetInternalPath); |
||||||
667 | } |
||||||
668 | |||||||
669 | // allow sync clients to send the mtime along in a header |
||||||
670 | if (isset($this->request->server['HTTP_X_OC_MTIME'])) { |
||||||
671 | $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']); |
||||||
672 | if ($targetStorage->touch($targetInternalPath, $mtime)) { |
||||||
673 | $this->header('X-OC-MTime: accepted'); |
||||||
674 | } |
||||||
675 | } |
||||||
676 | |||||||
677 | // since we skipped the view we need to scan and emit the hooks ourselves |
||||||
678 | $targetStorage->getUpdater()->update($targetInternalPath); |
||||||
679 | |||||||
680 | $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED); |
||||||
681 | |||||||
682 | $this->emitPostHooks($exists, $targetPath); |
||||||
683 | |||||||
684 | // FIXME: should call refreshInfo but can't because $this->path is not the of the final file |
||||||
685 | $info = $this->fileView->getFileInfo($targetPath); |
||||||
686 | |||||||
687 | if (isset($this->request->server['HTTP_OC_CHECKSUM'])) { |
||||||
688 | $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']); |
||||||
689 | $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]); |
||||||
690 | } elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') { |
||||||
691 | $this->fileView->putFileInfo($this->path, ['checksum' => '']); |
||||||
692 | } |
||||||
693 | |||||||
694 | $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED); |
||||||
695 | |||||||
696 | return $info->getEtag(); |
||||||
697 | } catch (\Exception $e) { |
||||||
698 | if ($partFile !== null) { |
||||||
699 | $targetStorage->unlink($targetInternalPath); |
||||||
700 | } |
||||||
701 | $this->convertToSabreException($e); |
||||||
702 | } |
||||||
703 | } |
||||||
704 | |||||||
705 | return null; |
||||||
706 | } |
||||||
707 | |||||||
708 | /** |
||||||
709 | * Convert the given exception to a SabreException instance |
||||||
710 | * |
||||||
711 | * @param \Exception $e |
||||||
712 | * |
||||||
713 | * @throws \Sabre\DAV\Exception |
||||||
714 | */ |
||||||
715 | private function convertToSabreException(\Exception $e) { |
||||||
716 | if ($e instanceof \Sabre\DAV\Exception) { |
||||||
717 | throw $e; |
||||||
718 | } |
||||||
719 | if ($e instanceof NotPermittedException) { |
||||||
720 | // a more general case - due to whatever reason the content could not be written |
||||||
721 | throw new Forbidden($e->getMessage(), 0, $e); |
||||||
722 | } |
||||||
723 | if ($e instanceof ForbiddenException) { |
||||||
724 | // the path for the file was forbidden |
||||||
725 | throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e); |
||||||
726 | } |
||||||
727 | if ($e instanceof EntityTooLargeException) { |
||||||
728 | // the file is too big to be stored |
||||||
729 | throw new EntityTooLarge($e->getMessage(), 0, $e); |
||||||
730 | } |
||||||
731 | if ($e instanceof InvalidContentException) { |
||||||
732 | // the file content is not permitted |
||||||
733 | throw new UnsupportedMediaType($e->getMessage(), 0, $e); |
||||||
734 | } |
||||||
735 | if ($e instanceof InvalidPathException) { |
||||||
736 | // the path for the file was not valid |
||||||
737 | // TODO: find proper http status code for this case |
||||||
738 | throw new Forbidden($e->getMessage(), 0, $e); |
||||||
739 | } |
||||||
740 | if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) { |
||||||
741 | // the file is currently being written to by another process |
||||||
742 | throw new FileLocked($e->getMessage(), $e->getCode(), $e); |
||||||
743 | } |
||||||
744 | if ($e instanceof GenericEncryptionException) { |
||||||
745 | // returning 503 will allow retry of the operation at a later point in time |
||||||
746 | throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e); |
||||||
747 | } |
||||||
748 | if ($e instanceof StorageNotAvailableException) { |
||||||
749 | throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e); |
||||||
750 | } |
||||||
751 | if ($e instanceof NotFoundException) { |
||||||
752 | throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e); |
||||||
753 | } |
||||||
754 | |||||||
755 | throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e); |
||||||
756 | } |
||||||
757 | |||||||
758 | /** |
||||||
759 | * Get the checksum for this file |
||||||
760 | * |
||||||
761 | * @return string|null |
||||||
762 | */ |
||||||
763 | public function getChecksum() { |
||||||
764 | return $this->info->getChecksum(); |
||||||
765 | } |
||||||
766 | |||||||
767 | public function setChecksum(string $checksum) { |
||||||
768 | $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]); |
||||||
769 | $this->refreshInfo(); |
||||||
770 | } |
||||||
771 | |||||||
772 | protected function header($string) { |
||||||
773 | if (!\OC::$CLI) { |
||||||
774 | \header($string); |
||||||
775 | } |
||||||
776 | } |
||||||
777 | |||||||
778 | public function hash(string $type) { |
||||||
779 | return $this->fileView->hash($type, $this->path); |
||||||
780 | } |
||||||
781 | |||||||
782 | public function getNode(): \OCP\Files\File { |
||||||
783 | return $this->node; |
||||||
0 ignored issues
–
show
|
|||||||
784 | } |
||||||
785 | |||||||
786 | public function getMetadata(string $group): FileMetadata { |
||||||
787 | return $this->metadata[$group]; |
||||||
788 | } |
||||||
789 | |||||||
790 | public function setMetadata(string $group, FileMetadata $metadata): void { |
||||||
791 | $this->metadata[$group] = $metadata; |
||||||
792 | } |
||||||
793 | |||||||
794 | public function hasMetadata(string $group) { |
||||||
795 | return array_key_exists($group, $this->metadata); |
||||||
796 | } |
||||||
797 | } |
||||||
798 |
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.