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 */ 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 */ 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); /** @var \Psr\Http\Message\RequestInterface $request When defer is true, create() returns the request instead of executing it */ $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; } } }