Passed
Push — master ( 514058...47cc5a )
by John
16:30 queued 03:00
created

File::emitPostHooks()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 11
nc 4
nop 2
dl 0
loc 16
rs 9.9
c 1
b 0
f 0
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 OCA\DAV\AppInfo\Application;
47
use OCA\DAV\Connector\Sabre\Exception\EntityTooLarge;
48
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
49
use OCA\DAV\Connector\Sabre\Exception\Forbidden as DAVForbiddenException;
50
use OCA\DAV\Connector\Sabre\Exception\UnsupportedMediaType;
51
use OCA\DAV\Connector\Sabre\Exception\BadGateway;
52
use OCP\Encryption\Exceptions\GenericEncryptionException;
53
use OCP\Files\EntityTooLargeException;
54
use OCP\Files\FileInfo;
55
use OCP\Files\ForbiddenException;
56
use OCP\Files\GenericFileException;
57
use OCP\Files\InvalidContentException;
58
use OCP\Files\InvalidPathException;
59
use OCP\Files\LockNotAcquiredException;
60
use OCP\Files\NotFoundException;
61
use OCP\Files\NotPermittedException;
62
use OCP\Files\Storage;
63
use OCP\Files\StorageNotAvailableException;
64
use OCP\IL10N;
65
use OCP\ILogger;
66
use OCP\L10N\IFactory as IL10NFactory;
67
use OCP\Lock\ILockingProvider;
68
use OCP\Lock\LockedException;
69
use OCP\Share\IManager;
70
use Sabre\DAV\Exception;
71
use Sabre\DAV\Exception\BadRequest;
72
use Sabre\DAV\Exception\Forbidden;
73
use Sabre\DAV\Exception\NotFound;
74
use Sabre\DAV\Exception\NotImplemented;
75
use Sabre\DAV\Exception\ServiceUnavailable;
76
use Sabre\DAV\IFile;
77
78
class File extends Node implements IFile {
79
	protected $request;
80
81
	protected IL10N $l10n;
82
83
	/**
84
	 * Sets up the node, expects a full path name
85
	 *
86
	 * @param \OC\Files\View $view
87
	 * @param \OCP\Files\FileInfo $info
88
	 * @param \OCP\Share\IManager $shareManager
89
	 * @param \OC\AppFramework\Http\Request $request
90
	 */
91
	public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
92
		parent::__construct($view, $info, $shareManager);
93
94
		// Querying IL10N directly results in a dependency loop
95
		/** @var IL10NFactory $l10nFactory */
96
		$l10nFactory = \OC::$server->get(IL10NFactory::class);
97
		$this->l10n = $l10nFactory->get(Application::APP_ID);
98
99
		if (isset($request)) {
100
			$this->request = $request;
101
		} else {
102
			$this->request = \OC::$server->getRequest();
103
		}
104
	}
105
106
	/**
107
	 * Updates the data
108
	 *
109
	 * The data argument is a readable stream resource.
110
	 *
111
	 * After a successful put operation, you may choose to return an ETag. The
112
	 * etag must always be surrounded by double-quotes. These quotes must
113
	 * appear in the actual string you're returning.
114
	 *
115
	 * Clients may use the ETag from a PUT request to later on make sure that
116
	 * when they update the file, the contents haven't changed in the mean
117
	 * time.
118
	 *
119
	 * If you don't plan to store the file byte-by-byte, and you return a
120
	 * different object on a subsequent GET you are strongly recommended to not
121
	 * return an ETag, and just return null.
122
	 *
123
	 * @param resource $data
124
	 *
125
	 * @throws Forbidden
126
	 * @throws UnsupportedMediaType
127
	 * @throws BadRequest
128
	 * @throws Exception
129
	 * @throws EntityTooLarge
130
	 * @throws ServiceUnavailable
131
	 * @throws FileLocked
132
	 * @return string|null
133
	 */
