Initial commit
This commit is contained in:
294
api/app/Services/GoogleDrive/GoogleDriveService.php
Normal file
294
api/app/Services/GoogleDrive/GoogleDriveService.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user