2026-05-28 09:34:28 +02:00
< ? php
declare ( strict_types = 1 );
namespace App\Services ;
2026-06-15 11:14:35 +02:00
use App\Enums\GoogleService ;
2026-05-28 09:34:28 +02:00
use App\Models\StorageRepository ;
2026-06-15 11:14:35 +02:00
use App\Services\GoogleOAuthService ;
2026-05-28 09:34:28 +02:00
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 {
2026-05-28 13:25:47 +02:00
$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 13:25:47 +02:00
}
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 {
2026-06-15 11:14:35 +02:00
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
}
2026-06-01 16:11:29 +02:00
$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 {
2026-05-28 13:25:47 +02:00
$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 [];
}
}
2026-05-28 13:25:47 +02:00
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 ]);
2026-05-28 13:25:47 +02:00
} 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 === '/' ) {
2026-05-28 13:25:47 +02:00
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 ;
2026-05-28 13:25:47 +02:00
$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' ] ? ? '' ,
2026-05-28 13:25:47 +02:00
'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
{
2026-05-28 13:25:47 +02:00
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 ();
2026-05-28 13:25:47 +02:00
$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 ();
2026-06-15 11:14:35 +02:00
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 ());
}
2026-06-01 16:11:29 +02:00
$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 ,
2026-06-15 11:14:35 +02:00
'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 ,
2026-06-01 16:11:29 +02:00
'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
}