134
	public function put($data) {
135
		try {
136
			$exists = $this->fileView->file_exists($this->path);
137
			if ($this->info && $exists && !$this->info->isUpdateable()) {
138
				throw new Forbidden();
139
			}
140
		} catch (StorageNotAvailableException $e) {
141
			throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
142
		}
143
144
		// verify path of the target
145
		$this->verifyPath();
146
147
		// chunked handling
148
		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
149
			try {
150
				return $this->createFileChunked($data);
151
			} catch (\Exception $e) {
152
				$this->convertToSabreException($e);
153
			}
154
		}
155
156
		/** @var Storage $partStorage */
157
		[$partStorage] = $this->fileView->resolvePath($this->path);
158
		$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
159
160
		$view = \OC\Files\Filesystem::getView();
161
162
		if ($needsPartFile) {
163
			// mark file as partial while uploading (ignored by the scanner)
164
			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
165
166
			if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
167
				$needsPartFile = false;
168
			}
169
		}
170
		if (!$needsPartFile) {
171
			// upload file directly as the final path
172
			$partFilePath = $this->path;
173
174
			if ($view && !$this->emitPreHooks($exists)) {
175
				throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
176
			}
177
		}
178
179
		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
180
		/** @var \OC\Files\Storage\Storage $partStorage */
181
		[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $partFilePath does not seem to be defined for all execution paths leading up to this point.
Loading history...
182
		/** @var \OC\Files\Storage\Storage $storage */
183
		[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
184
		try {
185
			if (!$needsPartFile) {
186
				try {
187
					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
188
				} catch (LockedException $e) {
189
					// during very large uploads, the shared lock we got at the start might have been expired
190
					// meaning that the above lock can fail not just only because somebody else got a shared lock
191
					// or because there is no existing shared lock to make exclusive
192
					//
193
					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
194
					// lock this will still fail, if our original shared lock expired the new lock will be successful and
195
					// the entire operation will be safe
196
197
					try {
198
						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
199
					} catch (LockedException $ex) {
200
						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
201
					}
202
				}
203
			}
204
205
			if (!is_resource($data)) {
206
				$tmpData = fopen('php://temp', 'r+');
207
				if ($data !== null) {
208
					fwrite($tmpData, $data);
209
					rewind($tmpData);
210
				}
211
				$data = $tmpData;
212
			}
213
214
			$data = HashWrapper::wrap($data, 'md5', function ($hash) {
215
				$this->header('X-Hash-MD5: ' . $hash);
216
			});
217
			$data = HashWrapper::wrap($data, 'sha1', function ($hash) {
218
				$this->header('X-Hash-SHA1: ' . $hash);
219
			});
220
			$data = HashWrapper::wrap($data, 'sha256', function ($hash) {
221
				$this->header('X-Hash-SHA256: ' . $hash);
222
			});
223
224
			if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
225
				$isEOF = false;
226
				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF) {
227
					$isEOF = feof($stream);
228
				});
229
230
				$result = true;
231
				$count = -1;
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
232
				try {
233
					$count = $partStorage->writeStream($internalPartPath, $wrappedData);
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

233
					/** @scrutinizer ignore-call */ 
234
     $count = $partStorage->writeStream($internalPartPath, $wrappedData);
Loading history...
234
				} catch (GenericFileException $e) {
235
					$result = false;
236
				} catch (BadGateway $e) {
237
					throw $e;
238
				}
239
240
241
				if ($result === false) {
242
					$result = $isEOF;
243
					if (is_resource($wrappedData)) {
244
						$result = feof($wrappedData);
245
					}
246
				}
247
			} else {
248
				$target = $partStorage->fopen($internalPartPath, 'wb');
249
				if ($target === false) {
250
					\OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
251
					// because we have no clue about the cause we can only throw back a 500/Internal Server Error
252
					throw new Exception($this->l10n->t('Could not write file contents'));
253
				}
254
				[$count, $result] = \OC_Helper::streamCopy($data, $target);
255
				fclose($target);
256
			}
