Passed
Push — master ( 790a65...e045ee )
by Pauli
02:14
created

Maintenance::resetAllData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
nc 2
nop 1
dl 0
loc 13
rs 9.9666
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 Morris Jobke <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Morris Jobke 2014
12
 * @copyright Pauli Järvinen 2017 - 2022
13
 */
14
15
namespace OCA\Music\Db;
16
17
use OCP\IDBConnection;
18
19
use OCA\Music\AppFramework\Core\Logger;
20
21
class Maintenance {
22
23
	/** @var IDBConnection */
24
	private $db;
25
	/** @var Logger */
26
	private $logger;
27
28
	public function __construct(IDBConnection $db, Logger $logger) {
29
		$this->db = $db;
30
		$this->logger = $logger;
31
	}
32
33
	/**
34
	 * Remove cover_file_id from album if the corresponding file does not exist
35
	 */
36
	private function removeObsoleteCoverImagesFromTable($table) {
37
		return $this->db->executeUpdate(
38
			"UPDATE `*PREFIX*$table` SET `cover_file_id` = NULL
39
			WHERE `cover_file_id` IS NOT NULL AND `cover_file_id` IN (
40
				SELECT `cover_file_id` FROM (
41
					SELECT `cover_file_id` FROM `*PREFIX*$table`
42
					LEFT JOIN `*PREFIX*filecache`
43
						ON `cover_file_id`=`fileid`
44
					WHERE `fileid` IS NULL
45
				) mysqlhack
46
			)"
47
		);
48
	}
49
50
	/**
51
	 * Remove cover_file_id from album if the corresponding file does not exist
52
	 */
53
	private function removeObsoleteAlbumCoverImages() {
54
		return $this->removeObsoleteCoverImagesFromTable('music_albums');
55
	}
56
57
	/**
58
	 * Remove cover_file_id from artist if the corresponding file does not exist
59
	 */
60
	private function removeObsoleteArtistCoverImages() {
61
		return $this->removeObsoleteCoverImagesFromTable('music_artists');
62
	}
63
64
	/**
65
	 * Remove all such rows from $tgtTable which don't have corresponding rows in $refTable
66
	 * so that $tgtTableKey = $refTableKey.
67
	 * @param string $tgtTable
68
	 * @param string $refTable
69
	 * @param string $tgtTableKey
70
	 * @param string $refTableKey
71
	 * @param string|null $extraCond
72
	 * @return int Number of removed rows
73
	 */
74
	private function removeUnreferencedDbRows($tgtTable, $refTable, $tgtTableKey, $refTableKey, $extraCond=null) {
75
		$tgtTable = '*PREFIX*' . $tgtTable;
76
		$refTable = '*PREFIX*' . $refTable;
77
78
		return $this->db->executeUpdate(
79
			"DELETE FROM `$tgtTable` WHERE `id` IN (
80
				SELECT `id` FROM (
81
					SELECT `$tgtTable`.`id`
82
					FROM `$tgtTable` LEFT JOIN `$refTable`
83
					ON `$tgtTable`.`$tgtTableKey` = `$refTable`.`$refTableKey`
84
					WHERE `$refTable`.`$refTableKey` IS NULL
85
				) mysqlhack
86
			)"
87
			.
88
			(empty($extraCond) ? '' : " AND $extraCond")
89
		);
90
	}
91
92
	/**
93
	 * Remvoe tracks which do not have corresponding file in the file system
94
	 * @return int Number of removed tracks
95
	 */
96
	private function removeObsoleteTracks() {
97
		return $this->removeUnreferencedDbRows('music_tracks', 'filecache', 'file_id', 'fileid');
98
	}
99
100
	/**
101
	 * Remove tracks which belong to non-existing album
102
	 * @return int Number of removed tracks
103
	 */
104
	private function removeTracksWithNoAlbum() {
105
		return $this->removeUnreferencedDbRows('music_tracks', 'music_albums', 'album_id', 'id');
106
	}
107
108
	/**
109
	 * Remove tracks which are performed by non-existing artist
110
	 * @return int Number of removed tracks
111
	 */
112
	private function removeTracksWithNoArtist() {
113
		return $this->removeUnreferencedDbRows('music_tracks', 'music_artists', 'artist_id', 'id');
114
	}
115
116
	/**
117
	 * Remove albums which have no tracks
118
	 * @return int Number of removed albums
119
	 */
120
	private function removeObsoleteAlbums() {
121
		return $this->removeUnreferencedDbRows('music_albums', 'music_tracks', 'id', 'album_id');
122
	}
123
124
	/**
125
	 * Remove albums which have a non-existing album artist
126
	 * @return int Number of removed albums
127
	 */
128
	private function removeAlbumsWithNoArtist() {
129
		return $this->removeUnreferencedDbRows('music_albums', 'music_artists', 'album_artist_id', 'id');
130
	}
131
132
	/**
133
	 * Remove artists which have no albums and no tracks
134
	 * @return int Number of removed artists
135
	 */
