Passed
Push — master ( 64bc7c...fc57f6 )
by Morris
16:05 queued 11s
created

Directory::copyInto()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 11
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 19
rs 9.9
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Jakob Sack <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Robin Appelman <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Vincent Petry <[email protected]>
17
 *
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program. If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OCA\DAV\Connector\Sabre;
35
36
use OC\Files\Mount\MoveableMount;
37
use OC\Files\View;
38
use OCA\DAV\Connector\Sabre\Exception\FileLocked;
39
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
40
use OCA\DAV\Connector\Sabre\Exception\InvalidPath;
41
use OCP\Files\FileInfo;
42
use OCP\Files\ForbiddenException;
43
use OCP\Files\InvalidPathException;
44
use OCP\Files\StorageNotAvailableException;
45
use OCP\Lock\ILockingProvider;
46
use OCP\Lock\LockedException;
47
use Sabre\DAV\Exception\BadRequest;
48
use Sabre\DAV\Exception\Locked;
49
use Sabre\DAV\Exception\NotFound;
50
use Sabre\DAV\Exception\ServiceUnavailable;
51
use Sabre\DAV\IFile;
52
use Sabre\DAV\INode;
53
54
class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
55
56
	/**
57
	 * Cached directory content
58
	 *
59
	 * @var \OCP\Files\FileInfo[]
60
	 */
61
	private $dirContent;
62
63
	/**
64
	 * Cached quota info
65
	 *
66
	 * @var array
67
	 */
68
	private $quotaInfo;
69
70
	/**
71
	 * @var ObjectTree|null
72
	 */
73
	private $tree;
74
75
	/**
76
	 * Sets up the node, expects a full path name
77
	 *
78
	 * @param \OC\Files\View $view
79
	 * @param \OCP\Files\FileInfo $info
80
	 * @param ObjectTree|null $tree
81
	 * @param \OCP\Share\IManager $shareManager
82
	 */
83
	public function __construct(View $view, FileInfo $info, $tree = null, $shareManager = null) {
84
		parent::__construct($view, $info, $shareManager);
85
		$this->tree = $tree;
86
	}
87
88
	/**
89
	 * Creates a new file in the directory
90
	 *
91
	 * Data will either be supplied as a stream resource, or in certain cases
92
	 * as a string. Keep in mind that you may have to support either.
93
	 *
94
	 * After successful creation of the file, you may choose to return the ETag
95
	 * of the new file here.
96
	 *
97
	 * The returned ETag must be surrounded by double-quotes (The quotes should
98
	 * be part of the actual string).
99
	 *
100
	 * If you cannot accurately determine the ETag, you should not return it.
101
	 * If you don't store the file exactly as-is (you're transforming it
102
	 * somehow) you should also not return an ETag.
103
	 *
104
	 * This means that if a subsequent GET to this new file does not exactly
105
	 * return the same contents of what was submitted here, you are strongly
106
	 * recommended to omit the ETag.
107
	 *
108
	 * @param string $name Name of the file
109
	 * @param resource|string $data Initial payload
110
	 * @return null|string
111
	 * @throws Exception\EntityTooLarge
112
	 * @throws Exception\UnsupportedMediaType
113
	 * @throws FileLocked
114
	 * @throws InvalidPath
115
	 * @throws \Sabre\DAV\Exception
116
	 * @throws \Sabre\DAV\Exception\BadRequest
117
	 * @throws \Sabre\DAV\Exception\Forbidden
118
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
119
	 */
120
	public function createFile($name, $data = null) {
121
		try {
122
			// for chunked upload also updating a existing file is a "createFile"
123
			// because we create all the chunks before re-assemble them to the existing file.
124
			if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
125
126
				// exit if we can't create a new file and we don't updatable existing file
127
				$chunkInfo = \OC_FileChunking::decodeName($name);
128
				if (!$this->fileView->isCreatable($this->path) &&
129
					!$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
130
				) {
131
					throw new \Sabre\DAV\Exception\Forbidden();
132
				}
133
			} else {
134
				// For non-chunked upload it is enough to check if we can create a new file
135
				if (!$this->fileView->isCreatable($this->path)) {
136
					throw new \Sabre\DAV\Exception\Forbidden();
137
				}
138
			}
139
140
			$this->fileView->verifyPath($this->path, $name);
141
142
			$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
143
			// in case the file already exists/overwriting
144
			$info = $this->fileView->getFileInfo($this->path . '/' . $name);
145
			if (!$info) {
146
				// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
147
				$info = new \OC\Files\FileInfo($path, null, null, [], null);
148
			}
149
			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
150
151
			// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
152
			$node->acquireLock(ILockingProvider::LOCK_SHARED);
153
			$this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
154
155
			$result = $node->put($data);
156
157
			$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
158
			$node->releaseLock(ILockingProvider::LOCK_SHARED);
159
			return $result;
160
		} catch (\OCP\Files\StorageNotAvailableException $e) {
161
			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
162
		} catch (InvalidPathException $ex) {
163
			throw new InvalidPath($ex->getMessage(), false, $ex);
164
		} catch (ForbiddenException $ex) {
165
			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
166
		} catch (LockedException $e) {
167
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
168
		}