257
258
			if ($result === false) {
259
				$expected = -1;
260
				if (isset($_SERVER['CONTENT_LENGTH'])) {
261
					$expected = $_SERVER['CONTENT_LENGTH'];
262
				}
263
				if ($expected !== "0") {
264
					throw new Exception(
265
						$this->l10n->t(
266
							'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
267
							[
268
								$this->l10n->n('%n byte', '%n bytes', $count),
269
								$this->l10n->n('%n byte', '%n bytes', $expected),
270
							],
271
						)
272
					);
273
				}
274
			}
275
276
			// if content length is sent by client:
277
			// double check if the file was fully received
278
			// compare expected and actual size
279
			if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
280
				$expected = (int)$_SERVER['CONTENT_LENGTH'];
281
				if ($count !== $expected) {
282
					throw new BadRequest(
283
						$this->l10n->t(
284
							'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.',
285
							[
286
								$this->l10n->n('%n byte', '%n bytes', $expected),
287
								$this->l10n->n('%n byte', '%n bytes', $count),
288
							],
289
						)
290
					);
291
				}
292
			}
293
		} catch (\Exception $e) {
294
			$context = [];
295
296
			if ($e instanceof LockedException) {
297
				$context['level'] = ILogger::DEBUG;
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::DEBUG has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

297
				$context['level'] = /** @scrutinizer ignore-deprecated */ ILogger::DEBUG;

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
298
			}
299
300
			\OC::$server->getLogger()->logException($e, $context);
301
			if ($needsPartFile) {
302
				$partStorage->unlink($internalPartPath);
303
			}
304
			$this->convertToSabreException($e);
305
		}
306
307
		try {
308
			if ($needsPartFile) {
309
				if ($view && !$this->emitPreHooks($exists)) {
310
					$partStorage->unlink($internalPartPath);
311
					throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
312
				}
313
				try {
314
					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
315
				} catch (LockedException $e) {
316
					// during very large uploads, the shared lock we got at the start might have been expired
317
					// meaning that the above lock can fail not just only because somebody else got a shared lock
318
					// or because there is no existing shared lock to make exclusive
319
					//
320
					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
321
					// lock this will still fail, if our original shared lock expired the new lock will be successful and
322
					// the entire operation will be safe
323
324
					try {
325
						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
326
					} catch (LockedException $ex) {
327
						if ($needsPartFile) {
0 ignored issues
show
introduced by
The condition $needsPartFile is always true.
Loading history...
328
							$partStorage->unlink($internalPartPath);
329
						}
330
						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
331
					}
332
				}
333
334
				// rename to correct path
335
				try {
336
					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
337
					$fileExists = $storage->file_exists($internalPath);
338
					if ($renameOkay === false || $fileExists === false) {
339
						\OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
340
						throw new Exception($this->l10n->t('Could not rename part file to final file'));
341
					}
342
				} catch (ForbiddenException $ex) {
343
					if (!$ex->getRetry()) {
344
						$partStorage->unlink($internalPartPath);
345
					}
346
					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
347
				} catch (\Exception $e) {
348
					$partStorage->unlink($internalPartPath);
349
					$this->convertToSabreException($e);
350
				}
351
			}
352
353
			// since we skipped the view we need to scan and emit the hooks ourselves
354
			$storage->getUpdater()->update($internalPath);
355
356
			try {
357
				$this->changeLock(ILockingProvider::LOCK_SHARED);
358
			} catch (LockedException $e) {
359
				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
360
			}
361
362
			// allow sync clients to send the mtime along in a header
363
			if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
364
				$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
365
				if ($this->fileView->touch($this->path, $mtime)) {
366
					$this->header('X-OC-MTime: accepted');
367
				}
368
			}
369
370
			$fileInfoUpdate = [
371
				'upload_time' => time()
372
			];
373
374
			// allow sync clients to send the creation time along in a header