136
	private function removeObsoleteArtists() {
137
		// Note: This originally used the NOT IN operation but that was terribly inefficient on PostgreSQL,
138
		// see https://github.com/owncloud/music/issues/997
139
		return $this->db->executeUpdate(
140
			'DELETE FROM `*PREFIX*music_artists`
141
				WHERE NOT EXISTS (SELECT 1 FROM `*PREFIX*music_albums` WHERE `*PREFIX*music_artists`.`id` = `album_artist_id` LIMIT 1)
142
				AND   NOT EXISTS (SELECT 1 FROM `*PREFIX*music_tracks` WHERE `*PREFIX*music_artists`.`id` = `artist_id` LIMIT 1)'
143
		);
144
	}
145
146
	/**
147
	 * Remove bookmarks referring tracks which do not exist
148
	 * @return int Number of removed bookmarks
149
	 */
150
	private function removeObsoleteBookmarks() {
151
		return $this->removeUnreferencedDbRows('music_bookmarks', 'music_tracks', 'entry_id', 'id', '`type` = 1')
152
			+ $this->removeUnreferencedDbRows('music_bookmarks', 'music_podcast_episodes', 'entry_id', 'id', '`type` = 2');
153
	}
154
155
	/**
156
	 * Remove podcast episodes which have a non-existing podcast channel
157
	 * @return int Number of removed albums
158
	 */
159
	private function removeObsoletePodcastEpisodes() {
160
		return $this->removeUnreferencedDbRows('music_podcast_episodes', 'music_podcast_channels', 'channel_id', 'id');
161
	}
162
163
	/**
164
	 * Removes orphaned data from the database
165
	 * @return array describing the number of removed entries per type
166
	 */
167
	public function cleanUp() : array {
168
		$removedCovers = $this->removeObsoleteAlbumCoverImages();
169
		$removedCovers += $this->removeObsoleteArtistCoverImages();
170
171
		$removedTracks = $this->removeObsoleteTracks();
172
		$removedAlbums = $this->removeObsoleteAlbums();
173
		$removedArtists = $this->removeObsoleteArtists();
174
		$removedBookmarks = $this->removeObsoleteBookmarks();
175
		$removedEpisodes = $this->removeObsoletePodcastEpisodes();
176
177
		$removedAlbums += $this->removeAlbumsWithNoArtist();
178
		$removedTracks += $this->removeTracksWithNoAlbum();
179
		$removedTracks += $this->removeTracksWithNoArtist();
180
181
		return [
182
			'covers' => $removedCovers,
183
			'artists' => $removedArtists,
184
			'albums' => $removedAlbums,
185
			'tracks' => $removedTracks,
186
			'bookmarks' => $removedBookmarks,
187
			'podcast_episodes' => $removedEpisodes
188
		];
189
	}
190
191
	private function resetTable(string $table, ?string $userId, bool $allUsers = false) : void {
0 ignored issues
show
Unused Code introduced by
The method resetTable() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
192
		if ($userId && $allUsers) {
193
			throw new \InvalidArgumentException('userId should be null if allUsers targeted');
194
		}
195
196
		$params = [];
197
		$sql = "DELETE FROM `*PREFIX*music_$table`";
198
		if (!$allUsers) {
199
			$sql .=  ' WHERE `user_id` = ?';
200
			$params[] = $userId;
201
		}
202
		$this->db->executeUpdate($sql, $params);
203
204
	}
205
206
	/**
207
	 * Wipe clean the music library of the given user, or all users
208
	 */
209
	public function resetLibrary(?string $userId, bool $allUsers = false) : void {
210
		$tables = [
211
			'tracks`',
212
			'albums`',
213
			'artists`',
214
			'playlists`',
215
			'genres`',
216
			'bookmarks`',
217
			'cache`'
218
		];
219
220
		foreach ($tables as $table) {
221
			resetTable($table, $userId, $allUsers);
0 ignored issues
show
Bug introduced by
The function resetTable was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

221
			/** @scrutinizer ignore-call */ 
222
   resetTable($table, $userId, $allUsers);
Loading history...
222
		}
223
224
		if ($allUsers) {
225
			$this->logger->log("Erased music databases of all users", 'info');
226
		} else {
227
			$this->logger->log("Erased music database of user $userId", 'info');
228
		}
229
	}
230
231
	/**
232
	 * Wipe clean all the music of the given user, including the library, podcasts, radio, Ampache/Subsonic keys
233
	 */
234
	public function resetAllData(string $userId) : void {
235
		$this->resetLibrary($userId);
236
237
		$tables = [
238
			'ampache_sessions`',
239
			'ampache_users`',
240
			'podcast_channels`',
241
			'podcast_episodes`',
242
			'radio_stations`'
243
		];
244
245
		foreach ($tables as $table) {
246
			resetTable($table, $userId);
0 ignored issues
show
Bug introduced by
The function resetTable was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

246
			/** @scrutinizer ignore-call */ 
247
   resetTable($table, $userId);
Loading history...
247
		}
248
	}
249
}
250