Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@

<properties>
<powsybl-ws-dependencies.version>2.23.0</powsybl-ws-dependencies.version>
<powsybl-ws-commons.version>1.29.0-SNAPSHOT</powsybl-ws-commons.version>

<gridsuite-filter.version>1.6.0</gridsuite-filter.version>

<org-apache-commons.version>4.4</org-apache-commons.version>
<org.hamcrest.version>2.2</org.hamcrest.version>

<spring-cloud-aws.version>3.2.0</spring-cloud-aws.version>

<sonar.organization>gridsuite</sonar.organization>
<sonar.projectKey>org.gridsuite:computation</sonar.projectKey>
</properties>
Expand Down Expand Up @@ -129,6 +133,11 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-starter-s3</artifactId>
<version>${spring-cloud-aws.version}</version>
</dependency>

<!-- AspectJ -->
<dependency>
Expand All @@ -145,6 +154,12 @@
</dependency>

<!-- Powsybl -->
<dependency>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-ws-commons</artifactId>
<version>${powsybl-ws-commons.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.powsybl</groupId>
<artifactId>powsybl-network-store-client</artifactId>
Expand Down Expand Up @@ -224,5 +239,12 @@
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>

<!-- In memory file system for read/write files testing -->
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.computation.s3;

import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.exception.SdkException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Map;

/**
* @author Thang PHAM <quyet-thang.pham at rte-france.com>
*/
public class ComputationS3Service {

public static final String S3_DELIMITER = "/";
public static final String S3_SERVICE_NOT_AVAILABLE_MESSAGE = "S3 service not available";

public static final String METADATA_FILE_NAME = "file-name";

private final S3Client s3Client;

private final String bucketName;

public ComputationS3Service(S3Client s3Client, String bucketName) {
this.s3Client = s3Client;
this.bucketName = bucketName;
}

public void uploadFile(Path filePath, String s3Key, String fileName, Integer expireAfterMinutes) throws IOException {
try {
PutObjectRequest putRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key(s3Key)
.metadata(Map.of(METADATA_FILE_NAME, fileName))
.tagging(expireAfterMinutes != null ? "expire-after-minutes=" + expireAfterMinutes : null)
.build();
s3Client.putObject(putRequest, RequestBody.fromFile(filePath));
} catch (SdkException e) {
throw new IOException("Error occurred while uploading file to S3: " + e.getMessage());
}
}

public S3InputStreamInfos downloadFile(String s3Key) throws IOException {
try {
GetObjectRequest getRequest = GetObjectRequest.builder()
.bucket(bucketName)
.key(s3Key)
.build();
ResponseInputStream<GetObjectResponse> inputStream = s3Client.getObject(getRequest);
return S3InputStreamInfos.builder()
.inputStream(inputStream)
.fileName(inputStream.response().metadata().get(METADATA_FILE_NAME))
.fileLength(inputStream.response().contentLength())
.build();
} catch (SdkException e) {
throw new IOException("Error occurred while downloading file from S3: " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.computation.s3;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import software.amazon.awssdk.services.s3.S3Client;

/**
* @author Thang PHAM <quyet-thang.pham at rte-france.com>
*/
@AutoConfiguration
@ConditionalOnProperty(name = "computation.s3.enabled", havingValue = "true")
public class S3AutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(S3AutoConfiguration.class);
@Value("${spring.cloud.aws.bucket:ws-bucket}")
private String bucketName;

@Bean
public ComputationS3Service s3Service(S3Client s3Client) {
LOGGER.info("Configuring ComputationS3Service with bucket: {}", bucketName);
return new ComputationS3Service(s3Client, bucketName);
}
}
24 changes: 24 additions & 0 deletions src/main/java/org/gridsuite/computation/s3/S3InputStreamInfos.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package org.gridsuite.computation.s3;

import lombok.Builder;
import lombok.Getter;

import java.io.InputStream;

/**
* @author Thang PHAM <quyet-thang.pham at rte-france.com>
*/
@Builder
@Getter
public class S3InputStreamInfos {
InputStream inputStream;
String fileName;
Long fileLength;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,14 @@ public abstract class AbstractComputationResultService<S> {
public abstract void deleteAll();

public abstract S findStatus(UUID resultUuid);

// --- Must implement these following methods if a computation server supports s3 storage --- //
public void saveDebugFileLocation(UUID resultUuid, String debugFilePath) {
// to override by subclasses
}

public String findDebugFileLocation(UUID resultUuid) {
// to override by subclasses
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import lombok.Setter;
import org.gridsuite.computation.dto.ReportInfos;

import java.nio.file.Path;
import java.util.UUID;

/**
Expand All @@ -30,9 +31,16 @@ public abstract class AbstractComputationRunContext<P> {
private P parameters;
private ReportNode reportNode;
private Network network;
private Boolean debug;
private Path debugDir;

protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
String userId, String provider, P parameters) {
this(networkUuid, variantId, receiver, reportInfos, userId, provider, parameters, null);
}

protected AbstractComputationRunContext(UUID networkUuid, String variantId, String receiver, ReportInfos reportInfos,
String userId, String provider, P parameters, Boolean debug) {
this.networkUuid = networkUuid;
this.variantId = variantId;
this.receiver = receiver;
Expand All @@ -42,5 +50,6 @@ protected AbstractComputationRunContext(UUID networkUuid, String variantId, Stri
this.parameters = parameters;
this.reportNode = ReportNode.NO_OP;
this.network = null;
this.debug = debug;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@
package org.gridsuite.computation.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powsybl.commons.PowsyblException;
import lombok.Getter;
import org.gridsuite.computation.s3.ComputationS3Service;
import org.gridsuite.computation.s3.S3InputStreamInfos;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

import static org.gridsuite.computation.s3.ComputationS3Service.S3_SERVICE_NOT_AVAILABLE_MESSAGE;

/**
* @author Mathieu Deharbe <mathieu.deharbe at rte-france.com>
* @param <C> run context specific to a computation, including parameters
Expand All @@ -26,6 +39,7 @@ public abstract class AbstractComputationService<C extends AbstractComputationRu
protected NotificationService notificationService;
protected UuidGeneratorService uuidGeneratorService;
protected T resultService;
protected ComputationS3Service computationS3Service;
@Getter
private final String defaultProvider;

Expand All @@ -34,11 +48,21 @@ protected AbstractComputationService(NotificationService notificationService,
ObjectMapper objectMapper,
UuidGeneratorService uuidGeneratorService,
String defaultProvider) {
this(notificationService, resultService, null, objectMapper, uuidGeneratorService, defaultProvider);
}

protected AbstractComputationService(NotificationService notificationService,
T resultService,
ComputationS3Service computationS3Service,
ObjectMapper objectMapper,
UuidGeneratorService uuidGeneratorService,
String defaultProvider) {
this.notificationService = Objects.requireNonNull(notificationService);
this.objectMapper = objectMapper;
this.uuidGeneratorService = Objects.requireNonNull(uuidGeneratorService);
this.defaultProvider = defaultProvider;
this.resultService = Objects.requireNonNull(resultService);
this.computationS3Service = computationS3Service;
}

public void stop(UUID resultUuid, String receiver) {
Expand Down Expand Up @@ -76,4 +100,37 @@ public void setStatus(List<UUID> resultUuids, S status) {
public S getStatus(UUID resultUuid) {
return resultService.findStatus(resultUuid);
}

public ResponseEntity<Resource> downloadDebugFile(UUID resultUuid) {
if (computationS3Service == null) {
throw new PowsyblException(S3_SERVICE_NOT_AVAILABLE_MESSAGE);
}

String s3Key = resultService.findDebugFileLocation(resultUuid);
if (s3Key == null) {
return ResponseEntity.notFound().build();
}

try {
S3InputStreamInfos s3InputStreamInfos = computationS3Service.downloadFile(s3Key);
InputStream inputStream = s3InputStreamInfos.getInputStream();
String fileName = s3InputStreamInfos.getFileName();
Long fileLength = s3InputStreamInfos.getFileLength();

// build header
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(fileName).build());
headers.setContentLength(fileLength);

// wrap s3 input stream
InputStreamResource resource = new InputStreamResource(inputStream);
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
} catch (IOException e) {
return ResponseEntity.notFound().build();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public Message<String> toMessage(ObjectMapper objectMapper) {
.setHeader(REPORT_UUID_HEADER, runContext.getReportInfos().reportUuid() != null ? runContext.getReportInfos().reportUuid().toString() : null)
.setHeader(REPORTER_ID_HEADER, runContext.getReportInfos().reporterId())
.setHeader(REPORT_TYPE_HEADER, runContext.getReportInfos().computationType())
.setHeader(HEADER_DEBUG, runContext.getDebug())
.copyHeaders(getSpecificMsgHeaders(objectMapper))
.build();
}
Expand Down
Loading
Loading