375
			if (isset($this->request->server['HTTP_X_OC_CTIME'])) {
376
				$ctime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_CTIME']);
377
				$fileInfoUpdate['creation_time'] = $ctime;
378
				$this->header('X-OC-CTime: accepted');
379
			}
380
381
			$this->fileView->putFileInfo($this->path, $fileInfoUpdate);
382
383
			if ($view) {
0 ignored issues
show
introduced by
$view is of type OC\Files\View, thus it always evaluated to true.
Loading history...
384
				$this->emitPostHooks($exists);
385
			}
386
387
			$this->refreshInfo();
388
389
			if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
390
				$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
391
				$this->setChecksum($checksum);
392
			} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
393
				$this->setChecksum('');
394
			}
395
		} catch (StorageNotAvailableException $e) {
396
			throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
397
		}
398
399
		return '"' . $this->info->getEtag() . '"';
400
	}
401
402
	private function getPartFileBasePath($path) {
403
		$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
404
		if ($partFileInStorage) {
405
			return $path;
406
		} else {
407
			return md5($path); // will place it in the root of the view with a unique name
408
		}
409
	}
410
411
	/**
412
	 * @param string $path
413
	 */
414
	private function emitPreHooks($exists, $path = null) {
415
		if (is_null($path)) {
416
			$path = $this->path;
417
		}
418
		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
419
		$run = true;
420
421
		if (!$exists) {
422
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, [
423
				\OC\Files\Filesystem::signal_param_path => $hookPath,
424
				\OC\Files\Filesystem::signal_param_run => &$run,
425
			]);
426
		} else {
427
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, [
428
				\OC\Files\Filesystem::signal_param_path => $hookPath,
429
				\OC\Files\Filesystem::signal_param_run => &$run,
430
			]);
431
		}
432
		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, [
433
			\OC\Files\Filesystem::signal_param_path => $hookPath,
434
			\OC\Files\Filesystem::signal_param_run => &$run,
435
		]);
436
		return $run;
437
	}
438
439
	/**
440
	 * @param string $path
441
	 */
442
	private function emitPostHooks($exists, $path = null) {
443
		if (is_null($path)) {
444
			$path = $this->path;
445
		}
446
		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
447
		if (!$exists) {
448
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, [
449
				\OC\Files\Filesystem::signal_param_path => $hookPath
450
			]);
451
		} else {
452
			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, [
453
				\OC\Files\Filesystem::signal_param_path => $hookPath
454
			]);
455
		}
456
		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, [
457
			\OC\Files\Filesystem::signal_param_path => $hookPath
458
		]);
459
	}
460
461
	/**
462
	 * Returns the data
463
	 *
464
	 * @return resource
465
	 * @throws Forbidden
466
	 * @throws ServiceUnavailable
467
	 */
468
	public function get() {
469
		//throw exception if encryption is disabled but files are still encrypted
470
		try {
471
			if (!$this->info->isReadable()) {
472
				// do a if the file did not exist
473
				throw new NotFound();
474
			}
475
			try {
476
				$res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
477
			} catch (\Exception $e) {
478
				$this->convertToSabreException($e);
479
			}
480
			if ($res === false) {
0 ignored issues
show
introduced by
The condition $res === false is always false.
Loading history...
481
				throw new ServiceUnavailable($this->l10n->t('Could not open file'));
482
			}
483
			return $res;
484
		} catch (GenericEncryptionException $e) {
485
			// returning 503 will allow retry of the operation at a later point in time
486
			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
487
		} catch (StorageNotAvailableException $e) {
488
			throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
489
		} catch (ForbiddenException $ex) {
490
			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
491
		} catch (LockedException $e) {
492
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
493
		}
494
	}
495
496
	/**
497
	 * Delete the current file
498
	 *
499
	 * @throws Forbidden
500
	 * @throws ServiceUnavailable
501
	 */
