setAllowParallelRuns(false); } /** * Makes the background job do its work * * @param array $argument unused argument * @throws \Exception */ public function run($argument) { $this->charReplacement = strval($argument['charReplacement']) ?: null; if (isset($argument['errorsOnly'])) { $this->retryFailedNodes(); return; } $this->offset = intval($argument['offset']); $this->limit = intval($argument['limit']); if ($this->offset === 0) { $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_RUNNING); } $this->currentIndex = 0; foreach ($this->manager->getSeenUsers($this->offset) as $user) { $this->sanitizeUserFiles($user); $this->currentIndex++; $this->appConfig->setAppValueInt('sanitize_filenames_index', $this->currentIndex); if ($this->currentIndex === $this->limit) { break; } } if ($this->currentIndex === $this->limit) { $this->offset += $this->limit; $this->jobList->add(self::class, ['limit' => $this->limit, 'offset' => $this->offset, 'charReplacement' => $this->charReplacement]); return; } // No index to process anymore, we are done $this->appConfig->deleteAppValue('sanitize_filenames_index'); $hasErrors = !empty($this->userConfig->getValuesByUsers(Application::APP_ID, 'sanitize_filenames_errors')); if ($hasErrors) { $this->logger->info('Filename sanitization finished with errors. Retrying failed files in next background job run.'); $this->jobList->add(self::class, ['errorsOnly' => true, 'charReplacement' => $this->charReplacement]); return; } // we are really done! $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); } /** * Retry to sanitize files that failed in the first run */ private function retryFailedNodes(): void { $this->logger->debug('Retry sanitizing failed filename sanitization.'); $results = $this->userConfig->getValuesByUsers(Application::APP_ID, 'sanitize_filenames_errors'); $hasErrors = false; foreach ($results as $userId => $errors) { $user = $this->manager->get($userId); if ($user === null) { // user got deleted meanwhile, ignore continue; } $hasErrors = $hasErrors || $this->retryFailedUserNodes($user, $errors); $this->userConfig->deleteUserConfig($userId, Application::APP_ID, 'sanitize_filenames_errors'); } if ($hasErrors) { $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_ERROR); $this->logger->error('Retrying filename sanitization failed permanently.'); } else { $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); $this->logger->info('Retrying filename sanitization succeeded.'); } } private function retryFailedUserNodes(IUser $user, array $errors): bool { $this->session->setVolatileActiveUser($user); $folder = $this->rootFolder->getUserFolder($user->getUID()); $this->logger->debug("filename sanitization retry: started for user '{$user->getUID()}'"); $hasErrors = false; foreach ($errors as $path) { try { $node = $folder->get($path); $this->sanitizeNode($node); } catch (NotFoundException) { // file got deleted meanwhile, ignore } catch (\Exception $error) { $this->logger->error('filename sanitization failed when retried: ' . $path, ['exception' => $error]); $hasErrors = true; } } // tear down FS for user to make sure we do not run out of memory due to cached user FS $this->setupManager->tearDown(); return $hasErrors; } private function sanitizeUserFiles(IUser $user): void { // Set an active user so that event listeners can correctly work (e.g. files versions) $this->session->setVolatileActiveUser($user); $folder = $this->rootFolder->getUserFolder($user->getUID()); $this->logger->debug("filename sanitization: started for user '{$user->getUID()}'"); $errors = $this->sanitizeFolder($folder); // tear down FS for user to make sure we do not run out of memory due to cached user FS $this->setupManager->tearDown(); if (!empty($errors)) { $this->userConfig->setValueArray($user->getUID(), 'files', 'sanitize_filenames_errors', $errors, true); } } /** * Sanitizes the filenames of all nodes in a folder * * @return list list of nodes that could not be sanitized */ private function sanitizeFolder(Folder $folder): array { $errors = []; foreach ($folder->getDirectoryListing() as $node) { try { $this->sanitizeNode($node); } catch (LockedException) { $this->logger->debug('filename sanitization skipped: ' . $node->getPath() . ' (file is locked)'); $errors[] = $node->getPath(); } catch (\Exception $error) { $this->logger->warning('filename sanitization failed: ' . $node->getPath(), ['exception' => $error]); $errors[] = $node->getPath(); } if ($node instanceof Folder) { $errors = array_merge($errors, $this->sanitizeFolder($node)); } } return $errors; } /** * Sanitizes the filename of a single node * * @throws LockedException If the file is locked * @throws \Exception Unknown error */ private function sanitizeNode(Node $node): void { if ($node->isShared() && !$node->isUpdateable()) { // we cannot rename files in shares where we do not have permissions - we do it when sanitizing the owner's files return; } try { $oldName = $node->getName(); $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement); if ($oldName !== $newName) { $newName = $node->getParent()->getNonExistingName($newName); $path = rtrim(dirname($node->getPath()), '/'); $node->move("$path/$newName"); } } catch (NotFoundException) { // file got deleted meanwhile, ignore // or this is shared without permissions to rename it, ignore (owner will rename it) } } }