Passed
Push — master ( 124be7...f358a5 )
by Pauli
03:35 queued 16s
created

FilesUtil::sanitizeFileName()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 17
nc 6
nop 2
dl 0
loc 32
rs 9.7
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2025
11
 */
12
13
namespace OCA\Music\Utility;
14
15
use OCA\Music\AppFramework\Utility\FileExistsException;
16
use OCP\Files\File;
17
use OCP\Files\Folder;
18
19
/**
20
 * Miscellaneous static utility functions for working with the cloud file system
21
 */
22
class FilesUtil {
23
24
	/**
25
	 * Get a Folder object using a parent Folder object and a relative path
26
	 */
27
	public static function getFolderFromRelativePath(Folder $parentFolder, string $relativePath) : Folder {
28
		if ($relativePath !== '/' && $relativePath !== '') {
29
			$node = $parentFolder->get($relativePath);
30
			if ($node instanceof Folder) {
31
				return $node;
32
			} else {
33
				throw new \InvalidArgumentException('Path points to a file while folder expected');
34
			}
35
		} else {
36
			return $parentFolder;
37
		}
38
	}
39
40
	/**
41
	 * Create relative path from the given working dir (CWD) to the given target path
42
	 * @param string $cwdPath Absolute CWD path
43
	 * @param string $targetPath Absolute target path
44
	 */
45
	public static function relativePath(string $cwdPath, string $targetPath) : string {
46
		$cwdParts = \explode('/', $cwdPath);
47
		$targetParts = \explode('/', $targetPath);
48
49
		// remove the common prefix of the paths
50
		while (\count($cwdParts) > 0 && \count($targetParts) > 0 && $cwdParts[0] === $targetParts[0]) {
51
			\array_shift($cwdParts);
52
			\array_shift($targetParts);
53
		}
54
55
		// prepend up-navigation from CWD to the closest common parent folder with the target
56
		for ($i = 0, $count = \count($cwdParts); $i < $count; ++$i) {
57
			\array_unshift($targetParts, '..');
58
		}
59
60
		return \implode('/', $targetParts);
61
	}
62
63
	/**
64
	 * Given a current working directory path (CWD) and a relative path (possibly containing '..' parts),
65
	 * form an absolute path matching the relative path. This is a reverse operation for FilesUtil::relativePath().
66
	 */
67
	public static function resolveRelativePath(string $cwdPath, string $relativePath) : string {
68
		$cwdParts = \explode('/', $cwdPath);
69
		$relativeParts = \explode('/', $relativePath);
70
71
		// get rid of the trailing empty part of CWD which appears when CWD has a trailing '/'
72
		if ($cwdParts[\count($cwdParts)-1] === '') {
73
			\array_pop($cwdParts);
74
		}
75
76
		foreach ($relativeParts as $part) {
77
			if ($part === '..') {
78
				\array_pop($cwdParts);
79
			} else {
80
				\array_push($cwdParts, $part);
81
			}
82
		}
83
84
		return \implode('/', $cwdParts);
85
	}
86
87
	/**
88
	 * @param ?string[] $validExtensions If defined, the output file is checked to have one of these extensions.
89
	 * 									If the extension is not already present, the first extension of the array
90
	 * 									is appended to the filename.
91
	 * @return string Sanitized file name
92
	 */
93
	public static function sanitizeFileName(string $filename, ?array $validExtensions=null) : string {
94
		// File names cannot contain the '/' character on Linux
95
		$filename = \str_replace('/', '-', $filename);
96
97
		// separate the file extension
98
		$parts = \pathinfo($filename);
99
		$ext = $parts['extension'] ?? '';
100
101
		// enforce proper extension if defined
102
		if (!empty($validExtensions)) {
103
			$validExtensions = \array_map('mb_strtolower', $validExtensions); // normalize to lower case
104
			if (!\in_array(\mb_strtolower($ext), $validExtensions)) {
105
				// no extension or invalid extension, append the proper one and keep any original extension in $filename
106
				$ext = $validExtensions[0];
107
			} else {
108
				$filename = $parts['filename']; // without the extension
109
			}
110
		}
111
112
		// In owncloud/Nextcloud, the whole file name must fit 250 characters, including the file extension.
113
		$maxLength = 250 - \strlen($ext) - 1;
114
		$filename = Util::truncate($filename, $maxLength);
115
		// Reserve another 5 characters to fit the postfix like " (xx)" on name collisions, unless there is such postfix already.
116
		// If there are more than 100 exports of the same playlist with overly long name, then this function will fail but we can live with that :).
117
		$matches = null;
118
		\assert($filename !== null); // for Scrutinizer, cannot be null
119
		if (\preg_match('/.+\(\d+\)$/', $filename, $matches) !== 1) {
120
			$maxLength -= 5;
121
			$filename = Util::truncate($filename, $maxLength);
122
		}
123
124
		return "$filename.$ext";
125
	}
126
127
	/**
128
	 * @param Folder $targetFolder target parent folder
129
	 * @param string $filename target file name
130
	 * @param string $collisionMode action to take on file name collision,
131
	 *								supported values:
132
	 *								- 'overwrite' The existing file will be overwritten
133
	 *								- 'keepboth' The new file is named with a suffix to make it unique
134
	 *								- 'abort' (default) The operation will fail
135
	 * @return File the newly created file
136
	 * @throws FileExistsException on name conflict if $collisionMode == 'abort'
137
	 * @throws \OCP\Files\NotPermittedException if the user is not allowed to write to the given folder
138
	 */
139
	public static function createFile(Folder $targetFolder, string $filename, string $collisionMode) : File {
140
		if ($targetFolder->nodeExists($filename)) {
141
			switch ($collisionMode) {
142
				case 'overwrite':
143
					$targetFolder->get($filename)->delete();
144
					break;
145
				case 'keepboth':
146
					$filename = $targetFolder->getNonExistingName($filename);
147
					break;
148
				default:
149
					throw new FileExistsException(
150
						$targetFolder->get($filename)->getPath(),
151
						$targetFolder->getNonExistingName($filename)
152
					);
153
			}
154
		}
155
		return $targetFolder->newFile($filename);
156
	}
157
}