502
	public function delete() {
503
		if (!$this->info->isDeletable()) {
504
			throw new Forbidden();
505
		}
506
507
		try {
508
			if (!$this->fileView->unlink($this->path)) {
509
				// assume it wasn't possible to delete due to permissions
510
				throw new Forbidden();
511
			}
512
		} catch (StorageNotAvailableException $e) {
513
			throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %1$s', [$e->getMessage()]));
514
		} catch (ForbiddenException $ex) {
515
			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
516
		} catch (LockedException $e) {
517
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
518
		}
519
	}
520
521
	/**
522
	 * Returns the mime-type for a file
523
	 *
524
	 * If null is returned, we'll assume application/octet-stream
525
	 *
526
	 * @return string
527
	 */
528
	public function getContentType() {
529
		$mimeType = $this->info->getMimetype();
530
531
		// PROPFIND needs to return the correct mime type, for consistency with the web UI
532
		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
533
			return $mimeType;
534
		}
535
		return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
536
	}
537
538
	/**
539
	 * @return array|bool
540
	 */
541
	public function getDirectDownload() {
542
		if (\OCP\App::isEnabled('encryption')) {
543
			return [];
544
		}
545
		/** @var \OCP\Files\Storage $storage */
546
		[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
547
		if (is_null($storage)) {
548
			return [];
549
		}
550
551
		return $storage->getDirectDownload($internalPath);
552
	}
553
554
	/**
555
	 * @param resource $data
556
	 * @return null|string
557
	 * @throws Exception
558
	 * @throws BadRequest
559
	 * @throws NotImplemented
560
	 * @throws ServiceUnavailable
561
	 */
562
	private function createFileChunked($data) {
563
		[$path, $name] = \Sabre\Uri\split($this->path);
564
565
		$info = \OC_FileChunking::decodeName($name);
566
		if (empty($info)) {
567
			throw new NotImplemented($this->l10n->t('Invalid chunk name'));
568
		}
569
570
		$chunk_handler = new \OC_FileChunking($info);
571
		$bytesWritten = $chunk_handler->store($info['index'], $data);
572
573
		//detect aborted upload
574
		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
575
			if (isset($_SERVER['CONTENT_LENGTH'])) {
576
				$expected = (int)$_SERVER['CONTENT_LENGTH'];
577
				if ($bytesWritten !== $expected) {
578
					$chunk_handler->remove($info['index']);
579
					throw new BadRequest(
580
						$this->l10n->t(
581
							'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.',
582
							[
583
								$this->l10n->n('%n byte', '%n bytes', $expected),
584
								$this->l10n->n('%n byte', '%n bytes', $bytesWritten),
585
							],
586
						)
587
					);
588
				}
589
			}
590
		}
591
592
		if ($chunk_handler->isComplete()) {
593
			/** @var Storage $storage */
594
			[$storage,] = $this->fileView->resolvePath($path);
595
			$needsPartFile = $storage->needsPartFile();
596
			$partFile = null;
597
598
			$targetPath = $path . '/' . $info['name'];
599
			/** @var \OC\Files\Storage\Storage $targetStorage */
600
			[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
601
602
			$exists = $this->fileView->file_exists($targetPath);
603
604
			try {
605
				$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
606
607
				$this->emitPreHooks($exists, $targetPath);
608
				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
609
				/** @var \OC\Files\Storage\Storage $targetStorage */
610
				[$targetStorage, $targetInternalPath] = $this->fileView->resolvePath($targetPath);
611
612
				if ($needsPartFile) {
613
					// we first assembly the target file as a part file
614
					$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
615
					/** @var \OC\Files\Storage\Storage $targetStorage */
616
					[$partStorage, $partInternalPath] = $this->fileView->resolvePath($partFile);
617
618
619
					$chunk_handler->file_assemble($partStorage, $partInternalPath);
620
621
					// here is the final atomic rename
622
					$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
623
					$fileExists = $targetStorage->file_exists($targetInternalPath);
624
					if ($renameOkay === false || $fileExists === false) {
625
						\OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
626
						// only delete if an error occurred and the target file was already created
627
						if ($fileExists) {
628
							// set to null to avoid double-deletion when handling exception
629
							// stray part file
630
							$partFile = null;
631
							$targetStorage->unlink($targetInternalPath);
632
						}
633
						$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
634
						throw new Exception($this->l10n->t('Could not rename part file assembled from chunks'));
635
					}
636
				} else {
637
					// assemble directly into the final file
638
					$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
639
				}
640
641
				// allow sync clients to send the mtime along in a header
642
				if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
643
					$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
644
					if ($targetStorage->touch($targetInternalPath, $mtime)) {
645
						$this->header('X-OC-MTime: accepted');
646
					}
647
				}
648
649
				// since we skipped the view we need to scan and emit the hooks ourselves
650
				$targetStorage->getUpdater()->update($targetInternalPath);
651
652
				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
653
654
				$this->emitPostHooks($exists, $targetPath);
655
656
				// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
657
				$info = $this->fileView->getFileInfo($targetPath);
658
659
				if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
660
					$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
661
					$this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
662
				} elseif ($info->getChecksum() !== null && $info->getChecksum() !== '') {
663
					$this->fileView->putFileInfo($this->path, ['checksum' => '']);
664
				}
665
666
				$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
667
668
				return $info->getEtag();
669
			} catch (\Exception $e) {
670
				if ($partFile !== null) {
671
					$targetStorage->unlink($targetInternalPath);
672
				}
673
				$this->convertToSabreException($e);
674
			}
675
		}
