Initial commit

This commit is contained in:
2026-02-03 10:38:46 +01:00
commit eb304f4b14
144 changed files with 22605 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
<?php
namespace App\Services\GoogleDrive;
use App\Models\GoogleDriveConnection;
use App\Models\User;
use Google\Client as GoogleClient;
use Google\Http\MediaFileUpload;
use Google\Service\Drive;
use Google\Service\Drive\DriveFile;
class GoogleDriveService
{
protected const DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive';
protected const DRIVE_METADATA_SCOPE = 'https://www.googleapis.com/auth/drive.metadata.readonly';
public function getAuthUrl(): string
{
$client = $this->createOAuthClient();
$client->setState(optional(request()->user())->id);
return $client->createAuthUrl();
}
public function handleCallback(string $code, User $user): GoogleDriveConnection
{
$client = $this->createOAuthClient();
$token = $client->fetchAccessTokenWithAuthCode($code);
if (isset($token['error'])) {
throw new \RuntimeException('Error fetching access token: '.($token['error_description'] ?? $token['error']));
}
$expiresAt = now();
if (isset($token['expires_in'])) {
$expiresAt = now()->addSeconds($token['expires_in']);
}
$connection = $user->googleDriveConnections()->first();
$email = $this->getTokenEmail($token, $client);
if ($connection) {
$connection->update([
'access_token' => $token['access_token'] ?? $connection->access_token,
'refresh_token' => $token['refresh_token'] ?? $connection->refresh_token,
'token_expires_at' => $expiresAt,
'account_email' => $email ?? $connection->account_email,
]);
} else {
$connection = $user->googleDriveConnections()->create([
'access_token' => $token['access_token'],
'refresh_token' => $token['refresh_token'] ?? '',
'token_expires_at' => $expiresAt,
'account_email' => $email ?? 'unknown',
]);
}
return $connection;
}
public function getClient(User $user): \Google\Client
{
$connection = $user->googleDriveConnections()->first();
if (! $connection) {
throw new \RuntimeException('No Google Drive connection found for user.');
}
$client = $this->createOAuthClient();
$token = [
'access_token' => $connection->access_token,
'refresh_token' => $connection->refresh_token,
'created' => $connection->token_expires_at->subSeconds(3600)->timestamp,
'expires_in' => 3600,
];
$client->setAccessToken($token);
if ($connection->token_expires_at->isPast() || $client->isAccessTokenExpired()) {
$newToken = $client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
if (isset($newToken['error'])) {
throw new \RuntimeException('Error refreshing token: '.($newToken['error_description'] ?? $newToken['error']));
}
$expiresAt = isset($newToken['expires_in'])
? now()->addSeconds($newToken['expires_in'])
: now()->addHour();
$connection->update([
'access_token' => $newToken['access_token'],
'token_expires_at' => $expiresAt,
]);
$client->setAccessToken($newToken);
}
return $client;
}
public function getDriveService(User $user): Drive
{
$client = $this->getClient($user);
return new Drive($client);
}
/**
* List all Shared Drives (Team Drives) the user has access to.
*
* @return \Illuminate\Support\Collection<int, array{id: string, name: string, type: string}>
*/
public function listSharedDrives(User $user): \Illuminate\Support\Collection
{
$service = $this->getDriveService($user);
$optParams = [
'pageSize' => 100,
'fields' => 'drives(id, name)',
];
$results = $service->drives->listDrives($optParams);
return collect($results->getDrives())->map(fn ($d) => [
'id' => $d->getId(),
'name' => $d->getName(),
'type' => 'shared_drive',
]);
}
/**
* @return \Illuminate\Support\Collection<int, array{id: string, name: string}>
*/
public function listFolders(User $user, ?string $parentId = null, ?string $driveId = null): \Illuminate\Support\Collection
{
$service = $this->getDriveService($user);
$query = "mimeType = 'application/vnd.google-apps.folder' and trashed = false";
if ($parentId) {
// List folders inside a specific parent folder
$query .= " and '{$parentId}' in parents";
} elseif ($driveId) {
// List root folders of the Shared Drive
// For Shared Drives, we need to get folders where the drive itself is the parent
// This is done by querying within the specific drive without a parent filter
// and filtering to only top-level items (those with the drive as direct parent)
// However, the API doesn't have a simple "root" concept for Shared Drives
// So we'll fetch all and filter, or use a different approach
// Better approach: Get the drive root and list its children
// We'll query for folders where parents contains the driveId
$query .= " and '{$driveId}' in parents";
} else {
// List root folders of My Drive
$query .= " and 'root' in parents";
}
$optParams = [
'q' => $query,
'fields' => 'files(id, name)',
'orderBy' => 'name',
'supportsAllDrives' => true,
'includeItemsFromAllDrives' => true,
];
if ($driveId) {
$optParams['driveId'] = $driveId;
$optParams['corpora'] = 'drive';
}
$results = $service->files->listFiles($optParams);
return collect($results->getFiles())->map(fn ($f) => ['id' => $f->getId(), 'name' => $f->getName()]);
}
public function createFolder(User $user, string $name, ?string $parentId = null, ?string $driveId = null): array
{
$service = $this->getDriveService($user);
$file = new DriveFile;
$file->setName($name);
$file->setMimeType('application/vnd.google-apps.folder');
if ($parentId) {
$file->setParents([$parentId]);
} elseif ($driveId) {
// Creating in root of Shared Drive - no parent needed, just driveId
$file->setDriveId($driveId);
}
$optParams = [
'supportsAllDrives' => true,
];
$created = $service->files->create($file, $optParams);
return ['id' => $created->getId(), 'name' => $created->getName()];
}
/**
* Upload a file to Google Drive. Returns array with 'id' and 'webViewLink'.
*/
public function uploadFile(User $user, string $filePath, string $folderId, string $filename, string $mimeType): array
{
$client = $this->getClient($user);
$service = new Drive($client);
$file = new DriveFile;
$file->setName($filename);
$file->setParents([$folderId]);
$client->setDefer(true);
$request = $service->files->create($file, [
'mimeType' => $mimeType,
'uploadType' => 'resumable',
'fields' => 'id, webViewLink',
'supportsAllDrives' => true,
]);
$chunkSize = 5 * 1024 * 1024; // 5MB
$media = new MediaFileUpload(
$client,
$request,
$mimeType,
'',
true,
$chunkSize
);
$media->setFileSize(filesize($filePath));
$handle = fopen($filePath, 'rb');
$status = false;
while (! $status) {
$chunk = fread($handle, $chunkSize);
$status = $media->nextChunk($chunk);
}
fclose($handle);
$client->setDefer(false);
$file = $status;
if (! $file instanceof DriveFile) {
throw new \RuntimeException('Upload did not return file metadata.');
}
return [
'id' => $file->getId(),
'webViewLink' => $file->getWebViewLink() ?? $file->getWebContentLink() ?? '',
];
}
public function deleteFile(User $user, string $fileId): void
{
$service = $this->getDriveService($user);
$service->files->delete($fileId, ['supportsAllDrives' => true]);
}
/**
* Get a temporary download URL or web link for a file.
*/
public function getFileLink(User $user, string $fileId): ?string
{
$service = $this->getDriveService($user);
$file = $service->files->get($fileId, [
'fields' => 'webViewLink, webContentLink',
'supportsAllDrives' => true,
]);
return $file->getWebViewLink() ?? $file->getWebContentLink();
}
protected function createOAuthClient(): GoogleClient
{
$client = new GoogleClient;
$client->setClientId(config('services.google.client_id'));
$client->setClientSecret(config('services.google.client_secret'));
$client->setRedirectUri(config('services.google.redirect_uri'));
$client->setScopes([self::DRIVE_SCOPE, self::DRIVE_METADATA_SCOPE]);
$client->setAccessType('offline');
$client->setPrompt('consent');
return $client;
}
protected function getTokenEmail(array $token, GoogleClient $client): ?string
{
if (isset($token['id_token'])) {
$payload = $client->verifyIdToken($token['id_token']);
if ($payload && isset($payload['email'])) {
return $payload['email'];
}
}
try {
$client->setAccessToken($token);
$oauth2 = new \Google\Service\Oauth2($client);
$info = $oauth2->userinfo->get();
return $info->getEmail();
} catch (\Throwable) {
return null;
}
}
}