169
	}
170
171
	/**
172
	 * Creates a new subdirectory
173
	 *
174
	 * @param string $name
175
	 * @throws FileLocked
176
	 * @throws InvalidPath
177
	 * @throws \Sabre\DAV\Exception\Forbidden
178
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
179
	 */
180
	public function createDirectory($name) {
181
		try {
182
			if (!$this->info->isCreatable()) {
183
				throw new \Sabre\DAV\Exception\Forbidden();
184
			}
185
186
			$this->fileView->verifyPath($this->path, $name);
187
			$newPath = $this->path . '/' . $name;
188
			if (!$this->fileView->mkdir($newPath)) {
189
				throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
190
			}
191
		} catch (\OCP\Files\StorageNotAvailableException $e) {
192
			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
193
		} catch (InvalidPathException $ex) {
194
			throw new InvalidPath($ex->getMessage());
195
		} catch (ForbiddenException $ex) {
196
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
197
		} catch (LockedException $e) {
198
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
199
		}
200
	}
201
202
	/**
203
	 * Returns a specific child node, referenced by its name
204
	 *
205
	 * @param string $name
206
	 * @param \OCP\Files\FileInfo $info
207
	 * @return \Sabre\DAV\INode
208
	 * @throws InvalidPath
209
	 * @throws \Sabre\DAV\Exception\NotFound
210
	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
211
	 */
212
	public function getChild($name, $info = null) {
213
		if (!$this->info->isReadable()) {
214
			// avoid detecting files through this way
215
			throw new NotFound();
216
		}
217
218
		$path = $this->path . '/' . $name;
219
		if (is_null($info)) {
220
			try {
221
				$this->fileView->verifyPath($this->path, $name);
222
				$info = $this->fileView->getFileInfo($path);
223
			} catch (\OCP\Files\StorageNotAvailableException $e) {
224
				throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
225
			} catch (InvalidPathException $ex) {
226
				throw new InvalidPath($ex->getMessage());
227
			} catch (ForbiddenException $e) {
228
				throw new \Sabre\DAV\Exception\Forbidden();
229
			}
230
		}
231
232
		if (!$info) {
233
			throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
234
		}
235
236
		if ($info['mimetype'] === 'httpd/unix-directory') {
237
			$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
238
		} else {
239
			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager);
240
		}
241
		if ($this->tree) {
242
			$this->tree->cacheNode($node);
243
		}
244
		return $node;
245
	}
246
247
	/**
248
	 * Returns an array with all the child nodes
249
	 *
250
	 * @return \Sabre\DAV\INode[]
251
	 * @throws \Sabre\DAV\Exception\Locked
252
	 * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
253
	 */
254
	public function getChildren() {
255
		if (!is_null($this->dirContent)) {
0 ignored issues
show
introduced by
The condition is_null($this->dirContent) is always false.
Loading history...
256
			return $this->dirContent;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->dirContent returns the type OCP\Files\FileInfo[] which is incompatible with the documented return type Sabre\DAV\INode[].
Loading history...
257
		}
258
		try {
259
			if (!$this->info->isReadable()) {
260
				// return 403 instead of 404 because a 404 would make
261
				// the caller believe that the collection itself does not exist
262
				throw new Forbidden('No read permissions');
263
			}
264
			$folderContent = $this->fileView->getDirectoryContent($this->path);
265
		} catch (LockedException $e) {
266
			throw new Locked();
267
		}
268
269
		$nodes = [];
270
		foreach ($folderContent as $info) {
271
			$node = $this->getChild($info->getName(), $info);
272
			$nodes[] = $node;
273
		}
274
		$this->dirContent = $nodes;
275
		return $this->dirContent;
276
	}
277
278
	/**
279
	 * Checks if a child exists.
280
	 *
281
	 * @param string $name
282
	 * @return bool
283
	 */
284
	public function childExists($name) {
285
		// note: here we do NOT resolve the chunk file name to the real file name
286
		// to make sure we return false when checking for file existence with a chunk
287
		// file name.
288
		// This is to make sure that "createFile" is still triggered
289
		// (required old code) instead of "updateFile".
290
		//
291
		// TODO: resolve chunk file name here and implement "updateFile"
292
		$path = $this->path . '/' . $name;
293
		return $this->fileView->file_exists($path);
294
	}
295
296
	/**
297
	 * Deletes all files in this directory, and then itself
298
	 *
299
	 * @return void
300
	 * @throws FileLocked
301
	 * @throws \Sabre\DAV\Exception\Forbidden
302
	 */
303
	public function delete() {
304
		if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
305
			throw new \Sabre\DAV\Exception\Forbidden();
306
		}
307
308
		try {
309
			if (!$this->fileView->rmdir($this->path)) {
310
				// assume it wasn't possible to remove due to permission issue
311
				throw new \Sabre\DAV\Exception\Forbidden();
312
			}
313
		} catch (ForbiddenException $ex) {
314
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
315
		} catch (LockedException $e) {
316
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
317
		}
318
	}
