Issues (40)

lib/Hooks/FileHooks.php (1 issue)

Severity
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 - 2025
13
 */
14
15
namespace OCA\Music\Hooks;
16
17
use OCA\Music\AppFramework\Core\Logger;
18
use OCP\Files\IRootFolder;
19
use OCP\Files\FileInfo;
20
use OCP\Files\Node;
21
22
use OCA\Music\AppInfo\Application;
23
use OCA\Music\BusinessLayer\TrackBusinessLayer;
24
use OCA\Music\Service\Scanner;
25
26
class FileHooks {
27
	private IRootFolder $filesystemRoot;
28
29
	public function __construct(IRootFolder $filesystemRoot) {
30
		$this->filesystemRoot = $filesystemRoot;
31
	}
32
33
	/**
34
	 * Invoke auto update of music database after file or folder deletion
35
	 * @param Node $node pointing to the file or folder
36
	 */
37
	private static function deleted(Node $node) : void {
38
		$scanner = self::inject(Scanner::class);
39
40
		if ($node->getType() == FileInfo::TYPE_FILE) {
41
			$scanner->delete($node->getId());
42
		} else {
43
			$scanner->deleteFolder($node);
44
		}
45
	}
46
47
	/**
48
	 * Invoke auto update of music database after file update or file creation
49
	 * @param Node $node pointing to the file
50
	 */
51
	private static function updated(Node $node) : void {
52
		// At least on Nextcloud 13, it sometimes happens that this hook is triggered
53
		// when the core creates some temporary file and trying to access the provided
54
		// node throws an exception, probably because the temp file is already removed
55
		// by the time the execution gets here. See #636.
56
		// Furthermore, when the core opens a file in stream mode for writing using
57
		// File::fopen, this hook gets triggered immediately after the opening succeeds,
58
		// before anything is actually written and while the file is *exclusively locked
59
		// because of the write mode*. See #638.
60
		try {
61
			self::handleUpdated($node);
62
		} catch (\OCP\Files\NotFoundException $e) {
63
			$logger = self::inject(Logger::class);
64
			$logger->warning('FileHooks::updated triggered for a non-existing file');
65
		} catch (\OCP\Lock\LockedException $e) {
66
			$logger = self::inject(Logger::class);
67
			$logger->warning('FileHooks::updated triggered for a locked file ' . $node->getName());
68
		}
69
	}
70
71
	private static function handleUpdated(Node $node) : void {
72
		// we are interested only about updates on files, not on folders
73
		if ($node->getType() == FileInfo::TYPE_FILE) {
74
			$scanner = self::inject(Scanner::class);
75
			$userId = self::getUser($node);
76
77
			// Ignore event if we got no user or folder or the user has not yet scanned the music
78
			// collection. The last condition is especially to prevent problems when creating new user
79
			// and the default file set contains one or more audio files (see the discussion in #638).
80
			if (!empty($userId) && self::userHasMusicLib($userId)) {
81
				$scanner->update($node, $userId, $node->getPath());
82
			}
83
		}
84
	}
85
86
	private static function moved(Node $node) : void {
87
		try {
88
			self::handleMoved($node);
89
		} catch (\OCP\Files\NotFoundException $e) {
90
			$logger = self::inject(Logger::class);
91
			$logger->warning('FileHooks::moved triggered for a non-existing file');
92
		} catch (\OCP\Lock\LockedException $e) {
93
			$logger = self::inject(Logger::class);
94
			$logger->warning('FileHooks::moved triggered for a locked file ' . $node->getName());
95
		}
96
	}
97
98
	private static function handleMoved(Node $node) : void {
99
		$scanner = self::inject(Scanner::class);
100
		$userId = self::getUser($node);
101
102
		if (!empty($userId) && self::userHasMusicLib($userId)) {
103
			if ($node->getType() == FileInfo::TYPE_FILE) {
104
				$scanner->fileMoved($node, $userId);
105
			} else {
106
				$scanner->folderMoved($node, $userId);
107
			}
108
		}
109
	}
110
111
	private static function getUser(Node $node) : ?string {
112
		$userId = self::inject('userId');
113
114
		// When a file is uploaded to a folder shared by link, we end up here without current user.
115
		// In that case, fall back to using file owner
116
		if (empty($userId)) {
117
			// At least some versions of NC may violate their PhpDoc and return null owner, hence we need to aid PHPStan a bit about the type.
118
			/** @var \OCP\IUser|null $owner */
119
			$owner = $node->getOwner();
120
			$userId = $owner ? $owner->getUID() : null;
0 ignored issues
show
$owner is of type OCP\IUser, thus it always evaluated to true.
Loading history...
121
		}
122
123
		return $userId;
124
	}
125
126
	/**
127
	 * Get the dependency identified by the given name
128
	 * @return mixed
129
	 */
130
	private static function inject(string $id) {
131
		$app = \OC::$server->query(Application::class);
132
		return $app->get($id);
133
	}
134
135
	/**
136
	 * Check if user has any scanned tracks in his/her music library
137
	 */
138
	private static function userHasMusicLib(string $userId) : bool {
139
		$trackBusinessLayer = self::inject(TrackBusinessLayer::class);
140
		return 0 < $trackBusinessLayer->count($userId);
141
	}
142
143
	private static function postRenamed(Node $source, Node $target) : void {
144
		// Beware: the $source describes the past state of the file and some of its functions will throw upon calling
145
146
		if ($source->getParent()->getId() != $target->getParent()->getId()) {
147
			self::moved($target);
148
		} else {
149
			self::updated($target);
150
		}
151
	}
152
153
	private static function safeExecute(callable $func) : void {
154
		// Don't let any exceptions or errors leak out of this method, no matter what unforeseen oddities happen.
155
		// We never want to prevent the actual file operation since our reactions to them are anyway non-crucial.
156
		// Especially during a server version update involving also Music app version update, the system may be
157
		// running a partially updated application version and that may lead to unexpected fatal errors, see
158
		// https://github.com/owncloud/music/issues/1231.
159
		try {
160
			try {
161
				$func();
162
			} catch (\Throwable $error) {
163
				$logger = self::inject(Logger::class);
164
				$logger->error("Error occurred while executing Music app file hook: {$error->getMessage()}. Stack trace: {$error->getTraceAsString()}");
165
			}
166
		} catch (\Throwable $error) {
167
			// even logging the error failed so just ignore
168
		}
169
	}
170
171
	public static function safeUpdated(Node $node) : void {
172
		self::safeExecute(fn() => self::updated($node));
173
	}
174
175
	public static function safeDeleted(Node $node) : void {
176
		self::safeExecute(fn() => self::deleted($node));
177
	}
178
179
	public static function safePostRenamed(Node $source, Node $target) : void {
180
		self::safeExecute(fn() => self::postRenamed($source, $target));
181
	}
182
183
	public function register() : void {
184
		$this->filesystemRoot->listen('\OC\Files', 'postWrite', [__CLASS__, 'safeUpdated']);
185
		$this->filesystemRoot->listen('\OC\Files', 'preDelete', [__CLASS__, 'safeDeleted']);
186
		$this->filesystemRoot->listen('\OC\Files', 'postRename', [__CLASS__, 'safePostRenamed']);
187
	}
188
}
189