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