319
320
	/**
321
	 * Returns available diskspace information
322
	 *
323
	 * @return array
324
	 */
325
	public function getQuotaInfo() {
326
		if ($this->quotaInfo) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->quotaInfo of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
327
			return $this->quotaInfo;
328
		}
329
		try {
330
			$info = $this->fileView->getFileInfo($this->path, false);
331
			$storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $info);
332
			if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
333
				$free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
334
			} else {
335
				$free = $storageInfo['free'];
336
			}
337
			$this->quotaInfo = [
338
				$storageInfo['used'],
339
				$free
340
			];
341
			return $this->quotaInfo;
342
		} catch (\OCP\Files\StorageNotAvailableException $e) {
343
			return [0, 0];
344
		}
345
	}
346
347
	/**
348
	 * Moves a node into this collection.
349
	 *
350
	 * It is up to the implementors to:
351
	 *   1. Create the new resource.
352
	 *   2. Remove the old resource.
353
	 *   3. Transfer any properties or other data.
354
	 *
355
	 * Generally you should make very sure that your collection can easily move
356
	 * the move.
357
	 *
358
	 * If you don't, just return false, which will trigger sabre/dav to handle
359
	 * the move itself. If you return true from this function, the assumption
360
	 * is that the move was successful.
361
	 *
362
	 * @param string $targetName New local file/collection name.
363
	 * @param string $fullSourcePath Full path to source node
364
	 * @param INode $sourceNode Source node itself
365
	 * @return bool
366
	 * @throws BadRequest
367
	 * @throws ServiceUnavailable
368
	 * @throws Forbidden
369
	 * @throws FileLocked
370
	 * @throws \Sabre\DAV\Exception\Forbidden
371
	 */
372
	public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
373
		if (!$sourceNode instanceof Node) {
374
			// it's a file of another kind, like FutureFile
375
			if ($sourceNode instanceof IFile) {
376
				// fallback to default copy+delete handling
377
				return false;
378
			}
379
			throw new BadRequest('Incompatible node types');
380
		}
381
382
		if (!$this->fileView) {
383
			throw new ServiceUnavailable('filesystem not setup');
384
		}
385
386
		$destinationPath = $this->getPath() . '/' . $targetName;
387
388
389
		$targetNodeExists = $this->childExists($targetName);
390
391
		// at getNodeForPath we also check the path for isForbiddenFileOrDir
392
		// with that we have covered both source and destination
393
		if ($sourceNode instanceof Directory && $targetNodeExists) {
394
			throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
395
		}
396
397
		[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
398
		$destinationDir = $this->getPath();
399
400
		$sourcePath = $sourceNode->getPath();
401
402
		$isMovableMount = false;
403
		$sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
404
		$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
405
		if ($sourceMount instanceof MoveableMount && $internalPath === '') {
406
			$isMovableMount = true;
407
		}
408
409
		try {
410
			$sameFolder = ($sourceDir === $destinationDir);
411
			// if we're overwriting or same folder
412
			if ($targetNodeExists || $sameFolder) {
413
				// note that renaming a share mount point is always allowed
414
				if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
415
					throw new \Sabre\DAV\Exception\Forbidden();
416
				}
417
			} else {
418
				if (!$this->fileView->isCreatable($destinationDir)) {
419
					throw new \Sabre\DAV\Exception\Forbidden();
420
				}
421
			}
422
423
			if (!$sameFolder) {
424
				// moving to a different folder, source will be gone, like a deletion
425
				// note that moving a share mount point is always allowed
426
				if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
427
					throw new \Sabre\DAV\Exception\Forbidden();
428
				}
429
			}
430
431
			$fileName = basename($destinationPath);
432
			try {
433
				$this->fileView->verifyPath($destinationDir, $fileName);
434
			} catch (InvalidPathException $ex) {
435
				throw new InvalidPath($ex->getMessage());
436
			}
437
438
			$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
439
			if (!$renameOkay) {
440
				throw new \Sabre\DAV\Exception\Forbidden('');
441
			}
442
		} catch (StorageNotAvailableException $e) {
443
			throw new ServiceUnavailable($e->getMessage());
444
		} catch (ForbiddenException $ex) {
445
			throw new Forbidden($ex->getMessage(), $ex->getRetry());
446
		} catch (LockedException $e) {
447
			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
448
		}
449
450
		return true;
451
	}
452
453
454
	public function copyInto($targetName, $sourcePath, INode $sourceNode) {
455
		if ($sourceNode instanceof File) {
456
			$destinationPath = $this->getPath() . '/' . $targetName;
457
			$sourcePath = $sourceNode->getPath();
458
459
			if (!$this->fileView->isCreatable($this->getPath())) {
460
				throw new \Sabre\DAV\Exception\Forbidden();
461
			}
462
463
			try {
464
				$this->fileView->verifyPath($this->getPath(), $targetName);
465
			} catch (InvalidPathException $ex) {
466
				throw new InvalidPath($ex->getMessage());
467
			}
468
469
			return $this->fileView->copy($sourcePath, $destinationPath);
470
		}
471
472
		return false;
473
	}
474
}
475