Completed
Push — master ( 910687...35930a )
by Robin
37:15 queued 58s
created
apps/dav/lib/Connector/Sabre/File.php 2 patches
Indentation   +581 added lines, -581 removed lines patch added patch discarded remove patch
@@ -49,585 +49,585 @@
 block discarded – undo
49 49
 use Sabre\DAV\IFile;
50 50
 
51 51
 class File extends Node implements IFile {
52
-	protected IRequest $request;
53
-	protected IL10N $l10n;
54
-
55
-	/**
56
-	 * Sets up the node, expects a full path name
57
-	 *
58
-	 * @param View $view
59
-	 * @param FileInfo $info
60
-	 * @param ?\OCP\Share\IManager $shareManager
61
-	 * @param ?IRequest $request
62
-	 * @param ?IL10N $l10n
63
-	 */
64
-	public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
65
-		parent::__construct($view, $info, $shareManager);
66
-
67
-		if ($l10n) {
68
-			$this->l10n = $l10n;
69
-		} else {
70
-			// Querying IL10N directly results in a dependency loop
71
-			/** @var IL10NFactory $l10nFactory */
72
-			$l10nFactory = Server::get(IL10NFactory::class);
73
-			$this->l10n = $l10nFactory->get(Application::APP_ID);
74
-		}
75
-
76
-		if (isset($request)) {
77
-			$this->request = $request;
78
-		} else {
79
-			$this->request = Server::get(IRequest::class);
80
-		}
81
-	}
82
-
83
-	/**
84
-	 * Updates the data
85
-	 *
86
-	 * The data argument is a readable stream resource.
87
-	 *
88
-	 * After a successful put operation, you may choose to return an ETag. The
89
-	 * etag must always be surrounded by double-quotes. These quotes must
90
-	 * appear in the actual string you're returning.
91
-	 *
92
-	 * Clients may use the ETag from a PUT request to later on make sure that
93
-	 * when they update the file, the contents haven't changed in the mean
94
-	 * time.
95
-	 *
96
-	 * If you don't plan to store the file byte-by-byte, and you return a
97
-	 * different object on a subsequent GET you are strongly recommended to not
98
-	 * return an ETag, and just return null.
99
-	 *
100
-	 * @param resource|string $data
101
-	 *
102
-	 * @throws Forbidden
103
-	 * @throws UnsupportedMediaType
104
-	 * @throws BadRequest
105
-	 * @throws Exception
106
-	 * @throws EntityTooLarge
107
-	 * @throws ServiceUnavailable
108
-	 * @throws FileLocked
109
-	 * @return string|null
110
-	 */
111
-	public function put($data) {
112
-		try {
113
-			$exists = $this->fileView->file_exists($this->path);
114
-			if ($exists && !$this->info->isUpdateable()) {
115
-				throw new Forbidden();
116
-			}
117
-		} catch (StorageNotAvailableException $e) {
118
-			throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
119
-		}
120
-
121
-		// verify path of the target
122
-		$this->verifyPath();
123
-
124
-		[$partStorage] = $this->fileView->resolvePath($this->path);
125
-		if ($partStorage === null) {
126
-			throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
127
-		}
128
-		$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
129
-
130
-		$view = Filesystem::getView();
131
-
132
-		if ($needsPartFile) {
133
-			$transferId = \rand();
134
-			// mark file as partial while uploading (ignored by the scanner)
135
-			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
136
-
137
-			if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
138
-				$needsPartFile = false;
139
-			}
140
-		}
141
-		if (!$needsPartFile) {
142
-			// upload file directly as the final path
143
-			$partFilePath = $this->path;
144
-
145
-			if ($view && !$this->emitPreHooks($exists)) {
146
-				throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
147
-			}
148
-		}
149
-
150
-		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
151
-		[$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
152
-		[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
153
-		if ($partStorage === null || $storage === null) {
154
-			throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
155
-		}
156
-		try {
157
-			if (!$needsPartFile) {
158
-				try {
159
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
160
-				} catch (LockedException $e) {
161
-					// during very large uploads, the shared lock we got at the start might have been expired
162
-					// meaning that the above lock can fail not just only because somebody else got a shared lock
163
-					// or because there is no existing shared lock to make exclusive
164
-					//
165
-					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
166
-					// lock this will still fail, if our original shared lock expired the new lock will be successful and
167
-					// the entire operation will be safe
168
-
169
-					try {
170
-						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
171
-					} catch (LockedException $ex) {
172
-						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
173
-					}
174
-				}
175
-			}
176
-
177
-			if (!is_resource($data)) {
178
-				$tmpData = fopen('php://temp', 'r+');
179
-				if ($data !== null) {
180
-					fwrite($tmpData, $data);
181
-					rewind($tmpData);
182
-				}
183
-				$data = $tmpData;
184
-			}
185
-
186
-			if ($this->request->getHeader('X-HASH') !== '') {
187
-				$hash = $this->request->getHeader('X-HASH');
188
-				if ($hash === 'all' || $hash === 'md5') {
189
-					$data = HashWrapper::wrap($data, 'md5', function ($hash): void {
190
-						$this->header('X-Hash-MD5: ' . $hash);
191
-					});
192
-				}
193
-
194
-				if ($hash === 'all' || $hash === 'sha1') {
195
-					$data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
196
-						$this->header('X-Hash-SHA1: ' . $hash);
197
-					});
198
-				}
199
-
200
-				if ($hash === 'all' || $hash === 'sha256') {
201
-					$data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
202
-						$this->header('X-Hash-SHA256: ' . $hash);
203
-					});
204
-				}
205
-			}
206
-
207
-			if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
208
-				$isEOF = false;
209
-				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
210
-					$isEOF = feof($stream);
211
-				});
212
-
213
-				$result = is_resource($wrappedData);
214
-				if ($result) {
215
-					$count = -1;
216
-					try {
217
-						/** @var IWriteStreamStorage $partStorage */
218
-						$count = $partStorage->writeStream($internalPartPath, $wrappedData);
219
-					} catch (GenericFileException $e) {
220
-						$logger = Server::get(LoggerInterface::class);
221
-						$logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
222
-						$result = $isEOF;
223
-						if (is_resource($wrappedData)) {
224
-							$result = feof($wrappedData);
225
-						}
226
-					}
227
-				}
228
-			} else {
229
-				$target = $partStorage->fopen($internalPartPath, 'wb');
230
-				if ($target === false) {
231
-					Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
232
-					// because we have no clue about the cause we can only throw back a 500/Internal Server Error
233
-					throw new Exception($this->l10n->t('Could not write file contents'));
234
-				}
235
-				[$count, $result] = Files::streamCopy($data, $target, true);
236
-				fclose($target);
237
-			}
238
-
239
-			$lengthHeader = $this->request->getHeader('content-length');
240
-			$expected = $lengthHeader !== '' ? (int)$lengthHeader : -1;
241
-			if ($result === false && $expected >= 0) {
242
-				throw new Exception(
243
-					$this->l10n->t(
244
-						'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
245
-						[
246
-							$this->l10n->n('%n byte', '%n bytes', $count),
247
-							$this->l10n->n('%n byte', '%n bytes', $expected),
248
-						],
249
-					)
250
-				);
251
-			}
252
-
253
-			// if content length is sent by client:
254
-			// double check if the file was fully received
255
-			// compare expected and actual size
256
-			if ($expected >= 0
257
-				&& $expected !== $count
258
-				&& $this->request->getMethod() === 'PUT'
259
-			) {
260
-				throw new BadRequest(
261
-					$this->l10n->t(
262
-						'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.',
263
-						[
264
-							$this->l10n->n('%n byte', '%n bytes', $expected),
265
-							$this->l10n->n('%n byte', '%n bytes', $count),
266
-						],
267
-					)
268
-				);
269
-			}
270
-		} catch (\Exception $e) {
271
-			if ($e instanceof LockedException) {
272
-				Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
273
-			} else {
274
-				Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
275
-			}
276
-
277
-			if ($needsPartFile) {
278
-				$partStorage->unlink($internalPartPath);
279
-			}
280
-			$this->convertToSabreException($e);
281
-		}
282
-
283
-		try {
284
-			if ($needsPartFile) {
285
-				if ($view && !$this->emitPreHooks($exists)) {
286
-					$partStorage->unlink($internalPartPath);
287
-					throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
288
-				}
289
-				try {
290
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
291
-				} catch (LockedException $e) {
292
-					// during very large uploads, the shared lock we got at the start might have been expired
293
-					// meaning that the above lock can fail not just only because somebody else got a shared lock
294
-					// or because there is no existing shared lock to make exclusive
295
-					//
296
-					// Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
297
-					// lock this will still fail, if our original shared lock expired the new lock will be successful and
298
-					// the entire operation will be safe
299
-
300
-					try {
301
-						$this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
302
-					} catch (LockedException $ex) {
303
-						if ($needsPartFile) {
304
-							$partStorage->unlink($internalPartPath);
305
-						}
306
-						throw new FileLocked($e->getMessage(), $e->getCode(), $e);
307
-					}
308
-				}
309
-
310
-				// rename to correct path
311
-				try {
312
-					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
313
-					$fileExists = $storage->file_exists($internalPath);
314
-					if ($renameOkay === false || $fileExists === false) {
315
-						Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
316
-						throw new Exception($this->l10n->t('Could not rename part file to final file'));
317
-					}
318
-				} catch (ForbiddenException $ex) {
319
-					if (!$ex->getRetry()) {
320
-						$partStorage->unlink($internalPartPath);
321
-					}
322
-					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
323
-				} catch (\Exception $e) {
324
-					$partStorage->unlink($internalPartPath);
325
-					$this->convertToSabreException($e);
326
-				}
327
-			}
328
-
329
-			// since we skipped the view we need to scan and emit the hooks ourselves
330
-			$storage->getUpdater()->update($internalPath);
331
-
332
-			try {
333
-				$this->changeLock(ILockingProvider::LOCK_SHARED);
334
-			} catch (LockedException $e) {
335
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
336
-			}
337
-
338
-			// allow sync clients to send the mtime along in a header
339
-			$mtimeHeader = $this->request->getHeader('x-oc-mtime');
340
-			if ($mtimeHeader !== '') {
341
-				$mtime = $this->sanitizeMtime($mtimeHeader);
342
-				if ($this->fileView->touch($this->path, $mtime)) {
343
-					$this->header('X-OC-MTime: accepted');
344
-				}
345
-			}
346
-
347
-			$fileInfoUpdate = [
348
-				'upload_time' => time()
349
-			];
350
-
351
-			// allow sync clients to send the creation time along in a header
352
-			$ctimeHeader = $this->request->getHeader('x-oc-ctime');
353
-			if ($ctimeHeader) {
354
-				$ctime = $this->sanitizeMtime($ctimeHeader);
355
-				$fileInfoUpdate['creation_time'] = $ctime;
356
-				$this->header('X-OC-CTime: accepted');
357
-			}
358
-
359
-			$this->fileView->putFileInfo($this->path, $fileInfoUpdate);
360
-
361
-			if ($view) {
362
-				$this->emitPostHooks($exists);
363
-			}
364
-
365
-			$this->refreshInfo();
366
-
367
-			$checksumHeader = $this->request->getHeader('oc-checksum');
368
-			if ($checksumHeader) {
369
-				$checksum = trim($checksumHeader);
370
-				$this->setChecksum($checksum);
371
-			} elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
372
-				$this->setChecksum('');
373
-			}
374
-		} catch (StorageNotAvailableException $e) {
375
-			throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
376
-		}
377
-
378
-		return '"' . $this->info->getEtag() . '"';
379
-	}
380
-
381
-	private function getPartFileBasePath($path) {
382
-		$partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
383
-		if ($partFileInStorage) {
384
-			$filename = basename($path);
385
-			// hash does not need to be secure but fast and semi unique
386
-			$hashedFilename = hash('xxh128', $filename);
387
-			return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
388
-		} else {
389
-			// will place the .part file in the users root directory
390
-			// therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
391
-			return hash('xxh128', $path);
392
-		}
393
-	}
394
-
395
-	private function emitPreHooks(bool $exists, ?string $path = null): bool {
396
-		if (is_null($path)) {
397
-			$path = $this->path;
398
-		}
399
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
400
-		if ($hookPath === null) {
401
-			// We only trigger hooks from inside default view
402
-			return true;
403
-		}
404
-		$run = true;
405
-
406
-		if (!$exists) {
407
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
408
-				Filesystem::signal_param_path => $hookPath,
409
-				Filesystem::signal_param_run => &$run,
410
-			]);
411
-		} else {
412
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
413
-				Filesystem::signal_param_path => $hookPath,
414
-				Filesystem::signal_param_run => &$run,
415
-			]);
416
-		}
417
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
418
-			Filesystem::signal_param_path => $hookPath,
419
-			Filesystem::signal_param_run => &$run,
420
-		]);
421
-		return $run;
422
-	}
423
-
424
-	private function emitPostHooks(bool $exists, ?string $path = null): void {
425
-		if (is_null($path)) {
426
-			$path = $this->path;
427
-		}
428
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
429
-		if ($hookPath === null) {
430
-			// We only trigger hooks from inside default view
431
-			return;
432
-		}
433
-		if (!$exists) {
434
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
435
-				Filesystem::signal_param_path => $hookPath
436
-			]);
437
-		} else {
438
-			\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
439
-				Filesystem::signal_param_path => $hookPath
440
-			]);
441
-		}
442
-		\OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
443
-			Filesystem::signal_param_path => $hookPath
444
-		]);
445
-	}
446
-
447
-	/**
448
-	 * Returns the data
449
-	 *
450
-	 * @return resource
451
-	 * @throws Forbidden
452
-	 * @throws ServiceUnavailable
453
-	 */
454
-	public function get() {
455
-		//throw exception if encryption is disabled but files are still encrypted
456
-		try {
457
-			if (!$this->info->isReadable()) {
458
-				// do a if the file did not exist
459
-				throw new NotFound();
460
-			}
461
-			$path = ltrim($this->path, '/');
462
-			try {
463
-				$res = $this->fileView->fopen($path, 'rb');
464
-			} catch (\Exception $e) {
465
-				$this->convertToSabreException($e);
466
-			}
467
-
468
-			if ($res === false) {
469
-				if ($this->fileView->file_exists($path)) {
470
-					throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
471
-				} else {
472
-					throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
473
-				}
474
-			}
475
-
476
-			// comparing current file size with the one in DB
477
-			// if different, fix DB and refresh cache.
478
-			if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
479
-				$logger = Server::get(LoggerInterface::class);
480
-				$logger->warning('fixing cached size of file id=' . $this->getId());
481
-
482
-				$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
483
-				$this->refreshInfo();
484
-			}
485
-
486
-			return $res;
487
-		} catch (GenericEncryptionException $e) {
488
-			// returning 503 will allow retry of the operation at a later point in time
489
-			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
490
-		} catch (StorageNotAvailableException $e) {
491
-			throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
492
-		} catch (ForbiddenException $ex) {
493
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
494
-		} catch (LockedException $e) {
495
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
496
-		}
497
-	}
498
-
499
-	/**
500
-	 * Delete the current file
501
-	 *
502
-	 * @throws Forbidden
503
-	 * @throws ServiceUnavailable
504
-	 */
505
-	public function delete() {
506
-		if (!$this->info->isDeletable()) {
507
-			throw new Forbidden();
508
-		}
509
-
510
-		try {
511
-			if (!$this->fileView->unlink($this->path)) {
512
-				// assume it wasn't possible to delete due to permissions
513
-				throw new Forbidden();
514
-			}
515
-		} catch (StorageNotAvailableException $e) {
516
-			throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %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
-	 * Returns the mime-type for a file
526
-	 *
527
-	 * If null is returned, we'll assume application/octet-stream
528
-	 *
529
-	 * @return string
530
-	 */
531
-	public function getContentType() {
532
-		$mimeType = $this->info->getMimetype();
533
-
534
-		// PROPFIND needs to return the correct mime type, for consistency with the web UI
535
-		if ($this->request->getMethod() === 'PROPFIND') {
536
-			return $mimeType;
537
-		}
538
-		return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
539
-	}
540
-
541
-	/**
542
-	 * @return array|bool
543
-	 */
544
-	public function getDirectDownload() {
545
-		if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
546
-			return [];
547
-		}
548
-		[$storage, $internalPath] = $this->fileView->resolvePath($this->path);
549
-		if (is_null($storage)) {
550
-			return [];
551
-		}
552
-
553
-		return $storage->getDirectDownload($internalPath);
554
-	}
555
-
556
-	/**
557
-	 * Convert the given exception to a SabreException instance
558
-	 *
559
-	 * @param \Exception $e
560
-	 *
561
-	 * @throws \Sabre\DAV\Exception
562
-	 */
563
-	private function convertToSabreException(\Exception $e) {
564
-		if ($e instanceof \Sabre\DAV\Exception) {
565
-			throw $e;
566
-		}
567
-		if ($e instanceof NotPermittedException) {
568
-			// a more general case - due to whatever reason the content could not be written
569
-			throw new Forbidden($e->getMessage(), 0, $e);
570
-		}
571
-		if ($e instanceof ForbiddenException) {
572
-			// the path for the file was forbidden
573
-			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
574
-		}
575
-		if ($e instanceof EntityTooLargeException) {
576
-			// the file is too big to be stored
577
-			throw new EntityTooLarge($e->getMessage(), 0, $e);
578
-		}
579
-		if ($e instanceof InvalidContentException) {
580
-			// the file content is not permitted
581
-			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
582
-		}
583
-		if ($e instanceof InvalidPathException) {
584
-			// the path for the file was not valid
585
-			// TODO: find proper http status code for this case
586
-			throw new Forbidden($e->getMessage(), 0, $e);
587
-		}
588
-		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
589
-			// the file is currently being written to by another process
590
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
591
-		}
592
-		if ($e instanceof GenericEncryptionException) {
593
-			// returning 503 will allow retry of the operation at a later point in time
594
-			throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
595
-		}
596
-		if ($e instanceof StorageNotAvailableException) {
597
-			throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
598
-		}
599
-		if ($e instanceof NotFoundException) {
600
-			throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
601
-		}
602
-
603
-		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
604
-	}
605
-
606
-	/**
607
-	 * Get the checksum for this file
608
-	 *
609
-	 * @return string|null
610
-	 */
611
-	public function getChecksum() {
612
-		return $this->info->getChecksum();
613
-	}
614
-
615
-	public function setChecksum(string $checksum) {
616
-		$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
617
-		$this->refreshInfo();
618
-	}
619
-
620
-	protected function header($string) {
621
-		if (!\OC::$CLI) {
622
-			\header($string);
623
-		}
624
-	}
625
-
626
-	public function hash(string $type) {
627
-		return $this->fileView->hash($type, $this->path);
628
-	}
629
-
630
-	public function getNode(): \OCP\Files\File {
631
-		return $this->node;
632
-	}
52
+    protected IRequest $request;
53
+    protected IL10N $l10n;
54
+
55
+    /**
56
+     * Sets up the node, expects a full path name
57
+     *
58
+     * @param View $view
59
+     * @param FileInfo $info
60
+     * @param ?\OCP\Share\IManager $shareManager
61
+     * @param ?IRequest $request
62
+     * @param ?IL10N $l10n
63
+     */
64
+    public function __construct(View $view, FileInfo $info, ?IManager $shareManager = null, ?IRequest $request = null, ?IL10N $l10n = null) {
65
+        parent::__construct($view, $info, $shareManager);
66
+
67
+        if ($l10n) {
68
+            $this->l10n = $l10n;
69
+        } else {
70
+            // Querying IL10N directly results in a dependency loop
71
+            /** @var IL10NFactory $l10nFactory */
72
+            $l10nFactory = Server::get(IL10NFactory::class);
73
+            $this->l10n = $l10nFactory->get(Application::APP_ID);
74
+        }
75
+
76
+        if (isset($request)) {
77
+            $this->request = $request;
78
+        } else {
79
+            $this->request = Server::get(IRequest::class);
80
+        }
81
+    }
82
+
83
+    /**
84
+     * Updates the data
85
+     *
86
+     * The data argument is a readable stream resource.
87
+     *
88
+     * After a successful put operation, you may choose to return an ETag. The
89
+     * etag must always be surrounded by double-quotes. These quotes must
90
+     * appear in the actual string you're returning.
91
+     *
92
+     * Clients may use the ETag from a PUT request to later on make sure that
93
+     * when they update the file, the contents haven't changed in the mean
94
+     * time.
95
+     *
96
+     * If you don't plan to store the file byte-by-byte, and you return a
97
+     * different object on a subsequent GET you are strongly recommended to not
98
+     * return an ETag, and just return null.
99
+     *
100
+     * @param resource|string $data
101
+     *
102
+     * @throws Forbidden
103
+     * @throws UnsupportedMediaType
104
+     * @throws BadRequest
105
+     * @throws Exception
106
+     * @throws EntityTooLarge
107
+     * @throws ServiceUnavailable
108
+     * @throws FileLocked
109
+     * @return string|null
110
+     */
111
+    public function put($data) {
112
+        try {
113
+            $exists = $this->fileView->file_exists($this->path);
114
+            if ($exists && !$this->info->isUpdateable()) {
115
+                throw new Forbidden();
116
+            }
117
+        } catch (StorageNotAvailableException $e) {
118
+            throw new ServiceUnavailable($this->l10n->t('File is not updatable: %1$s', [$e->getMessage()]));
119
+        }
120
+
121
+        // verify path of the target
122
+        $this->verifyPath();
123
+
124
+        [$partStorage] = $this->fileView->resolvePath($this->path);
125
+        if ($partStorage === null) {
126
+            throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
127
+        }
128
+        $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
129
+
130
+        $view = Filesystem::getView();
131
+
132
+        if ($needsPartFile) {
133
+            $transferId = \rand();
134
+            // mark file as partial while uploading (ignored by the scanner)
135
+            $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
136
+
137
+            if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
138
+                $needsPartFile = false;
139
+            }
140
+        }
141
+        if (!$needsPartFile) {
142
+            // upload file directly as the final path
143
+            $partFilePath = $this->path;
144
+
145
+            if ($view && !$this->emitPreHooks($exists)) {
146
+                throw new Exception($this->l10n->t('Could not write to final file, canceled by hook'));
147
+            }
148
+        }
149
+
150
+        // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
151
+        [$partStorage, $internalPartPath] = $this->fileView->resolvePath($partFilePath);
152
+        [$storage, $internalPath] = $this->fileView->resolvePath($this->path);
153
+        if ($partStorage === null || $storage === null) {
154
+            throw new ServiceUnavailable($this->l10n->t('Failed to get storage for file'));
155
+        }
156
+        try {
157
+            if (!$needsPartFile) {
158
+                try {
159
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
160
+                } catch (LockedException $e) {
161
+                    // during very large uploads, the shared lock we got at the start might have been expired
162
+                    // meaning that the above lock can fail not just only because somebody else got a shared lock
163
+                    // or because there is no existing shared lock to make exclusive
164
+                    //
165
+                    // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
166
+                    // lock this will still fail, if our original shared lock expired the new lock will be successful and
167
+                    // the entire operation will be safe
168
+
169
+                    try {
170
+                        $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
171
+                    } catch (LockedException $ex) {
172
+                        throw new FileLocked($e->getMessage(), $e->getCode(), $e);
173
+                    }
174
+                }
175
+            }
176
+
177
+            if (!is_resource($data)) {
178
+                $tmpData = fopen('php://temp', 'r+');
179
+                if ($data !== null) {
180
+                    fwrite($tmpData, $data);
181
+                    rewind($tmpData);
182
+                }
183
+                $data = $tmpData;
184
+            }
185
+
186
+            if ($this->request->getHeader('X-HASH') !== '') {
187
+                $hash = $this->request->getHeader('X-HASH');
188
+                if ($hash === 'all' || $hash === 'md5') {
189
+                    $data = HashWrapper::wrap($data, 'md5', function ($hash): void {
190
+                        $this->header('X-Hash-MD5: ' . $hash);
191
+                    });
192
+                }
193
+
194
+                if ($hash === 'all' || $hash === 'sha1') {
195
+                    $data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
196
+                        $this->header('X-Hash-SHA1: ' . $hash);
197
+                    });
198
+                }
199
+
200
+                if ($hash === 'all' || $hash === 'sha256') {
201
+                    $data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
202
+                        $this->header('X-Hash-SHA256: ' . $hash);
203
+                    });
204
+                }
205
+            }
206
+
207
+            if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
208
+                $isEOF = false;
209
+                $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
210
+                    $isEOF = feof($stream);
211
+                });
212
+
213
+                $result = is_resource($wrappedData);
214
+                if ($result) {
215
+                    $count = -1;
216
+                    try {
217
+                        /** @var IWriteStreamStorage $partStorage */
218
+                        $count = $partStorage->writeStream($internalPartPath, $wrappedData);
219
+                    } catch (GenericFileException $e) {
220
+                        $logger = Server::get(LoggerInterface::class);
221
+                        $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
222
+                        $result = $isEOF;
223
+                        if (is_resource($wrappedData)) {
224
+                            $result = feof($wrappedData);
225
+                        }
226
+                    }
227
+                }
228
+            } else {
229
+                $target = $partStorage->fopen($internalPartPath, 'wb');
230
+                if ($target === false) {
231
+                    Server::get(LoggerInterface::class)->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
232
+                    // because we have no clue about the cause we can only throw back a 500/Internal Server Error
233
+                    throw new Exception($this->l10n->t('Could not write file contents'));
234
+                }
235
+                [$count, $result] = Files::streamCopy($data, $target, true);
236
+                fclose($target);
237
+            }
238
+
239
+            $lengthHeader = $this->request->getHeader('content-length');
240
+            $expected = $lengthHeader !== '' ? (int)$lengthHeader : -1;
241
+            if ($result === false && $expected >= 0) {
242
+                throw new Exception(
243
+                    $this->l10n->t(
244
+                        'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)',
245
+                        [
246
+                            $this->l10n->n('%n byte', '%n bytes', $count),
247
+                            $this->l10n->n('%n byte', '%n bytes', $expected),
248
+                        ],
249
+                    )
250
+                );
251
+            }
252
+
253
+            // if content length is sent by client:
254
+            // double check if the file was fully received
255
+            // compare expected and actual size
256
+            if ($expected >= 0
257
+                && $expected !== $count
258
+                && $this->request->getMethod() === 'PUT'
259
+            ) {
260
+                throw new BadRequest(
261
+                    $this->l10n->t(
262
+                        '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.',
263
+                        [
264
+                            $this->l10n->n('%n byte', '%n bytes', $expected),
265
+                            $this->l10n->n('%n byte', '%n bytes', $count),
266
+                        ],
267
+                    )
268
+                );
269
+            }
270
+        } catch (\Exception $e) {
271
+            if ($e instanceof LockedException) {
272
+                Server::get(LoggerInterface::class)->debug($e->getMessage(), ['exception' => $e]);
273
+            } else {
274
+                Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]);
275
+            }
276
+
277
+            if ($needsPartFile) {
278
+                $partStorage->unlink($internalPartPath);
279
+            }
280
+            $this->convertToSabreException($e);
281
+        }
282
+
283
+        try {
284
+            if ($needsPartFile) {
285
+                if ($view && !$this->emitPreHooks($exists)) {
286
+                    $partStorage->unlink($internalPartPath);
287
+                    throw new Exception($this->l10n->t('Could not rename part file to final file, canceled by hook'));
288
+                }
289
+                try {
290
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
291
+                } catch (LockedException $e) {
292
+                    // during very large uploads, the shared lock we got at the start might have been expired
293
+                    // meaning that the above lock can fail not just only because somebody else got a shared lock
294
+                    // or because there is no existing shared lock to make exclusive
295
+                    //
296
+                    // Thus we try to get a new exclusive lock, if the original lock failed because of a different shared
297
+                    // lock this will still fail, if our original shared lock expired the new lock will be successful and
298
+                    // the entire operation will be safe
299
+
300
+                    try {
301
+                        $this->acquireLock(ILockingProvider::LOCK_EXCLUSIVE);
302
+                    } catch (LockedException $ex) {
303
+                        if ($needsPartFile) {
304
+                            $partStorage->unlink($internalPartPath);
305
+                        }
306
+                        throw new FileLocked($e->getMessage(), $e->getCode(), $e);
307
+                    }
308
+                }
309
+
310
+                // rename to correct path
311
+                try {
312
+                    $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
313
+                    $fileExists = $storage->file_exists($internalPath);
314
+                    if ($renameOkay === false || $fileExists === false) {
315
+                        Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
316
+                        throw new Exception($this->l10n->t('Could not rename part file to final file'));
317
+                    }
318
+                } catch (ForbiddenException $ex) {
319
+                    if (!$ex->getRetry()) {
320
+                        $partStorage->unlink($internalPartPath);
321
+                    }
322
+                    throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
323
+                } catch (\Exception $e) {
324
+                    $partStorage->unlink($internalPartPath);
325
+                    $this->convertToSabreException($e);
326
+                }
327
+            }
328
+
329
+            // since we skipped the view we need to scan and emit the hooks ourselves
330
+            $storage->getUpdater()->update($internalPath);
331
+
332
+            try {
333
+                $this->changeLock(ILockingProvider::LOCK_SHARED);
334
+            } catch (LockedException $e) {
335
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
336
+            }
337
+
338
+            // allow sync clients to send the mtime along in a header
339
+            $mtimeHeader = $this->request->getHeader('x-oc-mtime');
340
+            if ($mtimeHeader !== '') {
341
+                $mtime = $this->sanitizeMtime($mtimeHeader);
342
+                if ($this->fileView->touch($this->path, $mtime)) {
343
+                    $this->header('X-OC-MTime: accepted');
344
+                }
345
+            }
346
+
347
+            $fileInfoUpdate = [
348
+                'upload_time' => time()
349
+            ];
350
+
351
+            // allow sync clients to send the creation time along in a header
352
+            $ctimeHeader = $this->request->getHeader('x-oc-ctime');
353
+            if ($ctimeHeader) {
354
+                $ctime = $this->sanitizeMtime($ctimeHeader);
355
+                $fileInfoUpdate['creation_time'] = $ctime;
356
+                $this->header('X-OC-CTime: accepted');
357
+            }
358
+
359
+            $this->fileView->putFileInfo($this->path, $fileInfoUpdate);
360
+
361
+            if ($view) {
362
+                $this->emitPostHooks($exists);
363
+            }
364
+
365
+            $this->refreshInfo();
366
+
367
+            $checksumHeader = $this->request->getHeader('oc-checksum');
368
+            if ($checksumHeader) {
369
+                $checksum = trim($checksumHeader);
370
+                $this->setChecksum($checksum);
371
+            } elseif ($this->getChecksum() !== null && $this->getChecksum() !== '') {
372
+                $this->setChecksum('');
373
+            }
374
+        } catch (StorageNotAvailableException $e) {
375
+            throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
376
+        }
377
+
378
+        return '"' . $this->info->getEtag() . '"';
379
+    }
380
+
381
+    private function getPartFileBasePath($path) {
382
+        $partFileInStorage = Server::get(IConfig::class)->getSystemValue('part_file_in_storage', true);
383
+        if ($partFileInStorage) {
384
+            $filename = basename($path);
385
+            // hash does not need to be secure but fast and semi unique
386
+            $hashedFilename = hash('xxh128', $filename);
387
+            return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
388
+        } else {
389
+            // will place the .part file in the users root directory
390
+            // therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
391
+            return hash('xxh128', $path);
392
+        }
393
+    }
394
+
395
+    private function emitPreHooks(bool $exists, ?string $path = null): bool {
396
+        if (is_null($path)) {
397
+            $path = $this->path;
398
+        }
399
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
400
+        if ($hookPath === null) {
401
+            // We only trigger hooks from inside default view
402
+            return true;
403
+        }
404
+        $run = true;
405
+
406
+        if (!$exists) {
407
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_create, [
408
+                Filesystem::signal_param_path => $hookPath,
409
+                Filesystem::signal_param_run => &$run,
410
+            ]);
411
+        } else {
412
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_update, [
413
+                Filesystem::signal_param_path => $hookPath,
414
+                Filesystem::signal_param_run => &$run,
415
+            ]);
416
+        }
417
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_write, [
418
+            Filesystem::signal_param_path => $hookPath,
419
+            Filesystem::signal_param_run => &$run,
420
+        ]);
421
+        return $run;
422
+    }
423
+
424
+    private function emitPostHooks(bool $exists, ?string $path = null): void {
425
+        if (is_null($path)) {
426
+            $path = $this->path;
427
+        }
428
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
429
+        if ($hookPath === null) {
430
+            // We only trigger hooks from inside default view
431
+            return;
432
+        }
433
+        if (!$exists) {
434
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_create, [
435
+                Filesystem::signal_param_path => $hookPath
436
+            ]);
437
+        } else {
438
+            \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_update, [
439
+                Filesystem::signal_param_path => $hookPath
440
+            ]);
441
+        }
442
+        \OC_Hook::emit(Filesystem::CLASSNAME, Filesystem::signal_post_write, [
443
+            Filesystem::signal_param_path => $hookPath
444
+        ]);
445
+    }
446
+
447
+    /**
448
+     * Returns the data
449
+     *
450
+     * @return resource
451
+     * @throws Forbidden
452
+     * @throws ServiceUnavailable
453
+     */
454
+    public function get() {
455
+        //throw exception if encryption is disabled but files are still encrypted
456
+        try {
457
+            if (!$this->info->isReadable()) {
458
+                // do a if the file did not exist
459
+                throw new NotFound();
460
+            }
461
+            $path = ltrim($this->path, '/');
462
+            try {
463
+                $res = $this->fileView->fopen($path, 'rb');
464
+            } catch (\Exception $e) {
465
+                $this->convertToSabreException($e);
466
+            }
467
+
468
+            if ($res === false) {
469
+                if ($this->fileView->file_exists($path)) {
470
+                    throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file does seem to exist', [$path]));
471
+                } else {
472
+                    throw new ServiceUnavailable($this->l10n->t('Could not open file: %1$s, file doesn\'t seem to exist', [$path]));
473
+                }
474
+            }
475
+
476
+            // comparing current file size with the one in DB
477
+            // if different, fix DB and refresh cache.
478
+            if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
479
+                $logger = Server::get(LoggerInterface::class);
480
+                $logger->warning('fixing cached size of file id=' . $this->getId());
481
+
482
+                $this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
483
+                $this->refreshInfo();
484
+            }
485
+
486
+            return $res;
487
+        } catch (GenericEncryptionException $e) {
488
+            // returning 503 will allow retry of the operation at a later point in time
489
+            throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]));
490
+        } catch (StorageNotAvailableException $e) {
491
+            throw new ServiceUnavailable($this->l10n->t('Failed to open file: %1$s', [$e->getMessage()]));
492
+        } catch (ForbiddenException $ex) {
493
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
494
+        } catch (LockedException $e) {
495
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
496
+        }
497
+    }
498
+
499
+    /**
500
+     * Delete the current file
501
+     *
502
+     * @throws Forbidden
503
+     * @throws ServiceUnavailable
504
+     */
505
+    public function delete() {
506
+        if (!$this->info->isDeletable()) {
507
+            throw new Forbidden();
508
+        }
509
+
510
+        try {
511
+            if (!$this->fileView->unlink($this->path)) {
512
+                // assume it wasn't possible to delete due to permissions
513
+                throw new Forbidden();
514
+            }
515
+        } catch (StorageNotAvailableException $e) {
516
+            throw new ServiceUnavailable($this->l10n->t('Failed to unlink: %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
+     * Returns the mime-type for a file
526
+     *
527
+     * If null is returned, we'll assume application/octet-stream
528
+     *
529
+     * @return string
530
+     */
531
+    public function getContentType() {
532
+        $mimeType = $this->info->getMimetype();
533
+
534
+        // PROPFIND needs to return the correct mime type, for consistency with the web UI
535
+        if ($this->request->getMethod() === 'PROPFIND') {
536
+            return $mimeType;
537
+        }
538
+        return Server::get(IMimeTypeDetector::class)->getSecureMimeType($mimeType);
539
+    }
540
+
541
+    /**
542
+     * @return array|bool
543
+     */
544
+    public function getDirectDownload() {
545
+        if (Server::get(IAppManager::class)->isEnabledForUser('encryption')) {
546
+            return [];
547
+        }
548
+        [$storage, $internalPath] = $this->fileView->resolvePath($this->path);
549
+        if (is_null($storage)) {
550
+            return [];
551
+        }
552
+
553
+        return $storage->getDirectDownload($internalPath);
554
+    }
555
+
556
+    /**
557
+     * Convert the given exception to a SabreException instance
558
+     *
559
+     * @param \Exception $e
560
+     *
561
+     * @throws \Sabre\DAV\Exception
562
+     */
563
+    private function convertToSabreException(\Exception $e) {
564
+        if ($e instanceof \Sabre\DAV\Exception) {
565
+            throw $e;
566
+        }
567
+        if ($e instanceof NotPermittedException) {
568
+            // a more general case - due to whatever reason the content could not be written
569
+            throw new Forbidden($e->getMessage(), 0, $e);
570
+        }
571
+        if ($e instanceof ForbiddenException) {
572
+            // the path for the file was forbidden
573
+            throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
574
+        }
575
+        if ($e instanceof EntityTooLargeException) {
576
+            // the file is too big to be stored
577
+            throw new EntityTooLarge($e->getMessage(), 0, $e);
578
+        }
579
+        if ($e instanceof InvalidContentException) {
580
+            // the file content is not permitted
581
+            throw new UnsupportedMediaType($e->getMessage(), 0, $e);
582
+        }
583
+        if ($e instanceof InvalidPathException) {
584
+            // the path for the file was not valid
585
+            // TODO: find proper http status code for this case
586
+            throw new Forbidden($e->getMessage(), 0, $e);
587
+        }
588
+        if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
589
+            // the file is currently being written to by another process
590
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
591
+        }
592
+        if ($e instanceof GenericEncryptionException) {
593
+            // returning 503 will allow retry of the operation at a later point in time
594
+            throw new ServiceUnavailable($this->l10n->t('Encryption not ready: %1$s', [$e->getMessage()]), 0, $e);
595
+        }
596
+        if ($e instanceof StorageNotAvailableException) {
597
+            throw new ServiceUnavailable($this->l10n->t('Failed to write file contents: %1$s', [$e->getMessage()]), 0, $e);
598
+        }
599
+        if ($e instanceof NotFoundException) {
600
+            throw new NotFound($this->l10n->t('File not found: %1$s', [$e->getMessage()]), 0, $e);
601
+        }
602
+
603
+        throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
604
+    }
605
+
606
+    /**
607
+     * Get the checksum for this file
608
+     *
609
+     * @return string|null
610
+     */
611
+    public function getChecksum() {
612
+        return $this->info->getChecksum();
613
+    }
614
+
615
+    public function setChecksum(string $checksum) {
616
+        $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
617
+        $this->refreshInfo();
618
+    }
619
+
620
+    protected function header($string) {
621
+        if (!\OC::$CLI) {
622
+            \header($string);
623
+        }
624
+    }
625
+
626
+    public function hash(string $type) {
627
+        return $this->fileView->hash($type, $this->path);
628
+    }
629
+
630
+    public function getNode(): \OCP\Files\File {
631
+        return $this->node;
632
+    }
633 633
 }
Please login to merge, or discard this patch.
Spacing   +14 added lines, -14 removed lines patch added patch discarded remove patch
@@ -132,7 +132,7 @@  discard block
 block discarded – undo
132 132
 		if ($needsPartFile) {
133 133
 			$transferId = \rand();
134 134
 			// mark file as partial while uploading (ignored by the scanner)
135
-			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . $transferId . '.part';
135
+			$partFilePath = $this->getPartFileBasePath($this->path).'.ocTransferId'.$transferId.'.part';
136 136
 
137 137
 			if (!$view->isCreatable($partFilePath) && $view->isUpdatable($this->path)) {
138 138
 				$needsPartFile = false;
@@ -186,27 +186,27 @@  discard block
 block discarded – undo
186 186
 			if ($this->request->getHeader('X-HASH') !== '') {
187 187
 				$hash = $this->request->getHeader('X-HASH');
188 188
 				if ($hash === 'all' || $hash === 'md5') {
189
-					$data = HashWrapper::wrap($data, 'md5', function ($hash): void {
190
-						$this->header('X-Hash-MD5: ' . $hash);
189
+					$data = HashWrapper::wrap($data, 'md5', function($hash): void {
190
+						$this->header('X-Hash-MD5: '.$hash);
191 191
 					});
192 192
 				}
193 193
 
194 194
 				if ($hash === 'all' || $hash === 'sha1') {
195
-					$data = HashWrapper::wrap($data, 'sha1', function ($hash): void {
196
-						$this->header('X-Hash-SHA1: ' . $hash);
195
+					$data = HashWrapper::wrap($data, 'sha1', function($hash): void {
196
+						$this->header('X-Hash-SHA1: '.$hash);
197 197
 					});
198 198
 				}
199 199
 
200 200
 				if ($hash === 'all' || $hash === 'sha256') {
201
-					$data = HashWrapper::wrap($data, 'sha256', function ($hash): void {
202
-						$this->header('X-Hash-SHA256: ' . $hash);
201
+					$data = HashWrapper::wrap($data, 'sha256', function($hash): void {
202
+						$this->header('X-Hash-SHA256: '.$hash);
203 203
 					});
204 204
 				}
205 205
 			}
206 206
 
207 207
 			if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) {
208 208
 				$isEOF = false;
209
-				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void {
209
+				$wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function($stream) use (&$isEOF): void {
210 210
 					$isEOF = feof($stream);
211 211
 				});
212 212
 
@@ -218,7 +218,7 @@  discard block
 block discarded – undo
218 218
 						$count = $partStorage->writeStream($internalPartPath, $wrappedData);
219 219
 					} catch (GenericFileException $e) {
220 220
 						$logger = Server::get(LoggerInterface::class);
221
-						$logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
221
+						$logger->error('Error while writing stream to storage: '.$e->getMessage(), ['exception' => $e, 'app' => 'webdav']);
222 222
 						$result = $isEOF;
223 223
 						if (is_resource($wrappedData)) {
224 224
 							$result = feof($wrappedData);
@@ -237,7 +237,7 @@  discard block
 block discarded – undo
237 237
 			}
238 238
 
239 239
 			$lengthHeader = $this->request->getHeader('content-length');
240
-			$expected = $lengthHeader !== '' ? (int)$lengthHeader : -1;
240
+			$expected = $lengthHeader !== '' ? (int) $lengthHeader : -1;
241 241
 			if ($result === false && $expected >= 0) {
242 242
 				throw new Exception(
243 243
 					$this->l10n->t(
@@ -312,7 +312,7 @@  discard block
 block discarded – undo
312 312
 					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
313 313
 					$fileExists = $storage->file_exists($internalPath);
314 314
 					if ($renameOkay === false || $fileExists === false) {
315
-						Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
315
+						Server::get(LoggerInterface::class)->error('renaming part file to final file failed $renameOkay: '.($renameOkay ? 'true' : 'false').', $fileExists: '.($fileExists ? 'true' : 'false').')', ['app' => 'webdav']);
316 316
 						throw new Exception($this->l10n->t('Could not rename part file to final file'));
317 317
 					}
318 318
 				} catch (ForbiddenException $ex) {
@@ -375,7 +375,7 @@  discard block
 block discarded – undo
375 375
 			throw new ServiceUnavailable($this->l10n->t('Failed to check file size: %1$s', [$e->getMessage()]), 0, $e);
376 376
 		}
377 377
 
378
-		return '"' . $this->info->getEtag() . '"';
378
+		return '"'.$this->info->getEtag().'"';
379 379
 	}
380 380
 
381 381
 	private function getPartFileBasePath($path) {
@@ -384,7 +384,7 @@  discard block
 block discarded – undo
384 384
 			$filename = basename($path);
385 385
 			// hash does not need to be secure but fast and semi unique
386 386
 			$hashedFilename = hash('xxh128', $filename);
387
-			return substr($path, 0, strlen($path) - strlen($filename)) . $hashedFilename;
387
+			return substr($path, 0, strlen($path) - strlen($filename)).$hashedFilename;
388 388
 		} else {
389 389
 			// will place the .part file in the users root directory
390 390
 			// therefor we need to make the name (semi) unique - hash does not need to be secure but fast.
@@ -477,7 +477,7 @@  discard block
 block discarded – undo
477 477
 			// if different, fix DB and refresh cache.
478 478
 			if ($this->getSize() !== $this->fileView->filesize($this->getPath())) {
479 479
 				$logger = Server::get(LoggerInterface::class);
480
-				$logger->warning('fixing cached size of file id=' . $this->getId());
480
+				$logger->warning('fixing cached size of file id='.$this->getId());
481 481
 
482 482
 				$this->getFileInfo()->getStorage()->getUpdater()->update($this->getFileInfo()->getInternalPath());
483 483
 				$this->refreshInfo();
Please login to merge, or discard this patch.