requireUsableConfig(); $share = $this->connect($config); $full = $this->pathResolver->fullPath((string) $config->getBasePath(), $relativePath); try { $infos = $share->dir($full); } catch (Throwable $e) { throw new ShareConnectionException($e->getMessage(), 0, $e); } $entries = array_map(fn (IFileInfo $i): FileEntry => $this->toEntry($i, $relativePath), $infos); $this->sortEntries($entries); return $entries; } public function search(string $query, int $limit = 200): array { $needle = trim($query); if ('' === $needle) { return []; } $config = $this->requireUsableConfig(); $share = $this->connect($config); $base = (string) $config->getBasePath(); $results = []; $queue = ['']; // chemins relatifs des dossiers à explorer, racine en premier (parcours en largeur) $visitedDirs = 0; while ([] !== $queue && count($results) < $limit && $visitedDirs < self::SEARCH_MAX_DIRS) { $relative = array_shift($queue); $full = $this->pathResolver->fullPath($base, $relative); try { $infos = $share->dir($full); } catch (Throwable) { continue; // dossier illisible (droits, lien mort…) : on l'ignore et on poursuit } ++$visitedDirs; foreach ($infos as $info) { $entry = $this->toEntry($info, $relative); if ($entry->isDir) { $queue[] = $entry->path; } if (false !== mb_stripos($entry->name, $needle)) { $results[] = $entry; if (count($results) >= $limit) { break; } } } } $this->sortEntries($results); return $results; } public function read(string $relativePath) { $config = $this->requireUsableConfig(); $share = $this->connect($config); $full = $this->pathResolver->fullPath((string) $config->getBasePath(), $relativePath); try { return $share->read($full); } catch (Throwable $e) { throw new ShareConnectionException($e->getMessage(), 0, $e); } } public function test(): ShareTestResult { try { $config = $this->requireUsableConfig(); $share = $this->connect($config); $share->dir($this->pathResolver->fullPath((string) $config->getBasePath(), '')); return new ShareTestResult(true); } catch (ShareNotConfiguredException $e) { return new ShareTestResult(false, 'Configuration incomplète ou désactivée.'); } catch (Throwable $e) { return new ShareTestResult(false, $e->getMessage()); } } private function requireUsableConfig(): ShareConfiguration { $config = $this->configRepository->findSingleton(); if (null === $config || !$config->isUsable()) { throw new ShareNotConfiguredException('Share is not configured or disabled.'); } return $config; } private function connect(ShareConfiguration $config): IShare { $password = null !== $config->getEncryptedPassword() ? $this->tokenEncryptor->decrypt($config->getEncryptedPassword()) : ''; $auth = new BasicAuth( (string) $config->getUsername(), $config->getDomain() ?: 'WORKGROUP', $password, ); $server = new ServerFactory()->createServer((string) $config->getHost(), $auth); try { return $server->getShare((string) $config->getShareName()); } catch (Throwable $e) { throw new ShareConnectionException($e->getMessage(), 0, $e); } } /** * Trie en place : dossiers d'abord, puis tri alphabétique insensible à la casse. * * @param FileEntry[] $entries */ private function sortEntries(array &$entries): void { usort($entries, static function (FileEntry $a, FileEntry $b): int { if ($a->isDir !== $b->isDir) { return $a->isDir ? -1 : 1; } return strcasecmp($a->name, $b->name); }); } private function toEntry(IFileInfo $info, string $parentRelative): FileEntry { $parent = '' === $parentRelative ? '' : rtrim($parentRelative, '/').'/'; $path = $parent.$info->getName(); $isDir = $info->isDirectory(); $mime = 'application/octet-stream'; if (!$isDir) { $guessed = MimeTypes::getDefault()->getMimeTypes(pathinfo($info->getName(), PATHINFO_EXTENSION)); $mime = $guessed[0] ?? 'application/octet-stream'; } return new FileEntry( name: $info->getName(), path: $path, isDir: $isDir, size: $isDir ? 0 : $info->getSize(), modifiedAt: $info->getMTime(), mimeType: $mime, ); } }