Passed
Push — master ( f358a5...b5f949 )
by Pauli
03:17
created

CollectionService::getJson()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 14
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 Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2018 - 2025
11
 */
12
13
namespace OCA\Music\Service;
14
15
use OCA\Music\AppFramework\Core\Logger;
16
use OCA\Music\AppFramework\Db\UniqueConstraintViolationException;
17
use OCA\Music\BusinessLayer\Library;
18
use OCA\Music\Db\Cache;
19
20
use OCP\ICache;
21
22
/**
23
 * Utility to build and cache the monolithic json data describing the whole music library.
24
 *
25
 * There has to be a logged-in user to use this class, the userId is injected via the class
26
 * constructor.
27
 *
28
 * This class utilizes two caching mechanism: file-backed \OCP\ICache and database-backed
29
 * \OCA\Music\Db\Cache. The actual json data is stored in the former and a hash of the data
30
 * is stored into the latter. The hash is used as a flag indicating that the data is valid.
31
 * The rationale of this design is that the \OCP\ICache can be used only for the logged-in
32
 * user, but we must be able to invalidate the cache also in cases when the affected user is
33
 * not logged in (in FileHooks, ShareHooks, occ commands). On the other hand, depending on
34
 * the database configuration, the json data may be too large to store it to \OCA\Music\Db\Cache
35
 * (with tens of thousands of tracks, the size of the json may be more than 10 MB and the
36
 * DB may be configured with maximum object size of e.g. 1 MB).
37
 */
38
class CollectionService {
39
	private Library $library;
40
	private ICache $fileCache;
41
	private Cache $dbCache;
42
	private Logger $logger;
43
	private string $userId;
44
45
	public function __construct(
46
			Library $library,
47
			ICache $fileCache,
48
			Cache $dbCache,
49
			Logger $logger,
50
			?string $userId) {
51
		$this->library = $library;
52
		$this->fileCache = $fileCache;
53
		$this->dbCache = $dbCache;
54
		$this->logger = $logger;
55
		$this->userId = $userId ?? ''; // TODO: null makes no sense but we need it because ApiController may be constructed for public covers without a user
56
	}
57
58
	public function getJson() : string {
59
		$collectionJson = $this->getCachedJson();
60
61
		if ($collectionJson === null) {
62
			$collectionJson = \json_encode($this->library->toCollection($this->userId));
63
			try {
64
				$this->addJsonToCache($collectionJson);
65
			} catch (UniqueConstraintViolationException $ex) {
66
				$this->logger->log("Race condition: collection.json for user {$this->userId} ".
67
						"cached twice, ignoring latter.", 'warn');
68
			}
69
		}
70
71
		return $collectionJson;
72
	}
73
74
	public function getCachedJsonHash() : ?string {
75
		return $this->dbCache->get($this->userId, 'collection');
76
	}
77
78
	private function getCachedJson() : ?string {
79
		$json = null;
80
		$hash = $this->dbCache->get($this->userId, 'collection');
81
		if ($hash !== null) {
82
			$json = $this->fileCache->get('music_collection.json');
83
			if ($json === null) {
84
				$this->logger->log("Inconsistent collection state for user {$this->userId}: ".
85
						"Hash found from DB-backed cache but data not found from the ".
86
						"file-backed cache. Removing also the hash.", 'debug');
87
				$this->dbCache->remove($this->userId, 'collection');
88
			}
89
		}
90
		return $json;
91
	}
92
93
	private function addJsonToCache(string $json) : void {
94
		$hash = \hash('md5', $json);
95
		$this->dbCache->add($this->userId, 'collection', $hash);
96
		$this->fileCache->set('music_collection.json', $json, 5*365*24*60*60);
97
	}
98
}
99