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
introduced
by
![]() |
|||
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 |