Files
glastree/app/Services/StorageRepositoryService.php
T

365 lines
14 KiB
PHP
Raw Normal View History

2026-05-28 09:34:28 +02:00
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\StorageRepository;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use League\Flysystem\Filesystem;
use League\Flysystem\WebDAV\WebDAVAdapter;
use As247\Flysystem\GoogleDrive\GoogleDriveAdapter;
use Sabre\DAV\Client as WebDAVClient;
class StorageRepositoryService
{
public function buildFilesystem(StorageRepository $repo): ?Filesystem
{
try {
$config = $repo->getDecryptedConfig();
2026-05-29 06:23:57 +02:00
// 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
}
2026-05-28 09:34:28 +02:00
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.'];
}
2026-06-01 16:11:29 +02:00
$apiError = $this->checkGoogleDriveApiStatus($config);
if ($apiError) {
return ['success' => false, 'message' => $apiError];
}
}
2026-05-28 09:34:28 +02:00
$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()];
}
}
2026-06-01 16:11:29 +02:00
private function checkGoogleDriveApiStatus(array $config): ?string
{
try {
$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();
}
}
2026-05-28 09:34:28 +02:00
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 [];
}
2026-05-28 09:34:28 +02:00
$normalizedPath = $this->normalizePath($path);
$items = $filesystem->listContents($normalizedPath, false)->toArray();
$result = [];
foreach ($items as $item) {
2026-05-29 06:23:57 +02:00
$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);
2026-05-28 09:34:28 +02:00
$result[] = [
2026-05-29 06:23:57 +02:00
'path' => $path,
'type' => $type,
'basename' => $basename,
'lastModified' => $lastModified,
'fileSize' => $fileSize,
'mimeType' => $mimeType,
2026-05-28 09:34:28 +02:00
];
}
2026-05-29 06:23:57 +02:00
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));
2026-05-28 09:34:28 +02:00
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
2026-05-28 09:34:28 +02:00
{
$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];
2026-05-28 09:34:28 +02:00
}
}
return $config;
}
2026-05-29 06:54:07 +02:00
public function normalizePath(string $path): string
2026-05-28 09:34:28 +02:00
{
$path = trim($path);
if ($path === '' || $path === '\\') {
return '';
}
if ($path === '/') {
return '';
2026-05-28 09:34:28 +02:00
}
return ltrim($path, '/\\');
}
private function isEncrypted(string $value): bool
{
return str_starts_with($value, 'ey') || str_contains($value, ':');
}
private function buildWebDAV(array $config): ?Filesystem
{
2026-05-29 06:54:07 +02:00
$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,
};
2026-05-28 09:34:28 +02:00
$client = new WebDAVClient([
2026-05-29 06:54:07 +02:00
'baseUri' => $schemeHost . '/',
2026-05-28 09:34:28 +02:00
'userName' => $config['username'] ?? '',
'password' => $config['password'] ?? '',
'authType' => $authType,
2026-05-28 09:34:28 +02:00
]);
2026-05-29 06:54:07 +02:00
$adapter = new WebDAVAdapter($client, $prefix);
2026-05-28 09:34:28 +02:00
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.');
}
2026-05-28 09:34:28 +02:00
$client = new \Google_Client();
$client->setClientId($config['client_id']);
$client->setClientSecret($config['client_secret']);
2026-05-28 09:34:28 +02:00
$client->addScope(\Google_Service_Drive::DRIVE);
2026-05-29 06:23:57 +02:00
// Refresh token per ottenere un access token valido
$token = $client->refreshToken($config['refresh_token']);
$client->setAccessToken($token);
2026-05-28 09:34:28 +02:00
$service = new \Google_Service_Drive($client);
2026-05-29 06:23:57 +02:00
$rootFolderId = $config['root_folder_id'] ?? 'root';
$adapter = new GoogleDriveAdapter($service, $rootFolderId);
2026-05-28 09:34:28 +02:00
return new Filesystem($adapter);
}
2026-06-01 16:11:29 +02:00
public function readGoogleDriveFile(StorageRepository $repo, string $normalizedPath, string $basename): array
{
$config = $repo->getDecryptedConfig();
$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 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;
}
2026-05-28 09:34:28 +02:00
}