676
677
		return null;
678
	}
679
680
	/**
681
	 * Convert the given exception to a SabreException instance
682
	 *
683
	 * @param \Exception $e
684
	 *
685
	 * @throws \Sabre\DAV\Exception
686
	 */
687
	private function convertToSabreException(\Exception $e) {
688
		if ($e instanceof \Sabre\DAV\Exception) {
689
			throw $e;
690
		}
691
		if ($e instanceof NotPermittedException) {
692
			// a more general case - due to whatever reason the content could not be written
693
			throw new Forbidden($e->getMessage(), 0, $e);
694
		}
695
		if ($e instanceof ForbiddenException) {
696
			// the path for the file was forbidden
697
			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
698
		}
699
		if ($e instanceof EntityTooLargeException) {
700
			// the file is too big to be stored
701
			throw new EntityTooLarge($e->getMessage(), 0, $e);
702
		}
703
		if ($e instanceof InvalidContentException) {
704
			// the file content is not permitted
705
			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
706
		}
707
		if ($e instanceof InvalidPathException) {
708
			// the path for the file was not valid
709
			// TODO: find proper http status code for this case
710
			throw new Forbidden($e->getMessage(), 0, $e);
711
		}
712
		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
713
			// the file is currently being written to by another process
714
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
715
		}
716
		if ($e instanceof GenericEncryptionException) {
717
			// returning 503 will allow retry of the operation at a later point in time
718
			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
719
		}
720
		if ($e instanceof StorageNotAvailableException) {
721
			throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
722
		}
723
		if ($e instanceof NotFoundException) {
724
			throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
725
		}
726
727
		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
728
	}
729
730
	/**
731
	 * Get the checksum for this file
732
	 *
733
	 * @return string|null
734
	 */
735
	public function getChecksum() {
736
		if (!$this->info) {
737
			return null;
738
		}
739
		return $this->info->getChecksum();
740
	}
741
742
	public function setChecksum(string $checksum) {
743
		$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
744
		$this->refreshInfo();
745
	}
746
747
	protected function header($string) {
748
		if (!\OC::$CLI) {
749
			\header($string);
750
		}
751
	}
752
753
	public function hash(string $type) {
754
		return $this->fileView->hash($type, $this->path);
755
	}
756
}
757