Passed
Push — master ( 9ea657...6635bd )
by Morris
15:31
created
apps/dav/lib/Connector/Sabre/File.php 1 patch
Indentation   +542 added lines, -542 removed lines patch added patch discarded remove patch
@@ -66,546 +66,546 @@
 block discarded – undo
66 66
 
67 67
 class File extends Node implements IFile {
68 68
 
69
-	protected $request;
70
-
71
-	/**
72
-	 * Sets up the node, expects a full path name
73
-	 *
74
-	 * @param \OC\Files\View $view
75
-	 * @param \OCP\Files\FileInfo $info
76
-	 * @param \OCP\Share\IManager $shareManager
77
-	 * @param \OC\AppFramework\Http\Request $request
78
-	 */
79
-	public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
80
-		parent::__construct($view, $info, $shareManager);
81
-
82
-		if (isset($request)) {
83
-			$this->request = $request;
84
-		} else {
85
-			$this->request = \OC::$server->getRequest();
86
-		}
87
-	}
88
-
89
-	/**
90
-	 * Updates the data
91
-	 *
92
-	 * The data argument is a readable stream resource.
93
-	 *
94
-	 * After a successful put operation, you may choose to return an ETag. The
95
-	 * etag must always be surrounded by double-quotes. These quotes must
96
-	 * appear in the actual string you're returning.
97
-	 *
98
-	 * Clients may use the ETag from a PUT request to later on make sure that
99
-	 * when they update the file, the contents haven't changed in the mean
100
-	 * time.
101
-	 *
102
-	 * If you don't plan to store the file byte-by-byte, and you return a
103
-	 * different object on a subsequent GET you are strongly recommended to not
104
-	 * return an ETag, and just return null.
105
-	 *
106
-	 * @param resource $data
107
-	 *
108
-	 * @throws Forbidden
109
-	 * @throws UnsupportedMediaType
110
-	 * @throws BadRequest
111
-	 * @throws Exception
112
-	 * @throws EntityTooLarge
113
-	 * @throws ServiceUnavailable
114
-	 * @throws FileLocked
115
-	 * @return string|null
116
-	 */
117
-	public function put($data) {
118
-		try {
119
-			$exists = $this->fileView->file_exists($this->path);
120
-			if ($this->info && $exists && !$this->info->isUpdateable()) {
121
-				throw new Forbidden();
122
-			}
123
-		} catch (StorageNotAvailableException $e) {
124
-			throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
125
-		}
126
-
127
-		// verify path of the target
128
-		$this->verifyPath();
129
-
130
-		// chunked handling
131
-		if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
132
-			try {
133
-				return $this->createFileChunked($data);
134
-			} catch (\Exception $e) {
135
-				$this->convertToSabreException($e);
136
-			}
137
-		}
138
-
139
-		/** @var Storage $partStorage */
140
-		list($partStorage) = $this->fileView->resolvePath($this->path);
141
-		$needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
142
-
143
-		$view = \OC\Files\Filesystem::getView();
144
-
145
-		if ($needsPartFile) {
146
-			// mark file as partial while uploading (ignored by the scanner)
147
-			$partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
148
-		} else {
149
-			// upload file directly as the final path
150
-			$partFilePath = $this->path;
151
-
152
-			if ($view && !$this->emitPreHooks($exists)) {
153
-				throw new Exception('Could not write to final file, canceled by hook');
154
-			}
155
-		}
156
-
157
-		// the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
158
-		/** @var \OC\Files\Storage\Storage $partStorage */
159
-		list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
160
-		/** @var \OC\Files\Storage\Storage $storage */
161
-		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
162
-		try {
163
-			if (!$needsPartFile) {
164
-				$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
165
-			}
166
-
167
-			if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
168
-				$count = $partStorage->writeStream($internalPartPath, $data);
169
-				$result = $count > 0;
170
-				if ($result === false) {
171
-					$result = feof($data);
172
-				}
173
-
174
-			} else {
175
-				$target = $partStorage->fopen($internalPartPath, 'wb');
176
-				if ($target === false) {
177
-					\OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
178
-					// because we have no clue about the cause we can only throw back a 500/Internal Server Error
179
-					throw new Exception('Could not write file contents');
180
-				}
181
-				list($count, $result) = \OC_Helper::streamCopy($data, $target);
182
-				fclose($target);
183
-			}
184
-
185
-			if ($result === false) {
186
-				$expected = -1;
187
-				if (isset($_SERVER['CONTENT_LENGTH'])) {
188
-					$expected = $_SERVER['CONTENT_LENGTH'];
189
-				}
190
-				throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )');
191
-			}
192
-
193
-			// if content length is sent by client:
194
-			// double check if the file was fully received
195
-			// compare expected and actual size
196
-			if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
197
-				$expected = (int)$_SERVER['CONTENT_LENGTH'];
198
-				if ($count !== $expected) {
199
-					throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
200
-				}
201
-			}
202
-
203
-		} catch (\Exception $e) {
204
-			\OC::$server->getLogger()->logException($e);
205
-			if ($needsPartFile) {
206
-				$partStorage->unlink($internalPartPath);
207
-			}
208
-			$this->convertToSabreException($e);
209
-		}
210
-
211
-		try {
212
-			if ($needsPartFile) {
213
-				if ($view && !$this->emitPreHooks($exists)) {
214
-					$partStorage->unlink($internalPartPath);
215
-					throw new Exception('Could not rename part file to final file, canceled by hook');
216
-				}
217
-				try {
218
-					$this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
219
-				} catch (LockedException $e) {
220
-					if ($needsPartFile) {
221
-						$partStorage->unlink($internalPartPath);
222
-					}
223
-					throw new FileLocked($e->getMessage(), $e->getCode(), $e);
224
-				}
225
-
226
-				// rename to correct path
227
-				try {
228
-					$renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
229
-					$fileExists = $storage->file_exists($internalPath);
230
-					if ($renameOkay === false || $fileExists === false) {
231
-						\OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
232
-						throw new Exception('Could not rename part file to final file');
233
-					}
234
-				} catch (ForbiddenException $ex) {
235
-					throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
236
-				} catch (\Exception $e) {
237
-					$partStorage->unlink($internalPartPath);
238
-					$this->convertToSabreException($e);
239
-				}
240
-			}
241
-
242
-			// since we skipped the view we need to scan and emit the hooks ourselves
243
-			$storage->getUpdater()->update($internalPath);
244
-
245
-			try {
246
-				$this->changeLock(ILockingProvider::LOCK_SHARED);
247
-			} catch (LockedException $e) {
248
-				throw new FileLocked($e->getMessage(), $e->getCode(), $e);
249
-			}
250
-
251
-			// allow sync clients to send the mtime along in a header
252
-			if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
253
-				$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
254
-				if ($this->fileView->touch($this->path, $mtime)) {
255
-					$this->header('X-OC-MTime: accepted');
256
-				}
257
-			}
258
-
259
-			if ($view) {
260
-				$this->emitPostHooks($exists);
261
-			}
262
-
263
-			$this->refreshInfo();
264
-
265
-			if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
266
-				$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
267
-				$this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
268
-				$this->refreshInfo();
269
-			} else if ($this->getChecksum() !== null && $this->getChecksum() !== '') {
270
-				$this->fileView->putFileInfo($this->path, ['checksum' => '']);
271
-				$this->refreshInfo();
272
-			}
273
-
274
-		} catch (StorageNotAvailableException $e) {
275
-			throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
276
-		}
277
-
278
-		return '"' . $this->info->getEtag() . '"';
279
-	}
280
-
281
-	private function getPartFileBasePath($path) {
282
-		$partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
283
-		if ($partFileInStorage) {
284
-			return $path;
285
-		} else {
286
-			return md5($path); // will place it in the root of the view with a unique name
287
-		}
288
-	}
289
-
290
-	/**
291
-	 * @param string $path
292
-	 */
293
-	private function emitPreHooks($exists, $path = null) {
294
-		if (is_null($path)) {
295
-			$path = $this->path;
296
-		}
297
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
298
-		$run = true;
299
-
300
-		if (!$exists) {
301
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
302
-				\OC\Files\Filesystem::signal_param_path => $hookPath,
303
-				\OC\Files\Filesystem::signal_param_run => &$run,
304
-			));
305
-		} else {
306
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
307
-				\OC\Files\Filesystem::signal_param_path => $hookPath,
308
-				\OC\Files\Filesystem::signal_param_run => &$run,
309
-			));
310
-		}
311
-		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
312
-			\OC\Files\Filesystem::signal_param_path => $hookPath,
313
-			\OC\Files\Filesystem::signal_param_run => &$run,
314
-		));
315
-		return $run;
316
-	}
317
-
318
-	/**
319
-	 * @param string $path
320
-	 */
321
-	private function emitPostHooks($exists, $path = null) {
322
-		if (is_null($path)) {
323
-			$path = $this->path;
324
-		}
325
-		$hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
326
-		if (!$exists) {
327
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
328
-				\OC\Files\Filesystem::signal_param_path => $hookPath
329
-			));
330
-		} else {
331
-			\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
332
-				\OC\Files\Filesystem::signal_param_path => $hookPath
333
-			));
334
-		}
335
-		\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
336
-			\OC\Files\Filesystem::signal_param_path => $hookPath
337
-		));
338
-	}
339
-
340
-	/**
341
-	 * Returns the data
342
-	 *
343
-	 * @return resource
344
-	 * @throws Forbidden
345
-	 * @throws ServiceUnavailable
346
-	 */
347
-	public function get() {
348
-		//throw exception if encryption is disabled but files are still encrypted
349
-		try {
350
-			if (!$this->info->isReadable()) {
351
-				// do a if the file did not exist
352
-				throw new NotFound();
353
-			}
354
-			try {
355
-				$res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
356
-			} catch (\Exception $e) {
357
-				$this->convertToSabreException($e);
358
-			}
359
-			if ($res === false) {
360
-				throw new ServiceUnavailable("Could not open file");
361
-			}
362
-			return $res;
363
-		} catch (GenericEncryptionException $e) {
364
-			// returning 503 will allow retry of the operation at a later point in time
365
-			throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
366
-		} catch (StorageNotAvailableException $e) {
367
-			throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
368
-		} catch (ForbiddenException $ex) {
369
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
370
-		} catch (LockedException $e) {
371
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
372
-		}
373
-	}
374
-
375
-	/**
376
-	 * Delete the current file
377
-	 *
378
-	 * @throws Forbidden
379
-	 * @throws ServiceUnavailable
380
-	 */
381
-	public function delete() {
382
-		if (!$this->info->isDeletable()) {
383
-			throw new Forbidden();
384
-		}
385
-
386
-		try {
387
-			if (!$this->fileView->unlink($this->path)) {
388
-				// assume it wasn't possible to delete due to permissions
389
-				throw new Forbidden();
390
-			}
391
-		} catch (StorageNotAvailableException $e) {
392
-			throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
393
-		} catch (ForbiddenException $ex) {
394
-			throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
395
-		} catch (LockedException $e) {
396
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
397
-		}
398
-	}
399
-
400
-	/**
401
-	 * Returns the mime-type for a file
402
-	 *
403
-	 * If null is returned, we'll assume application/octet-stream
404
-	 *
405
-	 * @return string
406
-	 */
407
-	public function getContentType() {
408
-		$mimeType = $this->info->getMimetype();
409
-
410
-		// PROPFIND needs to return the correct mime type, for consistency with the web UI
411
-		if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
412
-			return $mimeType;
413
-		}
414
-		return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
415
-	}
416
-
417
-	/**
418
-	 * @return array|false
419
-	 */
420
-	public function getDirectDownload() {
421
-		if (\OCP\App::isEnabled('encryption')) {
422
-			return [];
423
-		}
424
-		/** @var \OCP\Files\Storage $storage */
425
-		list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
426
-		if (is_null($storage)) {
427
-			return [];
428
-		}
429
-
430
-		return $storage->getDirectDownload($internalPath);
431
-	}
432
-
433
-	/**
434
-	 * @param resource $data
435
-	 * @return null|string
436
-	 * @throws Exception
437
-	 * @throws BadRequest
438
-	 * @throws NotImplemented
439
-	 * @throws ServiceUnavailable
440
-	 */
441
-	private function createFileChunked($data) {
442
-		list($path, $name) = \Sabre\Uri\split($this->path);
443
-
444
-		$info = \OC_FileChunking::decodeName($name);
445
-		if (empty($info)) {
446
-			throw new NotImplemented('Invalid chunk name');
447
-		}
448
-
449
-		$chunk_handler = new \OC_FileChunking($info);
450
-		$bytesWritten = $chunk_handler->store($info['index'], $data);
451
-
452
-		//detect aborted upload
453
-		if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
454
-			if (isset($_SERVER['CONTENT_LENGTH'])) {
455
-				$expected = (int)$_SERVER['CONTENT_LENGTH'];
456
-				if ($bytesWritten !== $expected) {
457
-					$chunk_handler->remove($info['index']);
458
-					throw new BadRequest(
459
-						'expected filesize ' . $expected . ' got ' . $bytesWritten);
460
-				}
461
-			}
462
-		}
463
-
464
-		if ($chunk_handler->isComplete()) {
465
-			/** @var Storage $storage */
466
-			list($storage,) = $this->fileView->resolvePath($path);
467
-			$needsPartFile = $storage->needsPartFile();
468
-			$partFile = null;
469
-
470
-			$targetPath = $path . '/' . $info['name'];
471
-			/** @var \OC\Files\Storage\Storage $targetStorage */
472
-			list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
473
-
474
-			$exists = $this->fileView->file_exists($targetPath);
475
-
476
-			try {
477
-				$this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
478
-
479
-				$this->emitPreHooks($exists, $targetPath);
480
-				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
481
-				/** @var \OC\Files\Storage\Storage $targetStorage */
482
-				list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
483
-
484
-				if ($needsPartFile) {
485
-					// we first assembly the target file as a part file
486
-					$partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
487
-					/** @var \OC\Files\Storage\Storage $targetStorage */
488
-					list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
489
-
490
-
491
-					$chunk_handler->file_assemble($partStorage, $partInternalPath);
492
-
493
-					// here is the final atomic rename
494
-					$renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
495
-					$fileExists = $targetStorage->file_exists($targetInternalPath);
496
-					if ($renameOkay === false || $fileExists === false) {
497
-						\OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
498
-						// only delete if an error occurred and the target file was already created
499
-						if ($fileExists) {
500
-							// set to null to avoid double-deletion when handling exception
501
-							// stray part file
502
-							$partFile = null;
503
-							$targetStorage->unlink($targetInternalPath);
504
-						}
505
-						$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
506
-						throw new Exception('Could not rename part file assembled from chunks');
507
-					}
508
-				} else {
509
-					// assemble directly into the final file
510
-					$chunk_handler->file_assemble($targetStorage, $targetInternalPath);
511
-				}
512
-
513
-				// allow sync clients to send the mtime along in a header
514
-				if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
515
-					$mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
516
-					if ($targetStorage->touch($targetInternalPath, $mtime)) {
517
-						$this->header('X-OC-MTime: accepted');
518
-					}
519
-				}
520
-
521
-				// since we skipped the view we need to scan and emit the hooks ourselves
522
-				$targetStorage->getUpdater()->update($targetInternalPath);
523
-
524
-				$this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
525
-
526
-				$this->emitPostHooks($exists, $targetPath);
527
-
528
-				// FIXME: should call refreshInfo but can't because $this->path is not the of the final file
529
-				$info = $this->fileView->getFileInfo($targetPath);
530
-
531
-				if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
532
-					$checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
533
-					$this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
534
-				} else if ($info->getChecksum() !== null && $info->getChecksum() !== '') {
535
-					$this->fileView->putFileInfo($this->path, ['checksum' => '']);
536
-				}
537
-
538
-				$this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
539
-
540
-				return $info->getEtag();
541
-			} catch (\Exception $e) {
542
-				if ($partFile !== null) {
543
-					$targetStorage->unlink($targetInternalPath);
544
-				}
545
-				$this->convertToSabreException($e);
546
-			}
547
-		}
548
-
549
-		return null;
550
-	}
551
-
552
-	/**
553
-	 * Convert the given exception to a SabreException instance
554
-	 *
555
-	 * @param \Exception $e
556
-	 *
557
-	 * @throws \Sabre\DAV\Exception
558
-	 */
559
-	private function convertToSabreException(\Exception $e) {
560
-		if ($e instanceof \Sabre\DAV\Exception) {
561
-			throw $e;
562
-		}
563
-		if ($e instanceof NotPermittedException) {
564
-			// a more general case - due to whatever reason the content could not be written
565
-			throw new Forbidden($e->getMessage(), 0, $e);
566
-		}
567
-		if ($e instanceof ForbiddenException) {
568
-			// the path for the file was forbidden
569
-			throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
570
-		}
571
-		if ($e instanceof EntityTooLargeException) {
572
-			// the file is too big to be stored
573
-			throw new EntityTooLarge($e->getMessage(), 0, $e);
574
-		}
575
-		if ($e instanceof InvalidContentException) {
576
-			// the file content is not permitted
577
-			throw new UnsupportedMediaType($e->getMessage(), 0, $e);
578
-		}
579
-		if ($e instanceof InvalidPathException) {
580
-			// the path for the file was not valid
581
-			// TODO: find proper http status code for this case
582
-			throw new Forbidden($e->getMessage(), 0, $e);
583
-		}
584
-		if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
585
-			// the file is currently being written to by another process
586
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
587
-		}
588
-		if ($e instanceof GenericEncryptionException) {
589
-			// returning 503 will allow retry of the operation at a later point in time
590
-			throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
591
-		}
592
-		if ($e instanceof StorageNotAvailableException) {
593
-			throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
594
-		}
595
-
596
-		throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
597
-	}
598
-
599
-	/**
600
-	 * Get the checksum for this file
601
-	 *
602
-	 * @return string
603
-	 */
604
-	public function getChecksum() {
605
-		return $this->info->getChecksum();
606
-	}
607
-
608
-	protected function header($string) {
609
-		\header($string);
610
-	}
69
+    protected $request;
70
+
71
+    /**
72
+     * Sets up the node, expects a full path name
73
+     *
74
+     * @param \OC\Files\View $view
75
+     * @param \OCP\Files\FileInfo $info
76
+     * @param \OCP\Share\IManager $shareManager
77
+     * @param \OC\AppFramework\Http\Request $request
78
+     */
79
+    public function __construct(View $view, FileInfo $info, IManager $shareManager = null, Request $request = null) {
80
+        parent::__construct($view, $info, $shareManager);
81
+
82
+        if (isset($request)) {
83
+            $this->request = $request;
84
+        } else {
85
+            $this->request = \OC::$server->getRequest();
86
+        }
87
+    }
88
+
89
+    /**
90
+     * Updates the data
91
+     *
92
+     * The data argument is a readable stream resource.
93
+     *
94
+     * After a successful put operation, you may choose to return an ETag. The
95
+     * etag must always be surrounded by double-quotes. These quotes must
96
+     * appear in the actual string you're returning.
97
+     *
98
+     * Clients may use the ETag from a PUT request to later on make sure that
99
+     * when they update the file, the contents haven't changed in the mean
100
+     * time.
101
+     *
102
+     * If you don't plan to store the file byte-by-byte, and you return a
103
+     * different object on a subsequent GET you are strongly recommended to not
104
+     * return an ETag, and just return null.
105
+     *
106
+     * @param resource $data
107
+     *
108
+     * @throws Forbidden
109
+     * @throws UnsupportedMediaType
110
+     * @throws BadRequest
111
+     * @throws Exception
112
+     * @throws EntityTooLarge
113
+     * @throws ServiceUnavailable
114
+     * @throws FileLocked
115
+     * @return string|null
116
+     */
117
+    public function put($data) {
118
+        try {
119
+            $exists = $this->fileView->file_exists($this->path);
120
+            if ($this->info && $exists && !$this->info->isUpdateable()) {
121
+                throw new Forbidden();
122
+            }
123
+        } catch (StorageNotAvailableException $e) {
124
+            throw new ServiceUnavailable("File is not updatable: " . $e->getMessage());
125
+        }
126
+
127
+        // verify path of the target
128
+        $this->verifyPath();
129
+
130
+        // chunked handling
131
+        if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
132
+            try {
133
+                return $this->createFileChunked($data);
134
+            } catch (\Exception $e) {
135
+                $this->convertToSabreException($e);
136
+            }
137
+        }
138
+
139
+        /** @var Storage $partStorage */
140
+        list($partStorage) = $this->fileView->resolvePath($this->path);
141
+        $needsPartFile = $partStorage->needsPartFile() && (strlen($this->path) > 1);
142
+
143
+        $view = \OC\Files\Filesystem::getView();
144
+
145
+        if ($needsPartFile) {
146
+            // mark file as partial while uploading (ignored by the scanner)
147
+            $partFilePath = $this->getPartFileBasePath($this->path) . '.ocTransferId' . rand() . '.part';
148
+        } else {
149
+            // upload file directly as the final path
150
+            $partFilePath = $this->path;
151
+
152
+            if ($view && !$this->emitPreHooks($exists)) {
153
+                throw new Exception('Could not write to final file, canceled by hook');
154
+            }
155
+        }
156
+
157
+        // the part file and target file might be on a different storage in case of a single file storage (e.g. single file share)
158
+        /** @var \OC\Files\Storage\Storage $partStorage */
159
+        list($partStorage, $internalPartPath) = $this->fileView->resolvePath($partFilePath);
160
+        /** @var \OC\Files\Storage\Storage $storage */
161
+        list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
162
+        try {
163
+            if (!$needsPartFile) {
164
+                $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
165
+            }
166
+
167
+            if ($partStorage->instanceOfStorage(Storage\IWriteStreamStorage::class)) {
168
+                $count = $partStorage->writeStream($internalPartPath, $data);
169
+                $result = $count > 0;
170
+                if ($result === false) {
171
+                    $result = feof($data);
172
+                }
173
+
174
+            } else {
175
+                $target = $partStorage->fopen($internalPartPath, 'wb');
176
+                if ($target === false) {
177
+                    \OC::$server->getLogger()->error('\OC\Files\Filesystem::fopen() failed', ['app' => 'webdav']);
178
+                    // because we have no clue about the cause we can only throw back a 500/Internal Server Error
179
+                    throw new Exception('Could not write file contents');
180
+                }
181
+                list($count, $result) = \OC_Helper::streamCopy($data, $target);
182
+                fclose($target);
183
+            }
184
+
185
+            if ($result === false) {
186
+                $expected = -1;
187
+                if (isset($_SERVER['CONTENT_LENGTH'])) {
188
+                    $expected = $_SERVER['CONTENT_LENGTH'];
189
+                }
190
+                throw new Exception('Error while copying file to target location (copied bytes: ' . $count . ', expected filesize: ' . $expected . ' )');
191
+            }
192
+
193
+            // if content length is sent by client:
194
+            // double check if the file was fully received
195
+            // compare expected and actual size
196
+            if (isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
197
+                $expected = (int)$_SERVER['CONTENT_LENGTH'];
198
+                if ($count !== $expected) {
199
+                    throw new BadRequest('expected filesize ' . $expected . ' got ' . $count);
200
+                }
201
+            }
202
+
203
+        } catch (\Exception $e) {
204
+            \OC::$server->getLogger()->logException($e);
205
+            if ($needsPartFile) {
206
+                $partStorage->unlink($internalPartPath);
207
+            }
208
+            $this->convertToSabreException($e);
209
+        }
210
+
211
+        try {
212
+            if ($needsPartFile) {
213
+                if ($view && !$this->emitPreHooks($exists)) {
214
+                    $partStorage->unlink($internalPartPath);
215
+                    throw new Exception('Could not rename part file to final file, canceled by hook');
216
+                }
217
+                try {
218
+                    $this->changeLock(ILockingProvider::LOCK_EXCLUSIVE);
219
+                } catch (LockedException $e) {
220
+                    if ($needsPartFile) {
221
+                        $partStorage->unlink($internalPartPath);
222
+                    }
223
+                    throw new FileLocked($e->getMessage(), $e->getCode(), $e);
224
+                }
225
+
226
+                // rename to correct path
227
+                try {
228
+                    $renameOkay = $storage->moveFromStorage($partStorage, $internalPartPath, $internalPath);
229
+                    $fileExists = $storage->file_exists($internalPath);
230
+                    if ($renameOkay === false || $fileExists === false) {
231
+                        \OC::$server->getLogger()->error('renaming part file to final file failed $renameOkay: ' . ($renameOkay ? 'true' : 'false') . ', $fileExists: ' . ($fileExists ? 'true' : 'false') . ')', ['app' => 'webdav']);
232
+                        throw new Exception('Could not rename part file to final file');
233
+                    }
234
+                } catch (ForbiddenException $ex) {
235
+                    throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
236
+                } catch (\Exception $e) {
237
+                    $partStorage->unlink($internalPartPath);
238
+                    $this->convertToSabreException($e);
239
+                }
240
+            }
241
+
242
+            // since we skipped the view we need to scan and emit the hooks ourselves
243
+            $storage->getUpdater()->update($internalPath);
244
+
245
+            try {
246
+                $this->changeLock(ILockingProvider::LOCK_SHARED);
247
+            } catch (LockedException $e) {
248
+                throw new FileLocked($e->getMessage(), $e->getCode(), $e);
249
+            }
250
+
251
+            // allow sync clients to send the mtime along in a header
252
+            if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
253
+                $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
254
+                if ($this->fileView->touch($this->path, $mtime)) {
255
+                    $this->header('X-OC-MTime: accepted');
256
+                }
257
+            }
258
+
259
+            if ($view) {
260
+                $this->emitPostHooks($exists);
261
+            }
262
+
263
+            $this->refreshInfo();
264
+
265
+            if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
266
+                $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
267
+                $this->fileView->putFileInfo($this->path, ['checksum' => $checksum]);
268
+                $this->refreshInfo();
269
+            } else if ($this->getChecksum() !== null && $this->getChecksum() !== '') {
270
+                $this->fileView->putFileInfo($this->path, ['checksum' => '']);
271
+                $this->refreshInfo();
272
+            }
273
+
274
+        } catch (StorageNotAvailableException $e) {
275
+            throw new ServiceUnavailable("Failed to check file size: " . $e->getMessage());
276
+        }
277
+
278
+        return '"' . $this->info->getEtag() . '"';
279
+    }
280
+
281
+    private function getPartFileBasePath($path) {
282
+        $partFileInStorage = \OC::$server->getConfig()->getSystemValue('part_file_in_storage', true);
283
+        if ($partFileInStorage) {
284
+            return $path;
285
+        } else {
286
+            return md5($path); // will place it in the root of the view with a unique name
287
+        }
288
+    }
289
+
290
+    /**
291
+     * @param string $path
292
+     */
293
+    private function emitPreHooks($exists, $path = null) {
294
+        if (is_null($path)) {
295
+            $path = $this->path;
296
+        }
297
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
298
+        $run = true;
299
+
300
+        if (!$exists) {
301
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_create, array(
302
+                \OC\Files\Filesystem::signal_param_path => $hookPath,
303
+                \OC\Files\Filesystem::signal_param_run => &$run,
304
+            ));
305
+        } else {
306
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_update, array(
307
+                \OC\Files\Filesystem::signal_param_path => $hookPath,
308
+                \OC\Files\Filesystem::signal_param_run => &$run,
309
+            ));
310
+        }
311
+        \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_write, array(
312
+            \OC\Files\Filesystem::signal_param_path => $hookPath,
313
+            \OC\Files\Filesystem::signal_param_run => &$run,
314
+        ));
315
+        return $run;
316
+    }
317
+
318
+    /**
319
+     * @param string $path
320
+     */
321
+    private function emitPostHooks($exists, $path = null) {
322
+        if (is_null($path)) {
323
+            $path = $this->path;
324
+        }
325
+        $hookPath = Filesystem::getView()->getRelativePath($this->fileView->getAbsolutePath($path));
326
+        if (!$exists) {
327
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
328
+                \OC\Files\Filesystem::signal_param_path => $hookPath
329
+            ));
330
+        } else {
331
+            \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_update, array(
332
+                \OC\Files\Filesystem::signal_param_path => $hookPath
333
+            ));
334
+        }
335
+        \OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_write, array(
336
+            \OC\Files\Filesystem::signal_param_path => $hookPath
337
+        ));
338
+    }
339
+
340
+    /**
341
+     * Returns the data
342
+     *
343
+     * @return resource
344
+     * @throws Forbidden
345
+     * @throws ServiceUnavailable
346
+     */
347
+    public function get() {
348
+        //throw exception if encryption is disabled but files are still encrypted
349
+        try {
350
+            if (!$this->info->isReadable()) {
351
+                // do a if the file did not exist
352
+                throw new NotFound();
353
+            }
354
+            try {
355
+                $res = $this->fileView->fopen(ltrim($this->path, '/'), 'rb');
356
+            } catch (\Exception $e) {
357
+                $this->convertToSabreException($e);
358
+            }
359
+            if ($res === false) {
360
+                throw new ServiceUnavailable("Could not open file");
361
+            }
362
+            return $res;
363
+        } catch (GenericEncryptionException $e) {
364
+            // returning 503 will allow retry of the operation at a later point in time
365
+            throw new ServiceUnavailable("Encryption not ready: " . $e->getMessage());
366
+        } catch (StorageNotAvailableException $e) {
367
+            throw new ServiceUnavailable("Failed to open file: " . $e->getMessage());
368
+        } catch (ForbiddenException $ex) {
369
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
370
+        } catch (LockedException $e) {
371
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
372
+        }
373
+    }
374
+
375
+    /**
376
+     * Delete the current file
377
+     *
378
+     * @throws Forbidden
379
+     * @throws ServiceUnavailable
380
+     */
381
+    public function delete() {
382
+        if (!$this->info->isDeletable()) {
383
+            throw new Forbidden();
384
+        }
385
+
386
+        try {
387
+            if (!$this->fileView->unlink($this->path)) {
388
+                // assume it wasn't possible to delete due to permissions
389
+                throw new Forbidden();
390
+            }
391
+        } catch (StorageNotAvailableException $e) {
392
+            throw new ServiceUnavailable("Failed to unlink: " . $e->getMessage());
393
+        } catch (ForbiddenException $ex) {
394
+            throw new DAVForbiddenException($ex->getMessage(), $ex->getRetry());
395
+        } catch (LockedException $e) {
396
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
397
+        }
398
+    }
399
+
400
+    /**
401
+     * Returns the mime-type for a file
402
+     *
403
+     * If null is returned, we'll assume application/octet-stream
404
+     *
405
+     * @return string
406
+     */
407
+    public function getContentType() {
408
+        $mimeType = $this->info->getMimetype();
409
+
410
+        // PROPFIND needs to return the correct mime type, for consistency with the web UI
411
+        if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') {
412
+            return $mimeType;
413
+        }
414
+        return \OC::$server->getMimeTypeDetector()->getSecureMimeType($mimeType);
415
+    }
416
+
417
+    /**
418
+     * @return array|false
419
+     */
420
+    public function getDirectDownload() {
421
+        if (\OCP\App::isEnabled('encryption')) {
422
+            return [];
423
+        }
424
+        /** @var \OCP\Files\Storage $storage */
425
+        list($storage, $internalPath) = $this->fileView->resolvePath($this->path);
426
+        if (is_null($storage)) {
427
+            return [];
428
+        }
429
+
430
+        return $storage->getDirectDownload($internalPath);
431
+    }
432
+
433
+    /**
434
+     * @param resource $data
435
+     * @return null|string
436
+     * @throws Exception
437
+     * @throws BadRequest
438
+     * @throws NotImplemented
439
+     * @throws ServiceUnavailable
440
+     */
441
+    private function createFileChunked($data) {
442
+        list($path, $name) = \Sabre\Uri\split($this->path);
443
+
444
+        $info = \OC_FileChunking::decodeName($name);
445
+        if (empty($info)) {
446
+            throw new NotImplemented('Invalid chunk name');
447
+        }
448
+
449
+        $chunk_handler = new \OC_FileChunking($info);
450
+        $bytesWritten = $chunk_handler->store($info['index'], $data);
451
+
452
+        //detect aborted upload
453
+        if (isset ($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PUT') {
454
+            if (isset($_SERVER['CONTENT_LENGTH'])) {
455
+                $expected = (int)$_SERVER['CONTENT_LENGTH'];
456
+                if ($bytesWritten !== $expected) {
457
+                    $chunk_handler->remove($info['index']);
458
+                    throw new BadRequest(
459
+                        'expected filesize ' . $expected . ' got ' . $bytesWritten);
460
+                }
461
+            }
462
+        }
463
+
464
+        if ($chunk_handler->isComplete()) {
465
+            /** @var Storage $storage */
466
+            list($storage,) = $this->fileView->resolvePath($path);
467
+            $needsPartFile = $storage->needsPartFile();
468
+            $partFile = null;
469
+
470
+            $targetPath = $path . '/' . $info['name'];
471
+            /** @var \OC\Files\Storage\Storage $targetStorage */
472
+            list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
473
+
474
+            $exists = $this->fileView->file_exists($targetPath);
475
+
476
+            try {
477
+                $this->fileView->lockFile($targetPath, ILockingProvider::LOCK_SHARED);
478
+
479
+                $this->emitPreHooks($exists, $targetPath);
480
+                $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_EXCLUSIVE);
481
+                /** @var \OC\Files\Storage\Storage $targetStorage */
482
+                list($targetStorage, $targetInternalPath) = $this->fileView->resolvePath($targetPath);
483
+
484
+                if ($needsPartFile) {
485
+                    // we first assembly the target file as a part file
486
+                    $partFile = $this->getPartFileBasePath($path . '/' . $info['name']) . '.ocTransferId' . $info['transferid'] . '.part';
487
+                    /** @var \OC\Files\Storage\Storage $targetStorage */
488
+                    list($partStorage, $partInternalPath) = $this->fileView->resolvePath($partFile);
489
+
490
+
491
+                    $chunk_handler->file_assemble($partStorage, $partInternalPath);
492
+
493
+                    // here is the final atomic rename
494
+                    $renameOkay = $targetStorage->moveFromStorage($partStorage, $partInternalPath, $targetInternalPath);
495
+                    $fileExists = $targetStorage->file_exists($targetInternalPath);
496
+                    if ($renameOkay === false || $fileExists === false) {
497
+                        \OC::$server->getLogger()->error('\OC\Files\Filesystem::rename() failed', ['app' => 'webdav']);
498
+                        // only delete if an error occurred and the target file was already created
499
+                        if ($fileExists) {
500
+                            // set to null to avoid double-deletion when handling exception
501
+                            // stray part file
502
+                            $partFile = null;
503
+                            $targetStorage->unlink($targetInternalPath);
504
+                        }
505
+                        $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
506
+                        throw new Exception('Could not rename part file assembled from chunks');
507
+                    }
508
+                } else {
509
+                    // assemble directly into the final file
510
+                    $chunk_handler->file_assemble($targetStorage, $targetInternalPath);
511
+                }
512
+
513
+                // allow sync clients to send the mtime along in a header
514
+                if (isset($this->request->server['HTTP_X_OC_MTIME'])) {
515
+                    $mtime = $this->sanitizeMtime($this->request->server['HTTP_X_OC_MTIME']);
516
+                    if ($targetStorage->touch($targetInternalPath, $mtime)) {
517
+                        $this->header('X-OC-MTime: accepted');
518
+                    }
519
+                }
520
+
521
+                // since we skipped the view we need to scan and emit the hooks ourselves
522
+                $targetStorage->getUpdater()->update($targetInternalPath);
523
+
524
+                $this->fileView->changeLock($targetPath, ILockingProvider::LOCK_SHARED);
525
+
526
+                $this->emitPostHooks($exists, $targetPath);
527
+
528
+                // FIXME: should call refreshInfo but can't because $this->path is not the of the final file
529
+                $info = $this->fileView->getFileInfo($targetPath);
530
+
531
+                if (isset($this->request->server['HTTP_OC_CHECKSUM'])) {
532
+                    $checksum = trim($this->request->server['HTTP_OC_CHECKSUM']);
533
+                    $this->fileView->putFileInfo($targetPath, ['checksum' => $checksum]);
534
+                } else if ($info->getChecksum() !== null && $info->getChecksum() !== '') {
535
+                    $this->fileView->putFileInfo($this->path, ['checksum' => '']);
536
+                }
537
+
538
+                $this->fileView->unlockFile($targetPath, ILockingProvider::LOCK_SHARED);
539
+
540
+                return $info->getEtag();
541
+            } catch (\Exception $e) {
542
+                if ($partFile !== null) {
543
+                    $targetStorage->unlink($targetInternalPath);
544
+                }
545
+                $this->convertToSabreException($e);
546
+            }
547
+        }
548
+
549
+        return null;
550
+    }
551
+
552
+    /**
553
+     * Convert the given exception to a SabreException instance
554
+     *
555
+     * @param \Exception $e
556
+     *
557
+     * @throws \Sabre\DAV\Exception
558
+     */
559
+    private function convertToSabreException(\Exception $e) {
560
+        if ($e instanceof \Sabre\DAV\Exception) {
561
+            throw $e;
562
+        }
563
+        if ($e instanceof NotPermittedException) {
564
+            // a more general case - due to whatever reason the content could not be written
565
+            throw new Forbidden($e->getMessage(), 0, $e);
566
+        }
567
+        if ($e instanceof ForbiddenException) {
568
+            // the path for the file was forbidden
569
+            throw new DAVForbiddenException($e->getMessage(), $e->getRetry(), $e);
570
+        }
571
+        if ($e instanceof EntityTooLargeException) {
572
+            // the file is too big to be stored
573
+            throw new EntityTooLarge($e->getMessage(), 0, $e);
574
+        }
575
+        if ($e instanceof InvalidContentException) {
576
+            // the file content is not permitted
577
+            throw new UnsupportedMediaType($e->getMessage(), 0, $e);
578
+        }
579
+        if ($e instanceof InvalidPathException) {
580
+            // the path for the file was not valid
581
+            // TODO: find proper http status code for this case
582
+            throw new Forbidden($e->getMessage(), 0, $e);
583
+        }
584
+        if ($e instanceof LockedException || $e instanceof LockNotAcquiredException) {
585
+            // the file is currently being written to by another process
586
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
587
+        }
588
+        if ($e instanceof GenericEncryptionException) {
589
+            // returning 503 will allow retry of the operation at a later point in time
590
+            throw new ServiceUnavailable('Encryption not ready: ' . $e->getMessage(), 0, $e);
591
+        }
592
+        if ($e instanceof StorageNotAvailableException) {
593
+            throw new ServiceUnavailable('Failed to write file contents: ' . $e->getMessage(), 0, $e);
594
+        }
595
+
596
+        throw new \Sabre\DAV\Exception($e->getMessage(), 0, $e);
597
+    }
598
+
599
+    /**
600
+     * Get the checksum for this file
601
+     *
602
+     * @return string
603
+     */
604
+    public function getChecksum() {
605
+        return $this->info->getChecksum();
606
+    }
607
+
608
+    protected function header($string) {
609
+        \header($string);
610
+    }
611 611
 }
Please login to merge, or discard this patch.