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)) { |
||
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); |
||
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; |
||
0 ignored issues
–
show
Unused Code
introduced
by
Loading history...
|
|||
247 | try { |
||
248 | $count = $partStorage->writeStream($internalPartPath, $wrappedData); |
||
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) { |
||
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; |
||
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 |