From 52c216c52dceca147169ca32ae77cbb2cec46e51 Mon Sep 17 00:00:00 2001 From: nora-weisser Date: Wed, 8 Apr 2026 22:03:57 +0200 Subject: [PATCH] feat: re-wrote code related to Google Drive integration --- .gitignore | 1 + Dockerfile | 2 +- build.gradle.kts | 1 + .../configuration/GlobalExceptionHandler.java | 3 + .../GoogleDriveFileStorageRepository.java | 78 +++++-------------- 5 files changed, 27 insertions(+), 58 deletions(-) diff --git a/.gitignore b/.gitignore index 07baf3d7e..856aefe68 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ src/main/resources/application-local.yml /scripts/init-dev-env.sh .vercel /scripts/init-prod-env.sh +service-account.json # bruno api-flows/results.html diff --git a/Dockerfile b/Dockerfile index 67068ec85..3a56896f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build the application using Gradle and JDK 21 (Temurin) -FROM gradle:8.7-jdk21-alpine AS build +FROM gradle:8.7-jdk21 AS build WORKDIR /app # Copy configuration files to cache dependencies diff --git a/build.gradle.kts b/build.gradle.kts index d1722f7f9..afd1a975e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation("com.google.api-client:google-api-client:2.8.1") implementation("com.google.oauth-client:google-oauth-client-jetty:1.34.1") implementation("com.google.apis:google-api-services-drive:v3-rev20230822-2.0.0") + implementation("com.google.auth:google-auth-library-oauth2-http:1.23.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.skyscreamer:jsonassert:1.5.3") diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index c1bec9cde..3a116a44f 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -34,6 +34,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -41,6 +42,7 @@ /** Global controller to handle all exceptions for the API. */ @SuppressWarnings({"PMD.ExcessiveImports"}) +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -70,6 +72,7 @@ public ResponseEntity handleNotFoundException( @ResponseStatus(INTERNAL_SERVER_ERROR) public ResponseEntity handleInternalError( final RuntimeException ex, final WebRequest request) { + log.error("Internal error: {}", ex.getMessage(), ex); final var errorDetails = new ErrorDetails( INTERNAL_SERVER_ERROR.value(), ex.getMessage(), request.getDescription(false)); diff --git a/src/main/java/com/wcc/platform/repository/googledrive/GoogleDriveFileStorageRepository.java b/src/main/java/com/wcc/platform/repository/googledrive/GoogleDriveFileStorageRepository.java index ec63b6597..7f5d4579a 100644 --- a/src/main/java/com/wcc/platform/repository/googledrive/GoogleDriveFileStorageRepository.java +++ b/src/main/java/com/wcc/platform/repository/googledrive/GoogleDriveFileStorageRepository.java @@ -1,21 +1,17 @@ package com.wcc.platform.repository.googledrive; -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; -import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; -import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; -import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; import com.google.api.client.http.InputStreamContent; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; -import com.google.api.client.util.store.FileDataStoreFactory; import com.google.api.services.drive.Drive; import com.google.api.services.drive.DriveScopes; import com.google.api.services.drive.model.File; import com.google.api.services.drive.model.FileList; import com.google.api.services.drive.model.Permission; +import com.google.auth.http.HttpCredentialsAdapter; +import com.google.auth.oauth2.GoogleCredentials; import com.wcc.platform.domain.exceptions.PlatformInternalException; import com.wcc.platform.domain.platform.filestorage.FileStored; import com.wcc.platform.properties.FolderStorageProperties; @@ -24,7 +20,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.List; @@ -49,8 +44,7 @@ public class GoogleDriveFileStorageRepository implements FileStorageRepository { private static final String APPLICATION_NAME = "WCC Backend"; private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); private static final List SCOPES = Collections.singletonList(DriveScopes.DRIVE); - private static final String CREDS_FILE_PATH = "/credentials.json"; - private static final String TOKENS_DIR_PATH = "tokens"; + private static final String SERVICE_ACCOUNT_PATH = "/service-account.json"; private final Drive driveService; @@ -63,68 +57,34 @@ public GoogleDriveFileStorageRepository( this.folders = folders; } - /** Spring constructor: builds Drive client and reads folders from properties. */ + /** Spring constructor: builds Drive client using service account credentials. */ @Autowired public GoogleDriveFileStorageRepository(final FolderStorageProperties folders) throws GeneralSecurityException, IOException { final NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); this.driveService = - new Drive.Builder(httpTransport, JSON_FACTORY, getCredentials(httpTransport)) + new Drive.Builder(httpTransport, JSON_FACTORY, loadServiceAccountCredentials()) .setApplicationName(APPLICATION_NAME) .build(); this.folders = folders; } - /** Constructor that initializes the Google Drive service (no Spring). */ - public GoogleDriveFileStorageRepository() throws GeneralSecurityException, IOException { - final NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); - this.driveService = - new Drive.Builder(httpTransport, JSON_FACTORY, getCredentials(httpTransport)) - .setApplicationName(APPLICATION_NAME) - .build(); - this.folders = new FolderStorageProperties(); - } - /** - * Creates an authorized Credential object. + * Loads Google Drive credentials from a service account JSON file. * - * @param httpTransport The network HTTP Transport. - * @return An authorized Credential object. - * @throws IOException If the credentials.json file cannot be found. + * @return An {@link HttpCredentialsAdapter} wrapping the service account credentials. + * @throws IOException If the service account file cannot be found or read. */ - private static Credential getCredentials(final NetHttpTransport httpTransport) - throws IOException { + private static HttpCredentialsAdapter loadServiceAccountCredentials() throws IOException { try (InputStream in = - GoogleDriveFileStorageRepository.class.getResourceAsStream(CREDS_FILE_PATH)) { + GoogleDriveFileStorageRepository.class.getResourceAsStream(SERVICE_ACCOUNT_PATH)) { if (in == null) { - throw new FileNotFoundException("Resource not found: " + CREDS_FILE_PATH); + throw new FileNotFoundException("Resource not found: " + SERVICE_ACCOUNT_PATH); } - final var clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); - final String clientId = clientSecrets.getDetails().getClientId(); - final String userKey = "user-" + (clientId == null ? "unknown" : clientId); - - final var flow = - new GoogleAuthorizationCodeFlow.Builder( - httpTransport, JSON_FACTORY, clientSecrets, SCOPES) - .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIR_PATH))) - .setAccessType("offline") - .build(); - - final Credential credential = flow.loadCredential(userKey); - if (credential != null) { - log.info( - "Using existing Google Drive credentials from '{}' for clientId '{}'. " - + "No browser authorization needed.", - TOKENS_DIR_PATH, - clientId); - return credential; - } - - log.info( - "No existing credentials found for clientId '{}'. Opening browser for authorization...", - clientId); - final LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build(); - return new AuthorizationCodeInstalledApp(flow, receiver).authorize(userKey); + final GoogleCredentials credentials = + GoogleCredentials.fromStream(in).createScoped(SCOPES); + log.info("Loaded Google Drive service account credentials."); + return new HttpCredentialsAdapter(credentials); } } @@ -159,11 +119,15 @@ public FileStored uploadFile( new InputStreamContent(contentType, new ByteArrayInputStream(fileData)); final var file = - files().create(fileMetadata, mediaContent).setFields("id, name, webViewLink").execute(); + files() + .create(fileMetadata, mediaContent) + .setSupportsAllDrives(true) + .setFields("id, name, webViewLink") + .execute(); final var permission = new Permission().setType("anyone").setRole("reader"); - permissions().create(file.getId(), permission).execute(); + permissions().create(file.getId(), permission).setSupportsAllDrives(true).execute(); return new FileStored(file.getId(), file.getWebViewLink()); } catch (IOException e) {