295 lines
10 KiB
PHP
295 lines
10 KiB
PHP
<?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;
|
|
}
|
|
}
|
|
}
|