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 OCP\AppFramework\IAppContainer; |
18
|
|
|
use OCP\Files\IRootFolder; |
19
|
|
|
use OCP\Files\FileInfo; |
20
|
|
|
use OCP\Files\Node; |
21
|
|
|
|
22
|
|
|
use OCA\Music\AppInfo\Application; |
23
|
|
|
|
24
|
|
|
class FileHooks { |
25
|
|
|
private IRootFolder $filesystemRoot; |
26
|
|
|
|
27
|
|
|
public function __construct(IRootFolder $filesystemRoot) { |
28
|
|
|
$this->filesystemRoot = $filesystemRoot; |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Invoke auto update of music database after file or folder deletion |
33
|
|
|
* @param Node $node pointing to the file or folder |
34
|
|
|
*/ |
35
|
|
|
private static function deleted(Node $node) : void { |
36
|
|
|
$container = self::getContainer(); |
37
|
|
|
$scanner = $container->query('Scanner'); |
|
|
|
|
38
|
|
|
|
39
|
|
|
if ($node->getType() == FileInfo::TYPE_FILE) { |
40
|
|
|
$scanner->delete($node->getId()); |
41
|
|
|
} else { |
42
|
|
|
$scanner->deleteFolder($node); |
43
|
|
|
} |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Invoke auto update of music database after file update or file creation |
48
|
|
|
* @param Node $node pointing to the file |
49
|
|
|
*/ |
50
|
|
|
private static function updated(Node $node) : void { |
51
|
|
|
// At least on Nextcloud 13, it sometimes happens that this hook is triggered |
52
|
|
|
// when the core creates some temporary file and trying to access the provided |
53
|
|
|
// node throws an exception, probably because the temp file is already removed |
54
|
|
|
// by the time the execution gets here. See #636. |
55
|
|
|
// Furthermore, when the core opens a file in stream mode for writing using |
56
|
|
|
// File::fopen, this hook gets triggered immediately after the opening succeeds, |
57
|
|
|
// before anything is actually written and while the file is *exclusively locked |
58
|
|
|
// because of the write mode*. See #638. |
59
|
|
|
$container = self::getContainer(); |
60
|
|
|
try { |
61
|
|
|
self::handleUpdated($node, $container); |
62
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
63
|
|
|
$logger = $container->query('Logger'); |
|
|
|
|
64
|
|
|
$logger->log('FileHooks::updated triggered for a non-existing file', 'warn'); |
65
|
|
|
} catch (\OCP\Lock\LockedException $e) { |
66
|
|
|
$logger = $container->query('Logger'); |
|
|
|
|
67
|
|
|
$logger->log('FileHooks::updated triggered for a locked file ' . $node->getName(), 'warn'); |
68
|
|
|
} |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
private static function handleUpdated(Node $node, IAppContainer $container) : void { |
72
|
|
|
// we are interested only about updates on files, not on folders |
73
|
|
|
if ($node->getType() == FileInfo::TYPE_FILE) { |
74
|
|
|
$scanner = $container->query('Scanner'); |
|
|
|
|
75
|
|
|
$userId = self::getUser($node, $container); |
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, $container)) { |
81
|
|
|
$scanner->update($node, $userId, $node->getPath()); |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
private static function moved(Node $node) : void { |
87
|
|
|
$container = self::getContainer(); |
88
|
|
|
try { |
89
|
|
|
self::handleMoved($node, $container); |
90
|
|
|
} catch (\OCP\Files\NotFoundException $e) { |
91
|
|
|
$logger = $container->query('Logger'); |
|
|
|
|
92
|
|
|
$logger->log('FileHooks::moved triggered for a non-existing file', 'warn'); |
93
|
|
|
} catch (\OCP\Lock\LockedException $e) { |
94
|
|
|
$logger = $container->query('Logger'); |
|
|
|
|
95
|
|
|
$logger->log('FileHooks::moved triggered for a locked file ' . $node->getName(), 'warn'); |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
private static function handleMoved(Node $node, IAppContainer $container) : void { |
100
|
|
|
$scanner = $container->query('Scanner'); |
|
|
|
|
101
|
|
|
$userId = self::getUser($node, $container); |
102
|
|
|
|
103
|
|
|
if (!empty($userId) && self::userHasMusicLib($userId, $container)) { |
104
|
|
|
if ($node->getType() == FileInfo::TYPE_FILE) { |
105
|
|
|
$scanner->fileMoved($node, $userId); |
106
|
|
|
} else { |
107
|
|
|
$scanner->folderMoved($node, $userId); |
108
|
|
|
} |
109
|
|
|
} |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
private static function getUser(Node $node, IAppContainer $container) : ?string { |
113
|
|
|
$userId = $container->query('UserId'); |
|
|
|
|
114
|
|
|
|
115
|
|
|
// When a file is uploaded to a folder shared by link, we end up here without current user. |
116
|
|
|
// In that case, fall back to using file owner |
117
|
|
|
if (empty($userId)) { |
118
|
|
|
// 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. |
119
|
|
|
/** @var \OCP\IUser|null $owner */ |
120
|
|
|
$owner = $node->getOwner(); |
121
|
|
|
$userId = $owner ? $owner->getUID() : null; |
|
|
|
|
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
return $userId; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
private static function getContainer() : IAppContainer { |
128
|
|
|
$app = \OC::$server->query(Application::class); |
129
|
|
|
return $app->getContainer(); |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Check if user has any scanned tracks in his/her music library |
134
|
|
|
* @param string $userId |
135
|
|
|
* @param IAppContainer $container |
136
|
|
|
*/ |
137
|
|
|
private static function userHasMusicLib(string $userId, IAppContainer $container) : bool { |
138
|
|
|
$trackBusinessLayer = $container->query('TrackBusinessLayer'); |
|
|
|
|
139
|
|
|
return 0 < $trackBusinessLayer->count($userId); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
private static function postRenamed(Node $source, Node $target) : void { |
143
|
|
|
// Beware: the $source describes the past state of the file and some of its functions will throw upon calling |
144
|
|
|
|
145
|
|
|
if ($source->getParent()->getId() != $target->getParent()->getId()) { |
146
|
|
|
self::moved($target); |
147
|
|
|
} else { |
148
|
|
|
self::updated($target); |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
private static function safeExecute(callable $func) : void { |
153
|
|
|
// Don't let any exceptions or errors leak out of this method, no matter what unforeseen oddities happen. |
154
|
|
|
// We never want to prevent the actual file operation since our reactions to them are anyway non-crucial. |
155
|
|
|
// Especially during a server version update involving also Music app version update, the system may be |
156
|
|
|
// running a partially updated application version and that may lead to unexpected fatal errors, see |
157
|
|
|
// https://github.com/owncloud/music/issues/1231. |
158
|
|
|
try { |
159
|
|
|
try { |
160
|
|
|
$func(); |
161
|
|
|
} catch (\Throwable $error) { |
162
|
|
|
$container = self::getContainer(); |
163
|
|
|
$logger = $container->query('Logger'); |
|
|
|
|
164
|
|
|
$logger->log("Error occurred while executing Music app file hook: {$error->getMessage()}. Stack trace: {$error->getTraceAsString()}", 'error'); |
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
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.