Passed
Push — master ( 6670ca...130e3c )
by Pauli
03:33
created

FileSystemService::addExternalMountsToFoldersLut()   B

Complexity

Conditions 10
Paths 17

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 21
nc 17
nop 3
dl 0
loc 35
rs 7.6666
c 1
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Service;
14
15
use OCA\Music\AppFramework\Core\Logger;
16
use OCA\Music\Db\Track;
17
use OCA\Music\Db\TrackMapper;
18
use OCA\Music\Utility\ArrayUtil;
19
20
use OCP\AppFramework\Db\DoesNotExistException;
21
use OCP\Files\FileInfo;
22
use OCP\Files\Folder;
23
24
class FileSystemService {
25
26
	private TrackMapper $mapper;
27
	private Logger $logger;
28
29
	public function __construct(TrackMapper $trackMapper, Logger $logger) {
30
		$this->mapper = $trackMapper;
31
		$this->logger = $logger;
32
	}
33
34
	/**
35
	 * Returns all folders of the user containing indexed tracks, along with the contained track IDs
36
	 * @return array of entries like {id: int, name: string, parent: ?int, trackIds: int[]}
37
	 */
38
	public function findAllFolders(string $userId, Folder $musicFolder) : array {
39
		// All tracks of the user, grouped by their parent folders. Some of the parent folders
40
		// may be owned by other users and are invisible to this user (in case of shared files).
41
		$trackIdsByFolder = $this->mapper->findTrackAndFolderIds($userId);
42
		$foldersLut = $this->getFoldersLut($trackIdsByFolder, $userId, $musicFolder);
43
		return \array_map(
44
			fn($id, $folderInfo) => \array_merge($folderInfo, ['id' => $id]),
45
			\array_keys($foldersLut), $foldersLut
46
		);
47
	}
48
49
	/**
50
	 * @param Track[] $tracks (in|out)
51
	 */
52
	public function injectFolderPathsToTracks(array $tracks, string $userId, Folder $musicFolder) : void {
53
		$folderIds = \array_map(fn($t) => $t->getFolderId(), $tracks);
54
		$folderIds = \array_unique($folderIds);
55
		$trackIdsByFolder = \array_fill_keys($folderIds, []); // track IDs are not actually used here so we can use empty arrays
56
57
		$foldersLut = $this->getFoldersLut($trackIdsByFolder, $userId, $musicFolder);
58
59
		// recursive helper to get folder's path and cache all parent paths on the way
60
		$getFolderPath = function(int $id, array &$foldersLut) use (&$getFolderPath) : string {
61
			// setup the path if not cached already
62
			if (!isset($foldersLut[$id]['path'])) {
63
				$parentId = $foldersLut[$id]['parent'];
64
				if ($parentId === null) {
65
					$foldersLut[$id]['path'] = '';
66
				} else {
67
					$foldersLut[$id]['path'] = $getFolderPath($parentId, $foldersLut) . '/' . $foldersLut[$id]['name'];
68
				}
69
			}
70
			return $foldersLut[$id]['path'];
71
		};
72
73
		foreach ($tracks as $track) {
74
			$track->setFolderPath($getFolderPath($track->getFolderId(), $foldersLut));
75
		}
76
	}
77
78
	/**
79
	 * Find all direct and indirect sub folders of the given folder. The result will include also the start folder.
80
	 * NOTE: This does not return the mounted or shared folders even in case the $folderId points to user home directory.
81
	 * @return int[]
82
	 */
83
	public function findAllDescendantFolders(int $folderId) : array {
84
		$descendants = [];
85
		$foldersToProcess = [$folderId];
86
87
		while(\count($foldersToProcess)) {
88
			$descendants = \array_merge($descendants, $foldersToProcess);
89
			$foldersToProcess = $this->mapper->findSubFolderIds($foldersToProcess);
90
		}
91
92
		return $descendants;
93
	}
94
95
	/**
96
	 * Get folder info lookup table, for the given tracks. The table will contain all the predecessor folders
97
	 * between those tracks and the root music folder (inclusive).
98
	 * 
99
	 * @param array $trackIdsByFolder Keys are folder IDs and values are arrays of track IDs
100
	 * @return array Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]]
101
	 */
102
	private function getFoldersLut(array $trackIdsByFolder, string $userId, Folder $musicFolder) : array {
103
		// Get the folder names and direct parent folder IDs directly from the DB.
104
		// This is significantly more efficient than using the Files API because we need to
105
		// run only single DB query instead of one per folder.
106
		$folderNamesAndParents = $this->mapper->findNodeNamesAndParents(\array_keys($trackIdsByFolder));
107
108
		// Compile the look-up-table entries from our two intermediary arrays
109
		$lut = [];
110
		foreach ($trackIdsByFolder as $folderId => $trackIds) {
111
			// $folderId is not found from $folderNamesAndParents if it's a dummy ID created as placeholder on a malformed playlist
112
			$nameAndParent = $folderNamesAndParents[$folderId] ?? ['name' => '', 'parent' => null];
113
			$lut[$folderId] = \array_merge($nameAndParent, ['trackIds' => $trackIds]);
114
		}
115
116
		// the root folder should have null parent; here we also ensure it's included
117
		$rootFolderId = $musicFolder->getId();
118
		$rootTracks = $lut[$rootFolderId]['trackIds'] ?? [];
119
		$lut[$rootFolderId] = ['name' => '', 'parent' => null, 'trackIds' => $rootTracks];
120
121
		// External mounts and shared files/folders need some special handling. But if there are any, they should be found
122
		// right under the top-level folder.
123
		$this->addExternalMountsToFoldersLut($lut, $userId, $musicFolder);
124
125
		// Add the intermediate folders which do not directly contain any tracks
126
		$this->addMissingParentsToFoldersLut($lut);
127
128
		return $lut;
129
	}
130
131
	/**
132
	 * Add externally mounted folders and shared files and folders to the folder LUT if there are any under the $musicFolder
133
	 * 
134
	 * @param array $lut (in|out) Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]]
135
	 */
136
	private function addExternalMountsToFoldersLut(array &$lut, string $userId, Folder $musicFolder) : void {
137
		$nodesUnderRoot = $musicFolder->getDirectoryListing();
138
		$homeStorageId = $musicFolder->getStorage()->getId();
139
		$rootFolderId = $musicFolder->getId();
140
141
		foreach ($nodesUnderRoot as $node) {
142
			if ($node->getStorage()->getId() != $homeStorageId) {
143
				// shared file/folder or external mount
144
				if ($node->getType() == FileInfo::TYPE_FOLDER) {
145
					// The mount point folders are always included in the result. At this time, we don't know if
146
					// they actually contain any tracks, unless they have direct track children. If there are direct tracks,
147
					// then the parent ID is incorrectly set and needs to be overridden.
148
					$trackIds = $lut[$node->getId()]['trackIds'] ?? [];
149
					$lut[$node->getId()] = ['name' => $node->getName(), 'parent' => $rootFolderId, 'trackIds' => $trackIds];
150
151
				} else if ($node->getMimePart() == 'audio') {
152
					// shared audio file, check if it's actually a scanned file in our library
153
					try {
154
						$sharedTrack = $this->mapper->findByFileId($node->getId(), $userId);
155
						$trackId = $sharedTrack->getId();
156
						foreach ($lut as $folderId => &$entry) {
157
							$trackIdIdx = \array_search($trackId, $entry['trackIds']);
158
							if ($trackIdIdx !== false) {
159
								// move the track from it's actual parent (in other user's storage) to our root
160
								unset($entry['trackIds'][$trackIdIdx]);
161
								$lut[$rootFolderId]['trackIds'][] = $trackId;
162
163
								// remove the former parent folder if it has no more tracks and it's not one of the mount point folders
164
								if (\count($entry['trackIds']) == 0 && empty(\array_filter($nodesUnderRoot, fn($n) => $n->getId() == $folderId))) {
165
									unset($lut[$folderId]);
166
								}
167
								break;
168
							}
169
						}
170
					} catch (DoesNotExistException $e) {
171
						// ignore
172
					}
173
				}
174
			}
175
		}
176
	}
177
178
	/**
179
	 * Add any missing intermediary folder to the LUT. For this function to work correctly, the pre-condition is that the LUT contains
180
	 * a root node which is predecessor of all other contained nodes and has 'parent' set as null.
181
	 * 
182
	 * @param array $lut (in|out) Keys are folder IDs and values are arrays like ['name' : string, 'parent' : int, 'trackIds' : int[]]
183
	 */
184
	private function addMissingParentsToFoldersLut(array &$lut) : void {
185
		$foldersToProcess = $lut;
186
187
		while (\count($foldersToProcess)) {
188
			$parentIds = \array_unique(\array_column($foldersToProcess, 'parent'));
189
			// do not process root even if it's included in $foldersToProcess
190
			$parentIds = \array_filter($parentIds, fn($i) => $i !== null);
191
			$parentIds = ArrayUtil::diff($parentIds, \array_keys($lut));
192
			$parentFolders = $this->mapper->findNodeNamesAndParents($parentIds);
193
194
			$foldersToProcess = [];
195
			foreach ($parentFolders as $folderId => $nameAndParent) {
196
				$foldersToProcess[] = $lut[$folderId] = \array_merge($nameAndParent, ['trackIds' => []]);
197
			}
198
		}
199
	}
200
201
}