365 lines
14 KiB
PHP
365 lines
14 KiB
PHP
<?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();
|
|
// 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 {
|
|
$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();
|
|
|
|
$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;
|
|
}
|
|
}
|