getDecryptedConfig(); // Log only the keys of the config (avoid sensitive values) try { Log::debug("StorageRepository: config keys for repo #{$repo->id}: " . json_encode(array_keys($config))); } catch (\Exception) { // ignore logging errors } return match ($repo->tipo) { 'webdav' => $this->buildWebDAV($config), 'google_drive' => $this->buildGoogleDrive($config), default => null, }; } catch (\Exception $e) { Log::error("StorageRepository: failed to build filesystem for repo #{$repo->id} ({$repo->nome}): " . $e->getMessage()); return null; } } public function testConnection(StorageRepository $repo): array { try { $config = $repo->getDecryptedConfig(); if ($repo->tipo === 'google_drive') { if (empty($config['client_id']) || empty($config['client_secret']) || empty($config['refresh_token'])) { return ['success' => false, 'message' => 'Configurazione incompleta: client_id, client_secret e refresh_token sono necessari per Google Drive.']; } $apiError = $this->checkGoogleDriveApiStatus($config); if ($apiError) { return ['success' => false, 'message' => $apiError]; } } $filesystem = $this->buildFilesystem($repo); if (!$filesystem) { return ['success' => false, 'message' => 'Impossibile costruire il filesystem per questo tipo di repository.']; } $rootPath = $this->normalizePath('/'); $contents = $filesystem->listContents($rootPath, false)->toArray(); return ['success' => true, 'message' => 'Connessione riuscita. ' . count($contents) . ' elementi trovati nella root.']; } catch (\Exception $e) { Log::error("StorageRepository: test connection failed for repo #{$repo->id} ({$repo->nome}): " . $e->getMessage()); return ['success' => false, 'message' => 'Errore di connessione: ' . $e->getMessage()]; } } private function checkGoogleDriveApiStatus(array $config): ?string { try { try { $oauth = app(GoogleOAuthService::class); $connection = $oauth->getConnection(GoogleService::Drive); if ($connection) { $client = $oauth->buildClient(GoogleService::Drive); $service = new \Google_Service_Drive($client); $rootId = $config['root_folder_id'] ?? 'root'; $service->files->get($rootId, ['fields' => 'id']); return null; } } catch (\Exception $e) { // fall through to config-based auth } $client = new \Google_Client(); $client->setClientId($config['client_id']); $client->setClientSecret($config['client_secret']); $client->addScope(\Google_Service_Drive::DRIVE); $token = $client->refreshToken($config['refresh_token']); $client->setAccessToken($token); $service = new \Google_Service_Drive($client); $rootId = $config['root_folder_id'] ?? 'root'; $service->files->get($rootId, ['fields' => 'id']); return null; } catch (\Google\Service\Exception $e) { $errors = $e->getErrors(); $reason = $errors[0]['reason'] ?? ''; $message = $errors[0]['message'] ?? $e->getMessage(); if ($reason === 'accessNotConfigured' || str_contains($message, 'Drive API has not been used')) { $projectNumber = explode('-', $config['client_id'] ?? '')[0] ?? ''; return 'Google Drive API non abilitata. Vai su https://console.developers.google.com/apis/api/drive.googleapis.com/overview?project=' . $projectNumber . ' e clicca "Enable".'; } if ($e->getCode() === 404) { return 'Cartella root non trovata (ID: ' . ($config['root_folder_id'] ?? 'root') . '). Verifica che il folder ID sia corretto.'; } return 'Errore API Google Drive: ' . $message; } catch (\Exception $e) { return 'Errore di connessione Google Drive: ' . $e->getMessage(); } } public function listContents(StorageRepository $repo, string $path = '/'): array { try { $config = $repo->getDecryptedConfig(); if ($repo->tipo === 'google_drive') { if (empty($config['client_id']) || empty($config['client_secret']) || empty($config['refresh_token'])) { Log::warning("StorageRepository: listContents failed for repo #{$repo->id} ({$repo->nome}): credenziali Google Drive mancanti"); return []; } } $filesystem = $this->buildFilesystem($repo); if (!$filesystem) { return []; } $normalizedPath = $this->normalizePath($path); $items = $filesystem->listContents($normalizedPath, false)->toArray(); $result = []; foreach ($items as $item) { $path = method_exists($item, 'path') ? $item->path() : (is_array($item) && isset($item['path']) ? $item['path'] : null); $type = method_exists($item, 'type') ? $item->type() : (is_array($item) && isset($item['type']) ? $item['type'] : null); $basename = $path ? basename($path) : ''; $lastModified = method_exists($item, 'lastModified') ? $item->lastModified() : (is_array($item) && isset($item['lastmodified']) ? $item['lastmodified'] : null); $fileSize = method_exists($item, 'fileSize') ? $item->fileSize() : (is_array($item) && isset($item['filesize']) ? $item['filesize'] : null); $mimeType = method_exists($item, 'mimeType') ? $item->mimeType() : (is_array($item) && isset($item['mimetype']) ? $item['mimetype'] : null); $result[] = [ 'path' => $path, 'type' => $type, 'basename' => $basename, 'lastModified' => $lastModified, 'fileSize' => $fileSize, 'mimeType' => $mimeType, ]; } usort($result, fn($a, $b) => ($a['type'] === 'dir' && $b['type'] !== 'dir') ? -1 : (($b['type'] === 'dir' && $a['type'] !== 'dir') ? 1 : strcasecmp($a['basename'], $b['basename']))); Log::debug("StorageRepository: listContents repo #{$repo->id} tipo={$repo->tipo} path='{$normalizedPath}' results=" . count($result)); return $result; } catch (\Exception $e) { Log::error("StorageRepository: listContents failed for repo #{$repo->id} path '{$path}': " . $e->getMessage()); return []; } } public function encryptSensitiveConfig(array $config, string $tipo, array $existingConfig = []): array { $sensitiveFields = match ($tipo) { 'webdav' => ['password'], 'google_drive' => ['client_secret', 'refresh_token'], default => [], }; foreach ($sensitiveFields as $field) { if (!empty($config[$field]) && !$this->isEncrypted($config[$field])) { $config[$field] = Crypt::encryptString($config[$field]); } elseif (empty($config[$field]) && !empty($existingConfig[$field])) { $config[$field] = $existingConfig[$field]; } } return $config; } public function normalizePath(string $path): string { $path = trim($path); if ($path === '' || $path === '\\') { return ''; } if ($path === '/') { return ''; } return ltrim($path, '/\\'); } private function isEncrypted(string $value): bool { return str_starts_with($value, 'ey') || str_contains($value, ':'); } private function buildWebDAV(array $config): ?Filesystem { $fullBaseUri = rtrim($config['base_uri'] ?? '', '/') . '/'; $parsedUrl = parse_url($fullBaseUri); $schemeHost = ($parsedUrl['scheme'] ?? 'https') . '://' . ($parsedUrl['host'] ?? 'localhost'); $basePath = rawurldecode($parsedUrl['path'] ?? '/'); $root = '/' . trim($config['root'] ?? '/', '/') . '/'; $prefix = rtrim($basePath, '/') . $root; $authType = match ($config['auth_type'] ?? 'basic') { 'basic' => WebDAVClient::AUTH_BASIC, 'digest' => WebDAVClient::AUTH_DIGEST, 'ntlm' => WebDAVClient::AUTH_NTLM, default => WebDAVClient::AUTH_BASIC, }; $client = new WebDAVClient([ 'baseUri' => $schemeHost . '/', 'userName' => $config['username'] ?? '', 'password' => $config['password'] ?? '', 'authType' => $authType, ]); $adapter = new WebDAVAdapter($client, $prefix); return new Filesystem($adapter); } private function buildGoogleDrive(array $config): ?Filesystem { if (empty($config['client_id']) || empty($config['client_secret']) || empty($config['refresh_token'])) { throw new \InvalidArgumentException('Google Drive richiede client_id, client_secret e refresh_token per l\'autenticazione.'); } $client = new \Google_Client(); $client->setClientId($config['client_id']); $client->setClientSecret($config['client_secret']); $client->addScope(\Google_Service_Drive::DRIVE); // Refresh token per ottenere un access token valido $token = $client->refreshToken($config['refresh_token']); $client->setAccessToken($token); $service = new \Google_Service_Drive($client); $rootFolderId = $config['root_folder_id'] ?? 'root'; $adapter = new GoogleDriveAdapter($service, $rootFolderId); return new Filesystem($adapter); } public function readGoogleDriveFile(StorageRepository $repo, string $normalizedPath, string $basename): array { $config = $repo->getDecryptedConfig(); try { $oauth = app(GoogleOAuthService::class); $connection = $oauth->getConnection(GoogleService::Drive); if ($connection) { $client = $oauth->buildClient(GoogleService::Drive); $service = new \Google_Service_Drive($client); return $this->readGoogleDriveFileWithService($service, $config, $normalizedPath, $basename); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::warning('Central Google Drive OAuth failed in readGoogleDriveFile, falling back: ' . $e->getMessage()); } $client = new \Google_Client(); $client->setClientId($config['client_id']); $client->setClientSecret($config['client_secret']); $client->addScope(\Google_Service_Drive::DRIVE); $token = $client->refreshToken($config['refresh_token']); $client->setAccessToken($token); $service = new \Google_Service_Drive($client); $rootFolderId = $config['root_folder_id'] ?? 'root'; $fileId = $this->findGoogleDriveFileId($service, $rootFolderId, $normalizedPath); $file = $service->files->get($fileId, ['fields' => 'id,name,mimeType,size']); $mimeType = $file->getMimeType(); if ($mimeType && str_starts_with($mimeType, 'application/vnd.google-apps.')) { return $this->exportGoogleDriveDoc($service, $fileId, $mimeType, $basename); } $response = $service->files->get($fileId, ['alt' => 'media']); $body = $response->getBody(); $stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null; if (!$stream) { $stream = fopen('php://temp', 'r+'); fwrite($stream, (string) $body); rewind($stream); } $fileSize = $file->getSize(); return [ 'stream' => $stream, 'mimeType' => $mimeType, 'basename' => $basename, 'fileSize' => $fileSize !== null ? (int) $fileSize : null, ]; } private function readGoogleDriveFileWithService(\Google_Service_Drive $service, array $config, string $normalizedPath, string $basename): array { $rootFolderId = $config['root_folder_id'] ?? 'root'; $fileId = $this->findGoogleDriveFileId($service, $rootFolderId, $normalizedPath); $file = $service->files->get($fileId, ['fields' => 'id,name,mimeType,size']); $mimeType = $file->getMimeType(); if ($mimeType && str_starts_with($mimeType, 'application/vnd.google-apps.')) { return $this->exportGoogleDriveDoc($service, $fileId, $mimeType, $basename); } $response = $service->files->get($fileId, ['alt' => 'media']); $body = $response->getBody(); $stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null; if (!$stream) { $stream = fopen('php://temp', 'r+'); fwrite($stream, (string) $body); rewind($stream); } $fileSize = $file->getSize(); return [ 'stream' => $stream, 'mimeType' => $mimeType, 'basename' => $basename, 'fileSize' => $fileSize !== null ? (int) $fileSize : null, ]; } private function exportGoogleDriveDoc(\Google_Service_Drive $service, string $fileId, string $googleMimeType, string $originalBasename): array { $exportMap = [ 'application/vnd.google-apps.document' => [ 'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ext' => 'docx', ], 'application/vnd.google-apps.spreadsheet' => [ 'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ext' => 'xlsx', ], 'application/vnd.google-apps.presentation' => [ 'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ext' => 'pptx', ], 'application/vnd.google-apps.drawing' => [ 'mime' => 'image/png', 'ext' => 'png', ], ]; $format = $exportMap[$googleMimeType] ?? ['mime' => 'application/pdf', 'ext' => 'pdf']; $response = $service->files->export($fileId, $format['mime'], ['alt' => 'media']); $body = $response->getBody(); $stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null; if (!$stream) { $stream = fopen('php://temp', 'r+'); fwrite($stream, (string) $body); rewind($stream); } $nameWithoutExt = pathinfo($originalBasename, PATHINFO_FILENAME); $newBasename = $nameWithoutExt . '.' . $format['ext']; Log::info("StorageRepository: exported Google Drive file {$fileId} from {$googleMimeType} to {$format['mime']} as {$newBasename}"); return [ 'stream' => $stream, 'mimeType' => $format['mime'], 'basename' => $newBasename, 'fileSize' => null, ]; } private function findGoogleDriveFileId(\Google_Service_Drive $service, string $rootFolderId, string $path): string { $path = trim($path, '/'); if ($path === '') { return $rootFolderId; } $parts = explode('/', $path); $currentParentId = $rootFolderId; foreach ($parts as $name) { $optParams = [ 'q' => sprintf("trashed = false and '%s' in parents and name = '%s'", str_replace("'", "\\'", $currentParentId), str_replace("'", "\\'", $name) ), 'fields' => 'files(id, name, mimeType)', 'pageSize' => 1, ]; $results = $service->files->listFiles($optParams); $files = $results->getFiles(); if (empty($files)) { throw new \RuntimeException("Elemento non trovato su Google Drive: {$name} (path: {$path})"); } $currentParentId = $files[0]->getId(); } return $currentParentId; } }