owncloud /
music
| 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
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 |