From 19f5ed790176a195d3c77b4e4beeef591dbbed6e Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Thu, 23 Nov 2023 11:24:32 -0300
Subject: [PATCH 01/23] Bump AWS S3 SDK to V2 Issue: 105593

---
 gxcloudstorage-awss3/pom.xml                  |   4 +-
 .../genexus/db/driver/ExternalProviderS3.java | 436 ++++++++++++------
 pom.xml                                       |   2 +-
 3 files changed, 297 insertions(+), 145 deletions(-)

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3/pom.xml
index 33220655c..0ab39297e 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3/pom.xml
@@ -25,8 +25,8 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
-            <groupId>com.amazonaws</groupId>
-            <artifactId>aws-java-sdk-s3</artifactId>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
             <version>${com.amazonaws.version}</version>
         </dependency>
     </dependencies>
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index cd186fef5..ba741d8dd 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -1,33 +1,39 @@
 package com.genexus.db.driver;
 
-import com.amazonaws.auth.*;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.model.*;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.S3ClientOptions;
+import software.amazon.awssdk.auth.credentials.*;
+import software.amazon.awssdk.core.ResponseBytes;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.core.sync.ResponseTransformer;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.S3ClientBuilder;
+import software.amazon.awssdk.services.s3.S3Configuration;
+import software.amazon.awssdk.services.s3.model.*;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
+import software.amazon.awssdk.utils.IoUtils;
+
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LogManager;
-import com.amazonaws.HttpMethod;
+
 import com.genexus.Application;
 import com.genexus.util.GXService;
 import com.genexus.util.StorageUtils;
 import com.genexus.StructSdtMessages_Message;
-import java.io.File;
+
 import java.io.InputStream;
 import java.io.ByteArrayInputStream;
-
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.util.IOUtils;
-
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.*;
 
 
 public class ExternalProviderS3 extends ExternalProviderBase implements ExternalProvider  {
@@ -58,17 +64,15 @@ public class ExternalProviderS3 extends ExternalProviderBase implements External
 	@Deprecated
 	static final String REGION_DEPRECATED = "STORAGE_PROVIDER_REGION";
 
-
     static final String ACCELERATED = "s3-accelerate.amazonaws.com";
     static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
 	static final String DEFAULT_REGION = "us-east-1";
-
-    private AmazonS3 client;
+    private S3Client client;
+	private String clientRegion = "";
     private String bucket;
     private String folder;
     private String endpointUrl = ".s3.amazonaws.com/";
 	private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES;
-
 	private Boolean pathStyleUrls = false;
 
 	public String getName(){
@@ -94,7 +98,7 @@ private void initialize() throws Exception{
 		String secretKey = getEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED, "");
 		String bucket = getEncryptedPropertyValue(BUCKET, BUCKET_DEPRECATED);
 		String folder = getPropertyValue(FOLDER, FOLDER_DEPRECATED, "");
-		String region = getPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION);
+		clientRegion = getPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION);
 		String endpointValue = getPropertyValue(STORAGE_ENDPOINT, STORAGE_ENDPOINT_DEPRECATED, "");
 		if (endpointValue.equals("custom")) {
 			endpointValue = getPropertyValue(STORAGE_CUSTOM_ENDPOINT, STORAGE_CUSTOM_ENDPOINT_DEPRECATED);
@@ -103,29 +107,34 @@ private void initialize() throws Exception{
 		try {
 			defaultExpirationMinutes = Integer.parseInt(getPropertyValue(DEFAULT_EXPIRATION, DEFAULT_EXPIRATION_DEPRECATED, Integer.toString(defaultExpirationMinutes)));
 		} catch (Exception e) {
+			logger.error("Failed to parse default expiration time", e);
 		}
 
 		if (this.client == null) {
-			if (region.length() == 0) {
-				region = DEFAULT_REGION;
+			if (clientRegion.length() == 0) {
+				clientRegion = DEFAULT_REGION;
 			}
 
 			this.bucket = bucket;
 			this.folder = folder;
 
-			this.client = buildS3Client(accessKey, secretKey, endpointValue, region);
+			this.client = buildS3Client(accessKey, secretKey, endpointValue, clientRegion);
 			bucketExists();
 		}
 	}
 
-	private AmazonS3 buildS3Client(String accessKey, String secretKey, String endpoint, String region) {
-		AmazonS3 s3Client;
+	private S3Client buildS3Client(String accessKey, String secretKey, String endpoint, String region) {
+		S3Client s3Client;
 
 		boolean bUseIAM = !getPropertyValue(USE_IAM, "", "").isEmpty() || (accessKey.equals("") && secretKey.equals(""));
 
-		AmazonS3ClientBuilder builder = bUseIAM ?
-							AmazonS3ClientBuilder.standard():
-							AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
+		S3ClientBuilder builder = bUseIAM ?
+			S3Client.builder() :
+			S3Client.builder().credentialsProvider(
+				StaticCredentialsProvider.create(
+					AwsBasicCredentials.create(accessKey, secretKey)
+				)
+			);
 
 		if (bUseIAM) {
 			logger.debug("Using IAM Credentials");
@@ -135,48 +144,77 @@ private AmazonS3 buildS3Client(String accessKey, String secretKey, String endpoi
 			pathStyleUrls = true;
 
 			s3Client = builder
-				.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
-				.disableChunkedEncoding()
-				.enablePathStyleAccess()
+				.endpointOverride(URI.create(endpoint))
+				.region(Region.of(region))
+				.serviceConfiguration(S3Configuration.builder()
+					.pathStyleAccessEnabled(true)
+					.build())
 				.build();
 			endpointUrl = endpoint;
 		}
 		else {
 			pathStyleUrls = false;
-			s3Client = builder
-				.withRegion(region)
-				.build();
 			if (endpoint.equals(ACCELERATED)) {
-				s3Client.setS3ClientOptions(S3ClientOptions.builder().setAccelerateModeEnabled(true).build());
+				s3Client = builder
+					.region(Region.of(region))
+					.serviceConfiguration(S3Configuration.builder()
+						.accelerateModeEnabled(true)
+						.build())
+					.build();
 			}
 			else if (endpoint.equals(DUALSTACK)) {
-				s3Client.setS3ClientOptions(S3ClientOptions.builder().enableDualstack().setAccelerateModeEnabled(true).build());
+				s3Client = builder
+					.region(Region.of(region))
+					.serviceConfiguration(S3Configuration.builder()
+						.dualstackEnabled(true)
+						.accelerateModeEnabled(true)
+						.build())
+					.build();
+			} else {
+				s3Client = builder
+					.region(Region.of(region))
+					.build();
 			}
 		}
 		return s3Client;
 	}
 
     private void bucketExists() {
-        if (!client.doesBucketExistV2(bucket)) {
-            logger.debug(String.format("Bucket %s doesn't exist, please create the bucket", bucket));
-        }
+		// There is no "bucket.exists" method, so we attempt to get metadata about the bucket
+		// and if we get a 404 error then it means the bucket does not exist
+		try {
+			HeadBucketRequest headBucketRequest = HeadBucketRequest.builder()
+				.bucket(bucket)
+				.build();
+			client.headBucket(headBucketRequest);
+		} catch (S3Exception e) {
+			if (e.statusCode() == 404)
+				logger.debug(String.format("Bucket %s doesn't exist, please create the bucket", bucket));
+			else
+				logger.debug("Something went wrong while checking for bucket existence", e);
+		}
     }
 
     private String ensureFolder(String... pathPart) {
 		String folderName = buildPath(pathPart);
-        ObjectMetadata metadata = new ObjectMetadata();
-        metadata.setContentLength(0);
-        InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
-        PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, StorageUtils.normalizeDirectoryName(folderName), emptyContent, metadata);
-        client.putObject(putObjectRequest);
-        return folderName;
+		PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+			.bucket(bucket)
+			.key(StorageUtils.normalizeDirectoryName(folderName))
+			.contentLength(0L)
+			.build();
+		client.putObject(putObjectRequest, RequestBody.fromBytes(new byte[0]));
+		return folderName;
     }
 
     public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
         try {
-            S3Object object = client.getObject(new GetObjectRequest(bucket, externalFileName));
-            try (InputStream objectData = object.getObjectContent()) {
-				try (OutputStream outputStream = new FileOutputStream(new File(localFile))){
+			ResponseBytes<GetObjectResponse> objectBytes = client.getObject(GetObjectRequest.builder()
+					.bucket(bucket)
+					.key(externalFileName)
+					.build(),
+				ResponseTransformer.toBytes());
+            try (InputStream objectData = objectBytes.asInputStream()) {
+				try (OutputStream outputStream = new FileOutputStream(localFile)){
 					int read;
 					byte[] bytes = new byte[1024];
 					while ((read = objectData.read(bytes)) != -1) {
@@ -192,24 +230,33 @@ public void download(String externalFileName, String localFile, ResourceAccessCo
     }
 
     public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
-        client.putObject(new PutObjectRequest(bucket, externalFileName, new File(localFile)).withCannedAcl(internalToAWSACL(acl)));
+		client.putObject(PutObjectRequest.builder()
+				.bucket(bucket)
+				.key(externalFileName)
+				.build(),
+			RequestBody.fromFile(Paths.get(localFile)));
+		client.putObjectAcl(PutObjectAclRequest.builder()
+			.bucket(bucket)
+			.key(externalFileName)
+			.acl(internalToAWSACL(acl))
+			.build());
         return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
     }
 
-    private CannedAccessControlList internalToAWSACL(ResourceAccessControlList acl) {
+    private ObjectCannedACL internalToAWSACL(ResourceAccessControlList acl) {
         if (acl == ResourceAccessControlList.Default) {
         	acl = this.defaultAcl;
 		}
 
-		CannedAccessControlList accessControl = CannedAccessControlList.Private;
+		ObjectCannedACL accessControl = ObjectCannedACL.PRIVATE;
         if (acl == ResourceAccessControlList.Private) {
-            accessControl = CannedAccessControlList.Private;
+            accessControl = ObjectCannedACL.PRIVATE;
         }
 		else if (acl == ResourceAccessControlList.PublicRead) {
-			accessControl = CannedAccessControlList.PublicRead;
+			accessControl = ObjectCannedACL.PUBLIC_READ;
 		}
 		else if (acl == ResourceAccessControlList.PublicReadWrite) {
-			accessControl = CannedAccessControlList.PublicReadWrite;
+			accessControl = ObjectCannedACL.PUBLIC_READ_WRITE;
 		}
         return accessControl;
     }
@@ -217,15 +264,20 @@ else if (acl == ResourceAccessControlList.PublicReadWrite) {
     public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
         byte[] bytes;
         try {
-            bytes = IOUtils.toByteArray(input);
-            ObjectMetadata metadata = new ObjectMetadata();
-            metadata.setContentLength(bytes.length);
-            if (externalFileName.endsWith(".tmp")) {
-                metadata.setContentType("image/jpeg");
-            }
-            String upload = "";
-            try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
-				client.putObject(new PutObjectRequest(bucket, externalFileName, byteArrayInputStream, metadata).withCannedAcl(internalToAWSACL(acl)));
+            bytes = IoUtils.toByteArray(input);
+			PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder()
+				.bucket(bucket)
+				.key(externalFileName)
+				.contentLength((long) bytes.length);
+			if (externalFileName.endsWith(".tmp")) {
+				putRequestBuilder.contentType("image/jpeg");
+			}
+			ObjectCannedACL cannedACL = internalToAWSACL(acl);
+			putRequestBuilder.acl(cannedACL);
+
+            String upload;
+			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
+				client.putObject(putRequestBuilder.build(), RequestBody.fromInputStream(byteArrayInputStream, bytes.length));
 				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
 			}
 			return upload;
@@ -236,29 +288,57 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC
     }
 
     public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-        client.getObjectMetadata(bucket, externalFileName);
+		// Send a request to AWS S3 to retrieve the metadata for the specified object to see if
+		// the object exists and is accessible under the provided credentials and permissions.
+		try {
+			HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+				.bucket(bucket)
+				.key(externalFileName)
+				.build();
+			client.headObject(headObjectRequest);
+		} catch (NoSuchKeyException e) {
+			logger.info("The resource " + externalFileName +" is probably private or non-existent", e);
+		}
+
 		return getResourceUrl(externalFileName, acl, expirationMinutes);
 	}
 
 	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-		if (internalToAWSACL(acl) == CannedAccessControlList.Private) {
-			expirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
+		if (internalToAWSACL(acl) == ObjectCannedACL.PRIVATE) {
+			final int finalExpirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
 			Date expiration = new Date();
 			long msec = expiration.getTime();
 			msec += 60000 * expirationMinutes;
 			expiration.setTime(msec);
 
-			GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, externalFileName);
-			generatePresignedUrlRequest.setMethod(HttpMethod.GET);
-			generatePresignedUrlRequest.setExpiration(expiration);
-			return client.generatePresignedUrl(generatePresignedUrlRequest).toString();
+			S3Presigner presigner = S3Presigner.builder()
+				.region(Region.of(clientRegion))
+				.build();
+
+			GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+				.bucket(bucket)
+				.key(externalFileName)
+				.build();
+
+			PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(z -> z.signatureDuration(Duration.ofMinutes(finalExpirationMinutes))
+				.getObjectRequest(getObjectRequest));
+
+			String presignedUrl = presignedRequest.url().toString();
+			presigner.close();
+
+			return presignedUrl;
 		} else {
-			return ((AmazonS3Client) client).getResourceUrl(bucket, externalFileName);
+			return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, Region.of(clientRegion), externalFileName);
 		}
+
 	}
 
 	public void delete(String objectName, ResourceAccessControlList acl) {
-        client.deleteObject(bucket, objectName);
+		DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
+			.bucket(bucket)
+			.key(objectName)
+			.build();
+		client.deleteObject(deleteObjectRequest);
     }
 
     public String rename(String objectName, String newName, ResourceAccessControlList acl) {
@@ -268,8 +348,13 @@ public String rename(String objectName, String newName, ResourceAccessControlLis
     }
 
     public String copy(String objectName, String newName, ResourceAccessControlList acl) {
-        CopyObjectRequest request = new CopyObjectRequest(bucket, objectName, bucket, newName);
-        request.setCannedAccessControlList(internalToAWSACL(acl));
+		CopyObjectRequest request = CopyObjectRequest.builder()
+			.sourceBucket(bucket)
+			.sourceKey(objectName)
+			.destinationBucket(bucket)
+			.destinationKey(newName)
+			.acl(internalToAWSACL(acl))
+			.build();
         client.copyObject(request);
         return getResourceUrl(newName, acl, defaultExpirationMinutes);
     }
@@ -285,15 +370,22 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 			logger.error("Failed to Parse Storage Object URI for Copy operation", e);
 		}
 
-        ObjectMetadata metadata = new ObjectMetadata();
-        metadata.addUserMetadata("Table", tableName);
-        metadata.addUserMetadata("Field", fieldName);
-        metadata.addUserMetadata("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
+		Map<String, String> metadata = new HashMap<>();
+		metadata.put("Table", tableName);
+		metadata.put("Field", fieldName);
+		metadata.put("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
 
-        CopyObjectRequest request = new CopyObjectRequest(bucket, objectUrl, bucket, resourceKey);
-        request.setNewObjectMetadata(metadata);
-        request.setCannedAccessControlList(internalToAWSACL(acl));
-        client.copyObject(request);
+		CopyObjectRequest request = CopyObjectRequest.builder()
+			.sourceBucket(bucket)
+			.sourceKey(objectUrl)
+			.destinationBucket(bucket)
+			.destinationKey(newName)
+			.metadata(metadata)
+			.metadataDirective(MetadataDirective.REPLACE) // This will replace the metadata of the copied object
+			.acl(internalToAWSACL(acl))
+			.build();
+
+		client.copyObject(request);
 
 		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
     }
@@ -307,21 +399,37 @@ private String buildPath(String... pathPart) {
 		}
 		return String.join(StorageUtils.DELIMITER, pathParts);
 	}
+
     public long getLength(String objectName, ResourceAccessControlList acl) {
-        ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
-        return obj.getInstanceLength();
+		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+			.bucket(bucket)
+			.key(objectName)
+			.build();
+
+		HeadObjectResponse objectHead = client.headObject(headObjectRequest);
+		return objectHead.contentLength();
     }
 
     public Date getLastModified(String objectName, ResourceAccessControlList acl) {
-        ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
-        return obj.getLastModified();
+		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+			.bucket(bucket)
+			.key(objectName)
+			.build();
+
+		HeadObjectResponse objectHead = client.headObject(headObjectRequest);
+		return Date.from(objectHead.lastModified());
     }
 
     public boolean exists(String objectName, ResourceAccessControlList acl) {
         try {
-            client.getObjectMetadata(bucket, objectName);
-        } catch (AmazonS3Exception ex) {
-            if (ex.getStatusCode() == 404) {
+			HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+				.bucket(bucket)
+				.key(objectName)
+				.build();
+
+			client.headObject(headObjectRequest);
+        } catch (S3Exception ex) {
+            if (ex.statusCode() == 404) {
                 return false;
             }
         }
@@ -337,12 +445,13 @@ public String getDirectory(String directoryName) {
     }
 
 	public boolean existsDirectory(String directoryName) {
-		ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
-			.withBucketName(bucket)
-			.withDelimiter(StorageUtils.DELIMITER)
-			.withPrefix(StorageUtils.normalizeDirectoryName(directoryName))
-			.withMaxKeys(1);
-		return client.listObjectsV2(listObjectsRequest).getKeyCount() > 0;
+		ListObjectsV2Request listObjectsRequest = ListObjectsV2Request.builder()
+			.bucket(bucket)
+			.delimiter(StorageUtils.DELIMITER)
+			.prefix(StorageUtils.normalizeDirectoryName(directoryName))
+			.maxKeys(1)
+			.build();
+		return client.listObjectsV2(listObjectsRequest).keyCount() > 0;
 	}
 
     public void createDirectory(String directoryName) {
@@ -350,48 +459,78 @@ public void createDirectory(String directoryName) {
     }
 
     public void deleteDirectory(String directoryName) {
-        for (S3ObjectSummary file : client.listObjects(bucket, directoryName).getObjectSummaries()) {
-            client.deleteObject(bucket, file.getKey());
-        }
-        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-            .withBucketName(bucket).withDelimiter(StorageUtils.DELIMITER);
-        ObjectListing list = client.listObjects(listObjectsRequest);
-        List<String> toRemove = new ArrayList<String>();
-        List<String> prefixes = list.getCommonPrefixes();
-        for (String prefix : prefixes) {
-            if (prefix.startsWith(directoryName)) {
-                toRemove.add(prefix);
-            }
-        }
-        prefixes.removeAll(toRemove);
-        list.setCommonPrefixes(prefixes);
+		ListObjectsV2Request listReq = ListObjectsV2Request.builder()
+			.bucket(bucket)
+			.prefix(directoryName)
+			.build();
+
+		ListObjectsV2Response listRes;
+		do {
+			listRes = client.listObjectsV2(listReq);
+			for (S3Object s3Object : listRes.contents()) {
+				DeleteObjectRequest deleteReq = DeleteObjectRequest.builder()
+					.bucket(bucket)
+					.key(s3Object.key())
+					.build();
+				client.deleteObject(deleteReq);
+			}
+
+			listReq = listReq.toBuilder().continuationToken(listRes.nextContinuationToken()).build();
+		} while (listRes.isTruncated());
     }
 
     public void renameDirectory(String directoryName, String newDirectoryName) {
-        directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-        newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
-        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-            .withBucketName(bucket).withPrefix(directoryName);
-        for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
-            String newKey = file.getKey().replace(directoryName, newDirectoryName);
-            rename(file.getKey(), newKey, null);
-        }
-        deleteDirectory(directoryName);
+        final String finalDirectoryName = StorageUtils.normalizeDirectoryName(directoryName);
+        final String finalNewDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
+
+		ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder()
+			.bucket(bucket)
+			.prefix(directoryName)
+			.build();
+
+		client.listObjects(listObjectsRequest).contents().forEach(file -> {
+			String newKey = file.key().replace(finalDirectoryName, finalNewDirectoryName);
+
+			CopyObjectRequest copyObjectRequest = CopyObjectRequest.builder()
+				.sourceBucket(bucket)
+				.sourceKey(file.key())
+				.destinationBucket(bucket)
+				.destinationKey(newKey)
+				.build();
+			client.copyObject(copyObjectRequest);
+
+			DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
+				.bucket(bucket)
+				.key(file.key())
+				.build();
+			client.deleteObject(deleteObjectRequest);
+		});
+
+        deleteDirectory(finalDirectoryName);
     }
 
     public List<String> getFiles(String directoryName, String filter) {
-        filter = (filter == null || filter.isEmpty())? null: filter.replace("*", "");
-        List<String> files = new ArrayList<String>();
-        directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-            .withBucketName(bucket).withPrefix(directoryName).withDelimiter(StorageUtils.DELIMITER);
-        for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
-            String key = file.getKey();
-            if (isFile(directoryName, key) && (filter == null || filter.isEmpty() || key.contains(filter))) {
-                files.add(key);
-            }
-        }
-        return files;
+		filter = (filter == null || filter.isEmpty()) ? null : filter.replace("*", "");
+		List<String> files = new ArrayList<>();
+
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+
+		ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder()
+			.bucket(bucket)
+			.prefix(directoryName)
+			.delimiter(StorageUtils.DELIMITER)
+			.build();
+
+		ListObjectsResponse response = client.listObjects(listObjectsRequest);
+
+		for (S3Object file : response.contents()) {
+			String key = file.key();
+			if (isFile(directoryName, key) && (filter == null || filter.isEmpty() || key.contains(filter))) {
+				files.add(key);
+			}
+		}
+
+		return files;
     }
 
     private boolean isFile(String directory, String name) {
@@ -404,22 +543,34 @@ public List<String> getFiles(String directoryName) {
 
     public List<String> getSubDirectories(String directoryName) {
         directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-            .withBucketName(bucket).withPrefix(directoryName)
-            .withDelimiter(StorageUtils.DELIMITER);
-        ObjectListing objects = client.listObjects(listObjectsRequest);
-        return objects.getCommonPrefixes();
-    }
+		ListObjectsV2Request listReq = ListObjectsV2Request.builder()
+			.bucket(bucket)
+			.prefix(directoryName)
+			.delimiter("/")
+			.build();
+
+		List<String> subdirectories = new ArrayList<>();
 
-    public InputStream getStream(String objectName, ResourceAccessControlList acl) {
-        S3Object object = client.getObject(new GetObjectRequest(bucket, objectName));
-        return object.getObjectContent();
+		ListObjectsV2Response listRes = client.listObjectsV2(listReq);
+		listRes.commonPrefixes().forEach(prefix -> subdirectories.add(prefix.prefix()));
+
+		return subdirectories;
     }
 
+	public InputStream getStream(String objectName, ResourceAccessControlList acl) {
+		GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+			.bucket(bucket)
+			.key(objectName)
+			.build();
+
+		ResponseInputStream<GetObjectResponse> object = client.getObject(getObjectRequest);
+		return object;
+	}
+
     public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
         try {
-            AmazonS3Exception aex = (AmazonS3Exception) ex;
-            msg.setId(aex.getErrorCode());
+            S3Exception s3Exception = (S3Exception) ex;
+            msg.setId(s3Exception.awsErrorDetails().errorCode());
             return true;
         } catch (Exception e) {
             return false;
@@ -441,4 +592,5 @@ private String getStorageUri()
 			String.format("%s/%s/",  this.endpointUrl, this.bucket);
 	}
 }
+
 //http://192.168.254.78:9000/java-classes-unittests/text.txt
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index cde46401b..db16b94c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
 		<poi.version>5.2.2</poi.version>
 		<jackson.version>2.14.1</jackson.version>
 		<junit.version>4.13.2</junit.version>
-		<com.amazonaws.version>1.12.587</com.amazonaws.version>
+		<com.amazonaws.version>2.21.27</com.amazonaws.version>
 		<software.awssdk.version>2.21.21</software.awssdk.version>
 		<software.azure.cosmos.version>4.42.0</software.azure.cosmos.version>
 		<log4j.version>2.21.1</log4j.version>

From 68a3ac57942b355648579a0c2d0752d5e8d8e534 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Thu, 23 Nov 2023 14:48:15 -0300
Subject: [PATCH 02/23] Try fix missing credentials when gettin presinged url

---
 .../genexus/db/driver/ExternalProviderS3.java | 36 +++++++++----------
 1 file changed, 18 insertions(+), 18 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index ba741d8dd..4bcde11d8 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -33,6 +33,7 @@
 import java.net.URI;
 import java.nio.file.Paths;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.*;
 
 
@@ -68,6 +69,7 @@ public class ExternalProviderS3 extends ExternalProviderBase implements External
     static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
 	static final String DEFAULT_REGION = "us-east-1";
     private S3Client client;
+	private S3Presigner presigner;
 	private String clientRegion = "";
     private String bucket;
     private String folder;
@@ -93,7 +95,7 @@ public ExternalProviderS3(GXService providerService) throws Exception{
 		initialize();
     }
 
-	private void initialize() throws Exception{
+	private void initialize() throws Exception {
 		String accessKey = getEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED, "");
 		String secretKey = getEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED, "");
 		String bucket = getEncryptedPropertyValue(BUCKET, BUCKET_DEPRECATED);
@@ -119,6 +121,7 @@ private void initialize() throws Exception{
 			this.folder = folder;
 
 			this.client = buildS3Client(accessKey, secretKey, endpointValue, clientRegion);
+			this.presigner = buildS3Presinger(accessKey, secretKey, clientRegion);
 			bucketExists();
 		}
 	}
@@ -179,6 +182,13 @@ else if (endpoint.equals(DUALSTACK)) {
 		return s3Client;
 	}
 
+	private S3Presigner buildS3Presinger(String accessKey, String secretKey, String region){
+		return S3Presigner.builder()
+			.region(Region.of(region))
+			.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
+			.build();
+	}
+
     private void bucketExists() {
 		// There is no "bucket.exists" method, so we attempt to get metadata about the bucket
 		// and if we get a 404 error then it means the bucket does not exist
@@ -303,34 +313,24 @@ public String get(String externalFileName, ResourceAccessControlList acl, int ex
 		return getResourceUrl(externalFileName, acl, expirationMinutes);
 	}
 
-	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+	public String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
 		if (internalToAWSACL(acl) == ObjectCannedACL.PRIVATE) {
-			final int finalExpirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
-			Date expiration = new Date();
-			long msec = expiration.getTime();
-			msec += 60000 * expirationMinutes;
-			expiration.setTime(msec);
-
-			S3Presigner presigner = S3Presigner.builder()
-				.region(Region.of(clientRegion))
-				.build();
+			expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes;
+			Instant expiration = Instant.now().plus(Duration.ofMinutes(expirationMinutes));
 
 			GetObjectRequest getObjectRequest = GetObjectRequest.builder()
 				.bucket(bucket)
 				.key(externalFileName)
 				.build();
 
-			PresignedGetObjectRequest presignedRequest = presigner.presignGetObject(z -> z.signatureDuration(Duration.ofMinutes(finalExpirationMinutes))
-				.getObjectRequest(getObjectRequest));
-
-			String presignedUrl = presignedRequest.url().toString();
-			presigner.close();
+			PresignedGetObjectRequest presignedGetObjectRequest =
+				presigner.presignGetObject(r -> r.signatureDuration(Duration.between(Instant.now(), expiration))
+					.getObjectRequest(getObjectRequest));
 
-			return presignedUrl;
+			return presignedGetObjectRequest.url().toString();
 		} else {
 			return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, Region.of(clientRegion), externalFileName);
 		}
-
 	}
 
 	public void delete(String objectName, ResourceAccessControlList acl) {

From 08db9b6be7d64de7c88b42080295f868909210ed Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Fri, 24 Nov 2023 13:31:43 -0300
Subject: [PATCH 03/23] Fix null resource name from URL

---
 .../src/main/java/com/genexus/db/driver/ExternalProviderS3.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 4bcde11d8..09c9cb828 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -583,7 +583,7 @@ public String getObjectNameFromURL(String url) {
 		{
 			objectName = url.replace(this.getStorageUri(), "");
 		}
-		return objectName;
+		return objectName != null ? objectName : url.replaceAll(".*[\\\\/]", "");
 	}
 
 	private String getStorageUri()

From 38c2154762b5ff060859cf121385b2ab197ccba0 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 27 Nov 2023 09:51:24 -0300
Subject: [PATCH 04/23] Wrong permission level in class method

---
 .../src/main/java/com/genexus/db/driver/ExternalProviderS3.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 09c9cb828..a3bb66220 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -313,7 +313,7 @@ public String get(String externalFileName, ResourceAccessControlList acl, int ex
 		return getResourceUrl(externalFileName, acl, expirationMinutes);
 	}
 
-	public String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
 		if (internalToAWSACL(acl) == ObjectCannedACL.PRIVATE) {
 			expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes;
 			Instant expiration = Instant.now().plus(Duration.ofMinutes(expirationMinutes));

From 261858fc4f075757370b7a480ba76e12dfdcd55a Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 4 Dec 2023 13:40:32 -0300
Subject: [PATCH 05/23] Removed usage of com.amazonaws.version and bump
 software.awssdk.version to 2.21.27

---
 gxcloudstorage-awss3/pom.xml | 2 +-
 pom.xml                      | 3 +--
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3/pom.xml
index 0ab39297e..62987c89e 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3/pom.xml
@@ -27,7 +27,7 @@
         <dependency>
             <groupId>software.amazon.awssdk</groupId>
             <artifactId>s3</artifactId>
-            <version>${com.amazonaws.version}</version>
+            <version>${software.awssdk.version}</version>
         </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index db16b94c9..315054fb7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,8 +24,7 @@
 		<poi.version>5.2.2</poi.version>
 		<jackson.version>2.14.1</jackson.version>
 		<junit.version>4.13.2</junit.version>
-		<com.amazonaws.version>2.21.27</com.amazonaws.version>
-		<software.awssdk.version>2.21.21</software.awssdk.version>
+		<software.awssdk.version>2.21.27</software.awssdk.version>
 		<software.azure.cosmos.version>4.42.0</software.azure.cosmos.version>
 		<log4j.version>2.21.1</log4j.version>
         <io.opentelemetry.version>1.28.0</io.opentelemetry.version>

From 1b55b5fc69077cc17ea896b67b7519170cb10286 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 5 Dec 2023 11:09:12 -0300
Subject: [PATCH 06/23] Fixed broken file extension when uploading images

---
 .../java/com/genexus/db/driver/ExternalProviderS3.java    | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index a3bb66220..1ed6ae817 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -277,13 +277,13 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC
             bytes = IoUtils.toByteArray(input);
 			PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder()
 				.bucket(bucket)
-				.key(externalFileName)
 				.contentLength((long) bytes.length);
 			if (externalFileName.endsWith(".tmp")) {
 				putRequestBuilder.contentType("image/jpeg");
+				externalFileName = externalFileName.replace(".tmp", ".jpeg");
 			}
-			ObjectCannedACL cannedACL = internalToAWSACL(acl);
-			putRequestBuilder.acl(cannedACL);
+			putRequestBuilder.acl(internalToAWSACL(acl));
+			putRequestBuilder.key(externalFileName);
 
             String upload;
 			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
@@ -300,6 +300,8 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC
     public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
 		// Send a request to AWS S3 to retrieve the metadata for the specified object to see if
 		// the object exists and is accessible under the provided credentials and permissions.
+		if (externalFileName.endsWith(".tmp"))
+			externalFileName = externalFileName.replace(".tmp", ".jpeg");
 		try {
 			HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
 				.bucket(bucket)

From acac4094ffc074c843a6b9a77971f0a7dc97afb0 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 5 Dec 2023 13:10:51 -0300
Subject: [PATCH 07/23] Fix broken retrival of object name from bucket

---
 .../src/main/java/com/genexus/db/driver/ExternalProviderS3.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 1ed6ae817..f449bff62 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -585,7 +585,7 @@ public String getObjectNameFromURL(String url) {
 		{
 			objectName = url.replace(this.getStorageUri(), "");
 		}
-		return objectName != null ? objectName : url.replaceAll(".*[\\\\/]", "");
+		return objectName;
 	}
 
 	private String getStorageUri()

From 7a11ac516dd6695213dc115ab9866af62c8286f8 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 5 Dec 2023 15:48:36 -0300
Subject: [PATCH 08/23] Consider storage region when getting storage uri

---
 .../main/java/com/genexus/db/driver/ExternalProviderS3.java    | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index f449bff62..3b74eacc3 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -590,8 +590,7 @@ public String getObjectNameFromURL(String url) {
 
 	private String getStorageUri()
 	{
-		return (!pathStyleUrls) ? String.format("https://%s%s", this.bucket, this.endpointUrl):
-			String.format("%s/%s/",  this.endpointUrl, this.bucket);
+		return "https://" + bucket + ".s3." + Region.of(clientRegion) + ".amazonaws.com/";
 	}
 }
 

From 2cd54b0b3d83bdd3c892c705875d9f0250b37fbc Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Wed, 6 Dec 2023 13:57:39 -0300
Subject: [PATCH 09/23] Do not catch non-existant resource exception

---
 .../genexus/db/driver/ExternalProviderS3.java | 290 +++++++++---------
 1 file changed, 141 insertions(+), 149 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 3b74eacc3..3bcefaeb6 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -37,7 +37,7 @@
 import java.util.*;
 
 
-public class ExternalProviderS3 extends ExternalProviderBase implements ExternalProvider  {
+public class ExternalProviderS3 extends ExternalProviderBase implements ExternalProvider {
 	private static Logger logger = LogManager.getLogger(ExternalProviderS3.class);
 
 	static final String NAME = "AWSS3";
@@ -65,35 +65,35 @@ public class ExternalProviderS3 extends ExternalProviderBase implements External
 	@Deprecated
 	static final String REGION_DEPRECATED = "STORAGE_PROVIDER_REGION";
 
-    static final String ACCELERATED = "s3-accelerate.amazonaws.com";
-    static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
+	static final String ACCELERATED = "s3-accelerate.amazonaws.com";
+	static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
 	static final String DEFAULT_REGION = "us-east-1";
-    private S3Client client;
+	private S3Client client;
 	private S3Presigner presigner;
 	private String clientRegion = "";
-    private String bucket;
-    private String folder;
-    private String endpointUrl = ".s3.amazonaws.com/";
+	private String bucket;
+	private String folder;
+	private String endpointUrl = ".s3.amazonaws.com/";
 	private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES;
 	private Boolean pathStyleUrls = false;
 
-	public String getName(){
+	public String getName() {
 		return NAME;
 	}
 
-	public ExternalProviderS3(String service) throws Exception{
+	public ExternalProviderS3(String service) throws Exception {
 		this(Application.getGXServices().get(service));
 	}
 
-	public ExternalProviderS3() throws Exception{
+	public ExternalProviderS3() throws Exception {
 		super();
 		initialize();
 	}
 
-	public ExternalProviderS3(GXService providerService) throws Exception{
-    	super(providerService);
+	public ExternalProviderS3(GXService providerService) throws Exception {
+		super(providerService);
 		initialize();
-    }
+	}
 
 	private void initialize() throws Exception {
 		String accessKey = getEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED, "");
@@ -154,8 +154,7 @@ private S3Client buildS3Client(String accessKey, String secretKey, String endpoi
 					.build())
 				.build();
 			endpointUrl = endpoint;
-		}
-		else {
+		} else {
 			pathStyleUrls = false;
 			if (endpoint.equals(ACCELERATED)) {
 				s3Client = builder
@@ -164,8 +163,7 @@ private S3Client buildS3Client(String accessKey, String secretKey, String endpoi
 						.accelerateModeEnabled(true)
 						.build())
 					.build();
-			}
-			else if (endpoint.equals(DUALSTACK)) {
+			} else if (endpoint.equals(DUALSTACK)) {
 				s3Client = builder
 					.region(Region.of(region))
 					.serviceConfiguration(S3Configuration.builder()
@@ -182,14 +180,14 @@ else if (endpoint.equals(DUALSTACK)) {
 		return s3Client;
 	}
 
-	private S3Presigner buildS3Presinger(String accessKey, String secretKey, String region){
+	private S3Presigner buildS3Presinger(String accessKey, String secretKey, String region) {
 		return S3Presigner.builder()
 			.region(Region.of(region))
 			.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)))
 			.build();
 	}
 
-    private void bucketExists() {
+	private void bucketExists() {
 		// There is no "bucket.exists" method, so we attempt to get metadata about the bucket
 		// and if we get a 404 error then it means the bucket does not exist
 		try {
@@ -203,9 +201,9 @@ private void bucketExists() {
 			else
 				logger.debug("Something went wrong while checking for bucket existence", e);
 		}
-    }
+	}
 
-    private String ensureFolder(String... pathPart) {
+	private String ensureFolder(String... pathPart) {
 		String folderName = buildPath(pathPart);
 		PutObjectRequest putObjectRequest = PutObjectRequest.builder()
 			.bucket(bucket)
@@ -214,17 +212,17 @@ private String ensureFolder(String... pathPart) {
 			.build();
 		client.putObject(putObjectRequest, RequestBody.fromBytes(new byte[0]));
 		return folderName;
-    }
+	}
 
-    public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
-        try {
+	public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
+		try {
 			ResponseBytes<GetObjectResponse> objectBytes = client.getObject(GetObjectRequest.builder()
 					.bucket(bucket)
 					.key(externalFileName)
 					.build(),
 				ResponseTransformer.toBytes());
-            try (InputStream objectData = objectBytes.asInputStream()) {
-				try (OutputStream outputStream = new FileOutputStream(localFile)){
+			try (InputStream objectData = objectBytes.asInputStream()) {
+				try (OutputStream outputStream = new FileOutputStream(localFile)) {
 					int read;
 					byte[] bytes = new byte[1024];
 					while ((read = objectData.read(bytes)) != -1) {
@@ -232,14 +230,14 @@ public void download(String externalFileName, String localFile, ResourceAccessCo
 					}
 				}
 			}
-        } catch (FileNotFoundException ex) {
-            logger.error("Error while downloading file to the external provider", ex);
-        } catch (IOException ex) {
-            logger.error("Error while downloading file to the external provider", ex);
-        }
-    }
-
-    public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
+		} catch (FileNotFoundException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		} catch (IOException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		}
+	}
+
+	public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
 		client.putObject(PutObjectRequest.builder()
 				.bucket(bucket)
 				.key(externalFileName)
@@ -250,31 +248,29 @@ public String upload(String localFile, String externalFileName, ResourceAccessCo
 			.key(externalFileName)
 			.acl(internalToAWSACL(acl))
 			.build());
-        return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
-    }
+		return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+	}
 
-    private ObjectCannedACL internalToAWSACL(ResourceAccessControlList acl) {
-        if (acl == ResourceAccessControlList.Default) {
-        	acl = this.defaultAcl;
+	private ObjectCannedACL internalToAWSACL(ResourceAccessControlList acl) {
+		if (acl == ResourceAccessControlList.Default) {
+			acl = this.defaultAcl;
 		}
 
 		ObjectCannedACL accessControl = ObjectCannedACL.PRIVATE;
-        if (acl == ResourceAccessControlList.Private) {
-            accessControl = ObjectCannedACL.PRIVATE;
-        }
-		else if (acl == ResourceAccessControlList.PublicRead) {
+		if (acl == ResourceAccessControlList.Private) {
+			accessControl = ObjectCannedACL.PRIVATE;
+		} else if (acl == ResourceAccessControlList.PublicRead) {
 			accessControl = ObjectCannedACL.PUBLIC_READ;
-		}
-		else if (acl == ResourceAccessControlList.PublicReadWrite) {
+		} else if (acl == ResourceAccessControlList.PublicReadWrite) {
 			accessControl = ObjectCannedACL.PUBLIC_READ_WRITE;
 		}
-        return accessControl;
-    }
+		return accessControl;
+	}
 
-    public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
-        byte[] bytes;
-        try {
-            bytes = IoUtils.toByteArray(input);
+	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
+		byte[] bytes;
+		try {
+			bytes = IoUtils.toByteArray(input);
 			PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder()
 				.bucket(bucket)
 				.contentLength((long) bytes.length);
@@ -285,32 +281,29 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC
 			putRequestBuilder.acl(internalToAWSACL(acl));
 			putRequestBuilder.key(externalFileName);
 
-            String upload;
+			String upload;
 			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
 				client.putObject(putRequestBuilder.build(), RequestBody.fromInputStream(byteArrayInputStream, bytes.length));
 				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
 			}
 			return upload;
-        } catch (IOException ex) {
-            logger.error("Error while uploading file to the external provider.", ex);
-            return "";
-        }
-    }
+		} catch (IOException ex) {
+			logger.error("Error while uploading file to the external provider.", ex);
+			return "";
+		}
+	}
 
-    public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-		// Send a request to AWS S3 to retrieve the metadata for the specified object to see if
-		// the object exists and is accessible under the provided credentials and permissions.
+	public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
 		if (externalFileName.endsWith(".tmp"))
 			externalFileName = externalFileName.replace(".tmp", ".jpeg");
-		try {
-			HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
-				.bucket(bucket)
-				.key(externalFileName)
-				.build();
-			client.headObject(headObjectRequest);
-		} catch (NoSuchKeyException e) {
-			logger.info("The resource " + externalFileName +" is probably private or non-existent", e);
-		}
+
+		// Send a request to AWS S3 to retrieve the metadata for the specified object to see if
+		// the object exists and is accessible under the provided credentials and permissions.
+		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
+			.bucket(bucket)
+			.key(externalFileName)
+			.build();
+		client.headObject(headObjectRequest);
 
 		return getResourceUrl(externalFileName, acl, expirationMinutes);
 	}
@@ -331,7 +324,7 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList
 
 			return presignedGetObjectRequest.url().toString();
 		} else {
-			return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, Region.of(clientRegion), externalFileName);
+			return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, clientRegion, externalFileName);
 		}
 	}
 
@@ -341,15 +334,15 @@ public void delete(String objectName, ResourceAccessControlList acl) {
 			.key(objectName)
 			.build();
 		client.deleteObject(deleteObjectRequest);
-    }
+	}
 
-    public String rename(String objectName, String newName, ResourceAccessControlList acl) {
-        String url = copy(objectName, newName, acl);
-        delete(objectName, acl);
-        return url;
-    }
+	public String rename(String objectName, String newName, ResourceAccessControlList acl) {
+		String url = copy(objectName, newName, acl);
+		delete(objectName, acl);
+		return url;
+	}
 
-    public String copy(String objectName, String newName, ResourceAccessControlList acl) {
+	public String copy(String objectName, String newName, ResourceAccessControlList acl) {
 		CopyObjectRequest request = CopyObjectRequest.builder()
 			.sourceBucket(bucket)
 			.sourceKey(objectName)
@@ -357,18 +350,17 @@ public String copy(String objectName, String newName, ResourceAccessControlList
 			.destinationKey(newName)
 			.acl(internalToAWSACL(acl))
 			.build();
-        client.copyObject(request);
-        return getResourceUrl(newName, acl, defaultExpirationMinutes);
-    }
+		client.copyObject(request);
+		return getResourceUrl(newName, acl, defaultExpirationMinutes);
+	}
 
-    public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) {
-        String resourceFolderName = buildPath(folder, tableName, fieldName);
-        String resourceKey = resourceFolderName + StorageUtils.DELIMITER + newName;
+	public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) {
+		String resourceFolderName = buildPath(folder, tableName, fieldName);
+		String resourceKey = resourceFolderName + StorageUtils.DELIMITER + newName;
 
-        try {
+		try {
 			objectUrl = new URI(objectUrl).getPath();
-		}
-        catch (Exception e){
+		} catch (Exception e) {
 			logger.error("Failed to Parse Storage Object URI for Copy operation", e);
 		}
 
@@ -383,18 +375,18 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 			.destinationBucket(bucket)
 			.destinationKey(newName)
 			.metadata(metadata)
-			.metadataDirective(MetadataDirective.REPLACE) // This will replace the metadata of the copied object
+			.metadataDirective(MetadataDirective.REPLACE)
 			.acl(internalToAWSACL(acl))
 			.build();
 
 		client.copyObject(request);
 
 		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
-    }
+	}
 
-    private String buildPath(String... pathPart) {
+	private String buildPath(String... pathPart) {
 		ArrayList<String> pathParts = new ArrayList<>();
-		for(String part : pathPart){
+		for (String part : pathPart) {
 			if (part.length() > 0) {
 				pathParts.add(part);
 			}
@@ -402,7 +394,7 @@ private String buildPath(String... pathPart) {
 		return String.join(StorageUtils.DELIMITER, pathParts);
 	}
 
-    public long getLength(String objectName, ResourceAccessControlList acl) {
+	public long getLength(String objectName, ResourceAccessControlList acl) {
 		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
 			.bucket(bucket)
 			.key(objectName)
@@ -410,9 +402,9 @@ public long getLength(String objectName, ResourceAccessControlList acl) {
 
 		HeadObjectResponse objectHead = client.headObject(headObjectRequest);
 		return objectHead.contentLength();
-    }
+	}
 
-    public Date getLastModified(String objectName, ResourceAccessControlList acl) {
+	public Date getLastModified(String objectName, ResourceAccessControlList acl) {
 		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
 			.bucket(bucket)
 			.key(objectName)
@@ -420,31 +412,31 @@ public Date getLastModified(String objectName, ResourceAccessControlList acl) {
 
 		HeadObjectResponse objectHead = client.headObject(headObjectRequest);
 		return Date.from(objectHead.lastModified());
-    }
+	}
 
-    public boolean exists(String objectName, ResourceAccessControlList acl) {
-        try {
+	public boolean exists(String objectName, ResourceAccessControlList acl) {
+		try {
 			HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
 				.bucket(bucket)
 				.key(objectName)
 				.build();
 
 			client.headObject(headObjectRequest);
-        } catch (S3Exception ex) {
-            if (ex.statusCode() == 404) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public String getDirectory(String directoryName) {
-        if (existsDirectory(directoryName)) {
-            return bucket + ":" + StorageUtils.DELIMITER + directoryName + StorageUtils.DELIMITER;
-        } else {
-            return "";
-        }
-    }
+		} catch (S3Exception ex) {
+			if (ex.statusCode() == 404) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	public String getDirectory(String directoryName) {
+		if (existsDirectory(directoryName)) {
+			return bucket + ":" + StorageUtils.DELIMITER + directoryName + StorageUtils.DELIMITER;
+		} else {
+			return "";
+		}
+	}
 
 	public boolean existsDirectory(String directoryName) {
 		ListObjectsV2Request listObjectsRequest = ListObjectsV2Request.builder()
@@ -456,11 +448,11 @@ public boolean existsDirectory(String directoryName) {
 		return client.listObjectsV2(listObjectsRequest).keyCount() > 0;
 	}
 
-    public void createDirectory(String directoryName) {
-        ensureFolder(directoryName);
-    }
+	public void createDirectory(String directoryName) {
+		ensureFolder(directoryName);
+	}
 
-    public void deleteDirectory(String directoryName) {
+	public void deleteDirectory(String directoryName) {
 		ListObjectsV2Request listReq = ListObjectsV2Request.builder()
 			.bucket(bucket)
 			.prefix(directoryName)
@@ -479,11 +471,11 @@ public void deleteDirectory(String directoryName) {
 
 			listReq = listReq.toBuilder().continuationToken(listRes.nextContinuationToken()).build();
 		} while (listRes.isTruncated());
-    }
+	}
 
-    public void renameDirectory(String directoryName, String newDirectoryName) {
-        final String finalDirectoryName = StorageUtils.normalizeDirectoryName(directoryName);
-        final String finalNewDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
+	public void renameDirectory(String directoryName, String newDirectoryName) {
+		final String finalDirectoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		final String finalNewDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
 
 		ListObjectsRequest listObjectsRequest = ListObjectsRequest.builder()
 			.bucket(bucket)
@@ -508,10 +500,10 @@ public void renameDirectory(String directoryName, String newDirectoryName) {
 			client.deleteObject(deleteObjectRequest);
 		});
 
-        deleteDirectory(finalDirectoryName);
-    }
+		deleteDirectory(finalDirectoryName);
+	}
 
-    public List<String> getFiles(String directoryName, String filter) {
+	public List<String> getFiles(String directoryName, String filter) {
 		filter = (filter == null || filter.isEmpty()) ? null : filter.replace("*", "");
 		List<String> files = new ArrayList<>();
 
@@ -533,18 +525,18 @@ public List<String> getFiles(String directoryName, String filter) {
 		}
 
 		return files;
-    }
+	}
 
-    private boolean isFile(String directory, String name) {
-        return !name.endsWith(StorageUtils.DELIMITER);
-    }
+	private boolean isFile(String directory, String name) {
+		return !name.endsWith(StorageUtils.DELIMITER);
+	}
 
-    public List<String> getFiles(String directoryName) {
-        return getFiles(directoryName, null);
-    }
+	public List<String> getFiles(String directoryName) {
+		return getFiles(directoryName, null);
+	}
 
-    public List<String> getSubDirectories(String directoryName) {
-        directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+	public List<String> getSubDirectories(String directoryName) {
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
 		ListObjectsV2Request listReq = ListObjectsV2Request.builder()
 			.bucket(bucket)
 			.prefix(directoryName)
@@ -557,7 +549,7 @@ public List<String> getSubDirectories(String directoryName) {
 		listRes.commonPrefixes().forEach(prefix -> subdirectories.add(prefix.prefix()));
 
 		return subdirectories;
-    }
+	}
 
 	public InputStream getStream(String objectName, ResourceAccessControlList acl) {
 		GetObjectRequest getObjectRequest = GetObjectRequest.builder()
@@ -569,28 +561,28 @@ public InputStream getStream(String objectName, ResourceAccessControlList acl) {
 		return object;
 	}
 
-    public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
-        try {
-            S3Exception s3Exception = (S3Exception) ex;
-            msg.setId(s3Exception.awsErrorDetails().errorCode());
-            return true;
-        } catch (Exception e) {
-            return false;
-        }
-    }
-
-    public String getObjectNameFromURL(String url) {
-    	String objectName = null;
-		if (url.startsWith(this.getStorageUri()))
-		{
+	public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
+		try {
+			S3Exception s3Exception = (S3Exception) ex;
+			msg.setId(s3Exception.awsErrorDetails().errorCode());
+			return true;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+
+	public String getObjectNameFromURL(String url) {
+		String objectName = null;
+		if (url.startsWith(this.getStorageUri())) {
 			objectName = url.replace(this.getStorageUri(), "");
 		}
 		return objectName;
 	}
 
-	private String getStorageUri()
-	{
-		return "https://" + bucket + ".s3." + Region.of(clientRegion) + ".amazonaws.com/";
+	private String getStorageUri() {
+		return (!pathStyleUrls) ?
+			"https://" + bucket + ".s3." + clientRegion + ".amazonaws.com/" :
+			".s3." + clientRegion + ".amazonaws.com//" + bucket + "/";
 	}
 }
 

From 94c47f463278ab31541ee74bec148c40497f6bd1 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Wed, 13 Dec 2023 13:14:58 -0300
Subject: [PATCH 10/23] Change copy strategy. Download bytes and reupload

---
 .../genexus/db/driver/ExternalProviderS3.java | 50 ++++++++-----------
 1 file changed, 22 insertions(+), 28 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 3bcefaeb6..7631080aa 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -25,18 +25,17 @@
 import com.genexus.StructSdtMessages_Message;
 
 import java.io.InputStream;
-import java.io.ByteArrayInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
+import java.nio.ByteBuffer;
 import java.nio.file.Paths;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.*;
 
-
 public class ExternalProviderS3 extends ExternalProviderBase implements ExternalProvider {
 	private static Logger logger = LogManager.getLogger(ExternalProviderS3.class);
 
@@ -268,25 +267,21 @@ private ObjectCannedACL internalToAWSACL(ResourceAccessControlList acl) {
 	}
 
 	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
-		byte[] bytes;
 		try {
-			bytes = IoUtils.toByteArray(input);
-			PutObjectRequest.Builder putRequestBuilder = PutObjectRequest.builder()
+			ByteBuffer byteBuffer = ByteBuffer.wrap(IoUtils.toByteArray(input));
+			PutObjectRequest putObjectRequest = PutObjectRequest.builder()
 				.bucket(bucket)
-				.contentLength((long) bytes.length);
-			if (externalFileName.endsWith(".tmp")) {
-				putRequestBuilder.contentType("image/jpeg");
-				externalFileName = externalFileName.replace(".tmp", ".jpeg");
-			}
-			putRequestBuilder.acl(internalToAWSACL(acl));
-			putRequestBuilder.key(externalFileName);
+				.key(externalFileName)
+				.contentType(externalFileName.endsWith(".tmp") ? "image/jpeg" : null)
+				.acl(internalToAWSACL(acl))
+				.build();
 
-			String upload;
-			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
-				client.putObject(putRequestBuilder.build(), RequestBody.fromInputStream(byteArrayInputStream, bytes.length));
-				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+			PutObjectResponse response = client.putObject(putObjectRequest, RequestBody.fromByteBuffer(byteBuffer));
+			if (!response.sdkHttpResponse().isSuccessful()) {
+				logger.error("Error while uploading file: " + response.sdkHttpResponse().statusText().orElse("Unknown error"));
 			}
-			return upload;
+
+			return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
 		} catch (IOException ex) {
 			logger.error("Error while uploading file to the external provider.", ex);
 			return "";
@@ -294,9 +289,6 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC
 	}
 
 	public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-		if (externalFileName.endsWith(".tmp"))
-			externalFileName = externalFileName.replace(".tmp", ".jpeg");
-
 		// Send a request to AWS S3 to retrieve the metadata for the specified object to see if
 		// the object exists and is accessible under the provided credentials and permissions.
 		HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
@@ -369,17 +361,19 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 		metadata.put("Field", fieldName);
 		metadata.put("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
 
-		CopyObjectRequest request = CopyObjectRequest.builder()
-			.sourceBucket(bucket)
-			.sourceKey(objectUrl)
-			.destinationBucket(bucket)
-			.destinationKey(newName)
+		GetObjectRequest getObjectRequest = GetObjectRequest.builder()
+			.bucket(bucket)
+			.key(objectUrl)
+			.build();
+		ResponseBytes<GetObjectResponse> objectBytes = client.getObjectAsBytes(getObjectRequest);
+
+		PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+			.bucket(bucket)
+			.key(resourceKey)
 			.metadata(metadata)
-			.metadataDirective(MetadataDirective.REPLACE)
 			.acl(internalToAWSACL(acl))
 			.build();
-
-		client.copyObject(request);
+		client.putObject(putObjectRequest, RequestBody.fromBytes(objectBytes.asByteArray()));
 
 		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
 	}

From 5ae6f85b8507d3c7f7d64dad49183febf0482ecc Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 18 Dec 2023 11:19:48 -0300
Subject: [PATCH 11/23] Fixed HTTP 500 error when confirming TRN update

---
 .../com/genexus/db/driver/ExternalProviderS3.java    | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 7631080aa..91f95e27f 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -150,6 +150,7 @@ private S3Client buildS3Client(String accessKey, String secretKey, String endpoi
 				.region(Region.of(region))
 				.serviceConfiguration(S3Configuration.builder()
 					.pathStyleAccessEnabled(true)
+					.chunkedEncodingEnabled(false)
 					.build())
 				.build();
 			endpointUrl = endpoint;
@@ -567,9 +568,10 @@ public boolean getMessageFromException(Exception ex, StructSdtMessages_Message m
 
 	public String getObjectNameFromURL(String url) {
 		String objectName = null;
-		if (url.startsWith(this.getStorageUri())) {
+		if (url.startsWith(this.getStorageUri()))
 			objectName = url.replace(this.getStorageUri(), "");
-		}
+		else if (url.startsWith(this.getStorageUriWithoutRegion()))
+			objectName = url.replace(this.getStorageUriWithoutRegion(), "");
 		return objectName;
 	}
 
@@ -578,6 +580,12 @@ private String getStorageUri() {
 			"https://" + bucket + ".s3." + clientRegion + ".amazonaws.com/" :
 			".s3." + clientRegion + ".amazonaws.com//" + bucket + "/";
 	}
+
+	private String getStorageUriWithoutRegion() {
+		return (!pathStyleUrls) ?
+			"https://" + bucket + ".s3.amazonaws.com/" :
+			".s3.amazonaws.com//" + bucket + "/";
+	}
 }
 
 //http://192.168.254.78:9000/java-classes-unittests/text.txt
\ No newline at end of file

From 44ad30364711c345550328d3f5acf7bb5b689d1e Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Wed, 20 Dec 2023 10:54:38 -0300
Subject: [PATCH 12/23] Don't use ACL feature when using custom endpoint

---
 .../genexus/db/driver/ExternalProviderS3.java | 40 +++++++++++--------
 1 file changed, 23 insertions(+), 17 deletions(-)

diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
index 91f95e27f..6f68e00b0 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
@@ -243,11 +243,14 @@ public String upload(String localFile, String externalFileName, ResourceAccessCo
 				.key(externalFileName)
 				.build(),
 			RequestBody.fromFile(Paths.get(localFile)));
-		client.putObjectAcl(PutObjectAclRequest.builder()
-			.bucket(bucket)
-			.key(externalFileName)
-			.acl(internalToAWSACL(acl))
-			.build());
+		// As of December 2023, some S3-compatible storages do not
+		// implement every AWS S3 feature such as setting an object ACL
+		if (endpointUrl.contains(".amazonaws.com"))
+			client.putObjectAcl(PutObjectAclRequest.builder()
+				.bucket(bucket)
+				.key(externalFileName)
+				.acl(internalToAWSACL(acl))
+				.build());
 		return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
 	}
 
@@ -270,12 +273,13 @@ private ObjectCannedACL internalToAWSACL(ResourceAccessControlList acl) {
 	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
 		try {
 			ByteBuffer byteBuffer = ByteBuffer.wrap(IoUtils.toByteArray(input));
-			PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+			PutObjectRequest.Builder putObjectRequestBuilder = PutObjectRequest.builder()
 				.bucket(bucket)
 				.key(externalFileName)
-				.contentType(externalFileName.endsWith(".tmp") ? "image/jpeg" : null)
-				.acl(internalToAWSACL(acl))
-				.build();
+				.contentType(externalFileName.endsWith(".tmp") ? "image/jpeg" : null);
+			if (endpointUrl.contains(".amazonaws.com"))
+				putObjectRequestBuilder = putObjectRequestBuilder.acl(internalToAWSACL(acl));
+			PutObjectRequest putObjectRequest = putObjectRequestBuilder.build();
 
 			PutObjectResponse response = client.putObject(putObjectRequest, RequestBody.fromByteBuffer(byteBuffer));
 			if (!response.sdkHttpResponse().isSuccessful()) {
@@ -336,13 +340,14 @@ public String rename(String objectName, String newName, ResourceAccessControlLis
 	}
 
 	public String copy(String objectName, String newName, ResourceAccessControlList acl) {
-		CopyObjectRequest request = CopyObjectRequest.builder()
+		CopyObjectRequest.Builder requestBuilder = CopyObjectRequest.builder()
 			.sourceBucket(bucket)
 			.sourceKey(objectName)
 			.destinationBucket(bucket)
-			.destinationKey(newName)
-			.acl(internalToAWSACL(acl))
-			.build();
+			.destinationKey(newName);
+		if (endpointUrl.contains(".amazonaws.com"))
+			requestBuilder = requestBuilder.acl(internalToAWSACL(acl));
+		CopyObjectRequest request = requestBuilder.build();
 		client.copyObject(request);
 		return getResourceUrl(newName, acl, defaultExpirationMinutes);
 	}
@@ -368,12 +373,13 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 			.build();
 		ResponseBytes<GetObjectResponse> objectBytes = client.getObjectAsBytes(getObjectRequest);
 
-		PutObjectRequest putObjectRequest = PutObjectRequest.builder()
+		PutObjectRequest.Builder putObjectRequestBuilder = PutObjectRequest.builder()
 			.bucket(bucket)
 			.key(resourceKey)
-			.metadata(metadata)
-			.acl(internalToAWSACL(acl))
-			.build();
+			.metadata(metadata);
+		if (endpointUrl.contains(".amazonaws.com"))
+			putObjectRequestBuilder = putObjectRequestBuilder.acl(internalToAWSACL(acl));
+		PutObjectRequest putObjectRequest = putObjectRequestBuilder.build();
 		client.putObject(putObjectRequest, RequestBody.fromBytes(objectBytes.asByteArray()));
 
 		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);

From 7a54156bfd7a6a487c16cdbcf2dfcb05e0c7d711 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 2 Jan 2024 12:04:46 -0300
Subject: [PATCH 13/23] Keep support for legacy S3 client

---
 gxcloudstorage-awss3/pom.xml                  |   5 +
 .../db/driver/ExternalProviderS3Legacy.java   | 444 ++++++++++++++++++
 2 files changed, 449 insertions(+)
 create mode 100644 gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3/pom.xml
index 62987c89e..a39540c7e 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3/pom.xml
@@ -29,5 +29,10 @@
             <artifactId>s3</artifactId>
             <version>${software.awssdk.version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-s3</artifactId>
+            <version>1.12.587</version>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
new file mode 100644
index 000000000..299751447
--- /dev/null
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
@@ -0,0 +1,444 @@
+package com.genexus.db.driver;
+
+import com.amazonaws.auth.*;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.s3.model.*;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.S3ClientOptions;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import com.amazonaws.HttpMethod;
+import com.genexus.Application;
+import com.genexus.util.GXService;
+import com.genexus.util.StorageUtils;
+import com.genexus.StructSdtMessages_Message;
+import java.io.File;
+import java.io.InputStream;
+import java.io.ByteArrayInputStream;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.util.IOUtils;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+
+public class ExternalProviderS3Legacy extends ExternalProviderBase implements ExternalProvider  {
+	private static Logger logger = LogManager.getLogger(ExternalProviderS3Legacy.class);
+
+	static final String NAME = "AWSS3";
+	static final String ACCESS_KEY = "ACCESS_KEY";
+	static final String SECRET_ACCESS_KEY = "SECRET_KEY";
+	static final String STORAGE_CUSTOM_ENDPOINT = "CUSTOM_ENDPOINT";
+	static final String STORAGE_ENDPOINT = "ENDPOINT";
+	static final String BUCKET = "BUCKET_NAME";
+	static final String REGION = "REGION";
+	static final String USE_IAM = "USE_IAM";
+
+	//Keep it for compatibility reasons
+	@Deprecated
+	static final String ACCESS_KEY_ID_DEPRECATED = "STORAGE_PROVIDER_ACCESSKEYID";
+	@Deprecated
+	static final String SECRET_ACCESS_KEY_DEPRECATED = "STORAGE_PROVIDER_SECRETACCESSKEY";
+	@Deprecated
+	static final String STORAGE_CUSTOM_ENDPOINT_DEPRECATED = "STORAGE_CUSTOM_ENDPOINT";
+	@Deprecated
+	static final String STORAGE_ENDPOINT_DEPRECATED = "STORAGE_ENDPOINT";
+	@Deprecated
+	static final String BUCKET_DEPRECATED = "BUCKET_NAME";
+	@Deprecated
+	static final String FOLDER_DEPRECATED = "FOLDER_NAME";
+	@Deprecated
+	static final String REGION_DEPRECATED = "STORAGE_PROVIDER_REGION";
+
+
+	static final String ACCELERATED = "s3-accelerate.amazonaws.com";
+	static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
+	static final String DEFAULT_REGION = "us-east-1";
+
+	private AmazonS3 client;
+	private String bucket;
+	private String folder;
+	private String endpointUrl = ".s3.amazonaws.com/";
+	private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES;
+
+	private Boolean pathStyleUrls = false;
+
+	public String getName(){
+		return NAME;
+	}
+
+	public ExternalProviderS3Legacy(String service) throws Exception{
+		this(Application.getGXServices().get(service));
+	}
+
+	public ExternalProviderS3Legacy() throws Exception{
+		super();
+		initialize();
+	}
+
+	public ExternalProviderS3Legacy(GXService providerService) throws Exception{
+		super(providerService);
+		initialize();
+	}
+
+	private void initialize() throws Exception{
+		String accessKey = getEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED, "");
+		String secretKey = getEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED, "");
+		String bucket = getEncryptedPropertyValue(BUCKET, BUCKET_DEPRECATED);
+		String folder = getPropertyValue(FOLDER, FOLDER_DEPRECATED, "");
+		String region = getPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION);
+		String endpointValue = getPropertyValue(STORAGE_ENDPOINT, STORAGE_ENDPOINT_DEPRECATED, "");
+		if (endpointValue.equals("custom")) {
+			endpointValue = getPropertyValue(STORAGE_CUSTOM_ENDPOINT, STORAGE_CUSTOM_ENDPOINT_DEPRECATED);
+		}
+
+		try {
+			defaultExpirationMinutes = Integer.parseInt(getPropertyValue(DEFAULT_EXPIRATION, DEFAULT_EXPIRATION_DEPRECATED, Integer.toString(defaultExpirationMinutes)));
+		} catch (Exception e) {
+		}
+
+		if (this.client == null) {
+			if (region.length() == 0) {
+				region = DEFAULT_REGION;
+			}
+
+			this.bucket = bucket;
+			this.folder = folder;
+
+			this.client = buildS3Client(accessKey, secretKey, endpointValue, region);
+			bucketExists();
+		}
+	}
+
+	private AmazonS3 buildS3Client(String accessKey, String secretKey, String endpoint, String region) {
+		AmazonS3 s3Client;
+
+		boolean bUseIAM = !getPropertyValue(USE_IAM, "", "").isEmpty() || (accessKey.equals("") && secretKey.equals(""));
+
+		AmazonS3ClientBuilder builder = bUseIAM ?
+			AmazonS3ClientBuilder.standard():
+			AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
+
+		if (bUseIAM) {
+			logger.debug("Using IAM Credentials");
+		}
+
+		if (endpoint.length() > 0 && !endpoint.contains(".amazonaws.com")) {
+			pathStyleUrls = true;
+
+			s3Client = builder
+				.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
+				.disableChunkedEncoding()
+				.enablePathStyleAccess()
+				.build();
+			endpointUrl = endpoint;
+		}
+		else {
+			pathStyleUrls = false;
+			s3Client = builder
+				.withRegion(region)
+				.build();
+			if (endpoint.equals(ACCELERATED)) {
+				s3Client.setS3ClientOptions(S3ClientOptions.builder().setAccelerateModeEnabled(true).build());
+			}
+			else if (endpoint.equals(DUALSTACK)) {
+				s3Client.setS3ClientOptions(S3ClientOptions.builder().enableDualstack().setAccelerateModeEnabled(true).build());
+			}
+		}
+		return s3Client;
+	}
+
+	private void bucketExists() {
+		if (!client.doesBucketExistV2(bucket)) {
+			logger.debug(String.format("Bucket %s doesn't exist, please create the bucket", bucket));
+		}
+	}
+
+	private String ensureFolder(String... pathPart) {
+		String folderName = buildPath(pathPart);
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.setContentLength(0);
+		InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
+		PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, StorageUtils.normalizeDirectoryName(folderName), emptyContent, metadata);
+		client.putObject(putObjectRequest);
+		return folderName;
+	}
+
+	public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
+		try {
+			S3Object object = client.getObject(new GetObjectRequest(bucket, externalFileName));
+			try (InputStream objectData = object.getObjectContent()) {
+				try (OutputStream outputStream = new FileOutputStream(new File(localFile))){
+					int read;
+					byte[] bytes = new byte[1024];
+					while ((read = objectData.read(bytes)) != -1) {
+						outputStream.write(bytes, 0, read);
+					}
+				}
+			}
+		} catch (FileNotFoundException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		} catch (IOException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		}
+	}
+
+	public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
+		client.putObject(new PutObjectRequest(bucket, externalFileName, new File(localFile)).withCannedAcl(internalToAWSACL(acl)));
+		return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+	}
+
+	private CannedAccessControlList internalToAWSACL(ResourceAccessControlList acl) {
+		if (acl == ResourceAccessControlList.Default) {
+			acl = this.defaultAcl;
+		}
+
+		CannedAccessControlList accessControl = CannedAccessControlList.Private;
+		if (acl == ResourceAccessControlList.Private) {
+			accessControl = CannedAccessControlList.Private;
+		}
+		else if (acl == ResourceAccessControlList.PublicRead) {
+			accessControl = CannedAccessControlList.PublicRead;
+		}
+		else if (acl == ResourceAccessControlList.PublicReadWrite) {
+			accessControl = CannedAccessControlList.PublicReadWrite;
+		}
+		return accessControl;
+	}
+
+	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
+		byte[] bytes;
+		try {
+			bytes = IOUtils.toByteArray(input);
+			ObjectMetadata metadata = new ObjectMetadata();
+			metadata.setContentLength(bytes.length);
+			if (externalFileName.endsWith(".tmp")) {
+				metadata.setContentType("image/jpeg");
+			}
+			String upload = "";
+			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
+				client.putObject(new PutObjectRequest(bucket, externalFileName, byteArrayInputStream, metadata).withCannedAcl(internalToAWSACL(acl)));
+				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+			}
+			return upload;
+		} catch (IOException ex) {
+			logger.error("Error while uploading file to the external provider.", ex);
+			return "";
+		}
+	}
+
+	public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+		client.getObjectMetadata(bucket, externalFileName);
+		return getResourceUrl(externalFileName, acl, expirationMinutes);
+	}
+
+	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+		if (internalToAWSACL(acl) == CannedAccessControlList.Private) {
+			expirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
+			Date expiration = new Date();
+			long msec = expiration.getTime();
+			msec += 60000 * expirationMinutes;
+			expiration.setTime(msec);
+
+			GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, externalFileName);
+			generatePresignedUrlRequest.setMethod(HttpMethod.GET);
+			generatePresignedUrlRequest.setExpiration(expiration);
+			return client.generatePresignedUrl(generatePresignedUrlRequest).toString();
+		} else {
+			return ((AmazonS3Client) client).getResourceUrl(bucket, externalFileName);
+		}
+	}
+
+	public void delete(String objectName, ResourceAccessControlList acl) {
+		client.deleteObject(bucket, objectName);
+	}
+
+	public String rename(String objectName, String newName, ResourceAccessControlList acl) {
+		String url = copy(objectName, newName, acl);
+		delete(objectName, acl);
+		return url;
+	}
+
+	public String copy(String objectName, String newName, ResourceAccessControlList acl) {
+		CopyObjectRequest request = new CopyObjectRequest(bucket, objectName, bucket, newName);
+		request.setCannedAccessControlList(internalToAWSACL(acl));
+		client.copyObject(request);
+		return getResourceUrl(newName, acl, defaultExpirationMinutes);
+	}
+
+	public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) {
+		String resourceFolderName = buildPath(folder, tableName, fieldName);
+		String resourceKey = resourceFolderName + StorageUtils.DELIMITER + newName;
+
+		try {
+			objectUrl = new URI(objectUrl).getPath();
+		}
+		catch (Exception e){
+			logger.error("Failed to Parse Storage Object URI for Copy operation", e);
+		}
+
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.addUserMetadata("Table", tableName);
+		metadata.addUserMetadata("Field", fieldName);
+		metadata.addUserMetadata("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
+
+		CopyObjectRequest request = new CopyObjectRequest(bucket, objectUrl, bucket, resourceKey);
+		request.setNewObjectMetadata(metadata);
+		request.setCannedAccessControlList(internalToAWSACL(acl));
+		client.copyObject(request);
+
+		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
+	}
+
+	private String buildPath(String... pathPart) {
+		ArrayList<String> pathParts = new ArrayList<>();
+		for(String part : pathPart){
+			if (part.length() > 0) {
+				pathParts.add(part);
+			}
+		}
+		return String.join(StorageUtils.DELIMITER, pathParts);
+	}
+	public long getLength(String objectName, ResourceAccessControlList acl) {
+		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
+		return obj.getInstanceLength();
+	}
+
+	public Date getLastModified(String objectName, ResourceAccessControlList acl) {
+		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
+		return obj.getLastModified();
+	}
+
+	public boolean exists(String objectName, ResourceAccessControlList acl) {
+		try {
+			client.getObjectMetadata(bucket, objectName);
+		} catch (AmazonS3Exception ex) {
+			if (ex.getStatusCode() == 404) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	public String getDirectory(String directoryName) {
+		if (existsDirectory(directoryName)) {
+			return bucket + ":" + StorageUtils.DELIMITER + directoryName + StorageUtils.DELIMITER;
+		} else {
+			return "";
+		}
+	}
+
+	public boolean existsDirectory(String directoryName) {
+		ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
+			.withBucketName(bucket)
+			.withDelimiter(StorageUtils.DELIMITER)
+			.withPrefix(StorageUtils.normalizeDirectoryName(directoryName))
+			.withMaxKeys(1);
+		return client.listObjectsV2(listObjectsRequest).getKeyCount() > 0;
+	}
+
+	public void createDirectory(String directoryName) {
+		ensureFolder(directoryName);
+	}
+
+	public void deleteDirectory(String directoryName) {
+		for (S3ObjectSummary file : client.listObjects(bucket, directoryName).getObjectSummaries()) {
+			client.deleteObject(bucket, file.getKey());
+		}
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withDelimiter(StorageUtils.DELIMITER);
+		ObjectListing list = client.listObjects(listObjectsRequest);
+		List<String> toRemove = new ArrayList<String>();
+		List<String> prefixes = list.getCommonPrefixes();
+		for (String prefix : prefixes) {
+			if (prefix.startsWith(directoryName)) {
+				toRemove.add(prefix);
+			}
+		}
+		prefixes.removeAll(toRemove);
+		list.setCommonPrefixes(prefixes);
+	}
+
+	public void renameDirectory(String directoryName, String newDirectoryName) {
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName);
+		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
+			String newKey = file.getKey().replace(directoryName, newDirectoryName);
+			rename(file.getKey(), newKey, null);
+		}
+		deleteDirectory(directoryName);
+	}
+
+	public List<String> getFiles(String directoryName, String filter) {
+		filter = (filter == null || filter.isEmpty())? null: filter.replace("*", "");
+		List<String> files = new ArrayList<String>();
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName).withDelimiter(StorageUtils.DELIMITER);
+		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
+			String key = file.getKey();
+			if (isFile(directoryName, key) && (filter == null || filter.isEmpty() || key.contains(filter))) {
+				files.add(key);
+			}
+		}
+		return files;
+	}
+
+	private boolean isFile(String directory, String name) {
+		return !name.endsWith(StorageUtils.DELIMITER);
+	}
+
+	public List<String> getFiles(String directoryName) {
+		return getFiles(directoryName, null);
+	}
+
+	public List<String> getSubDirectories(String directoryName) {
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName)
+			.withDelimiter(StorageUtils.DELIMITER);
+		ObjectListing objects = client.listObjects(listObjectsRequest);
+		return objects.getCommonPrefixes();
+	}
+
+	public InputStream getStream(String objectName, ResourceAccessControlList acl) {
+		S3Object object = client.getObject(new GetObjectRequest(bucket, objectName));
+		return object.getObjectContent();
+	}
+
+	public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
+		try {
+			AmazonS3Exception aex = (AmazonS3Exception) ex;
+			msg.setId(aex.getErrorCode());
+			return true;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+
+	public String getObjectNameFromURL(String url) {
+		String objectName = null;
+		if (url.startsWith(this.getStorageUri()))
+		{
+			objectName = url.replace(this.getStorageUri(), "");
+		}
+		return objectName;
+	}
+
+	private String getStorageUri()
+	{
+		return (!pathStyleUrls) ? String.format("https://%s%s", this.bucket, this.endpointUrl):
+			String.format("%s/%s/",  this.endpointUrl, this.bucket);
+	}
+}
+//http://192.168.254.78:9000/java-classes-unittests/text.txt

From 1fe2180734eeb412dcf7c0bf6476f80b054b7d8c Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 2 Jan 2024 17:16:35 -0300
Subject: [PATCH 14/23] Revert "Keep support for legacy S3 client"

This reverts commit 7a54156bfd7a6a487c16cdbcf2dfcb05e0c7d711.
---
 gxcloudstorage-awss3/pom.xml                  |   5 -
 .../db/driver/ExternalProviderS3Legacy.java   | 444 ------------------
 2 files changed, 449 deletions(-)
 delete mode 100644 gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3/pom.xml
index a39540c7e..62987c89e 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3/pom.xml
@@ -29,10 +29,5 @@
             <artifactId>s3</artifactId>
             <version>${software.awssdk.version}</version>
         </dependency>
-        <dependency>
-            <groupId>com.amazonaws</groupId>
-            <artifactId>aws-java-sdk-s3</artifactId>
-            <version>1.12.587</version>
-        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
deleted file mode 100644
index 299751447..000000000
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
+++ /dev/null
@@ -1,444 +0,0 @@
-package com.genexus.db.driver;
-
-import com.amazonaws.auth.*;
-import com.amazonaws.client.builder.AwsClientBuilder;
-import com.amazonaws.services.s3.model.*;
-import com.amazonaws.services.s3.AmazonS3ClientBuilder;
-import com.amazonaws.services.s3.S3ClientOptions;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.LogManager;
-import com.amazonaws.HttpMethod;
-import com.genexus.Application;
-import com.genexus.util.GXService;
-import com.genexus.util.StorageUtils;
-import com.genexus.StructSdtMessages_Message;
-import java.io.File;
-import java.io.InputStream;
-import java.io.ByteArrayInputStream;
-
-import com.amazonaws.services.s3.AmazonS3;
-import com.amazonaws.services.s3.AmazonS3Client;
-import com.amazonaws.util.IOUtils;
-
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-
-
-public class ExternalProviderS3Legacy extends ExternalProviderBase implements ExternalProvider  {
-	private static Logger logger = LogManager.getLogger(ExternalProviderS3Legacy.class);
-
-	static final String NAME = "AWSS3";
-	static final String ACCESS_KEY = "ACCESS_KEY";
-	static final String SECRET_ACCESS_KEY = "SECRET_KEY";
-	static final String STORAGE_CUSTOM_ENDPOINT = "CUSTOM_ENDPOINT";
-	static final String STORAGE_ENDPOINT = "ENDPOINT";
-	static final String BUCKET = "BUCKET_NAME";
-	static final String REGION = "REGION";
-	static final String USE_IAM = "USE_IAM";
-
-	//Keep it for compatibility reasons
-	@Deprecated
-	static final String ACCESS_KEY_ID_DEPRECATED = "STORAGE_PROVIDER_ACCESSKEYID";
-	@Deprecated
-	static final String SECRET_ACCESS_KEY_DEPRECATED = "STORAGE_PROVIDER_SECRETACCESSKEY";
-	@Deprecated
-	static final String STORAGE_CUSTOM_ENDPOINT_DEPRECATED = "STORAGE_CUSTOM_ENDPOINT";
-	@Deprecated
-	static final String STORAGE_ENDPOINT_DEPRECATED = "STORAGE_ENDPOINT";
-	@Deprecated
-	static final String BUCKET_DEPRECATED = "BUCKET_NAME";
-	@Deprecated
-	static final String FOLDER_DEPRECATED = "FOLDER_NAME";
-	@Deprecated
-	static final String REGION_DEPRECATED = "STORAGE_PROVIDER_REGION";
-
-
-	static final String ACCELERATED = "s3-accelerate.amazonaws.com";
-	static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
-	static final String DEFAULT_REGION = "us-east-1";
-
-	private AmazonS3 client;
-	private String bucket;
-	private String folder;
-	private String endpointUrl = ".s3.amazonaws.com/";
-	private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES;
-
-	private Boolean pathStyleUrls = false;
-
-	public String getName(){
-		return NAME;
-	}
-
-	public ExternalProviderS3Legacy(String service) throws Exception{
-		this(Application.getGXServices().get(service));
-	}
-
-	public ExternalProviderS3Legacy() throws Exception{
-		super();
-		initialize();
-	}
-
-	public ExternalProviderS3Legacy(GXService providerService) throws Exception{
-		super(providerService);
-		initialize();
-	}
-
-	private void initialize() throws Exception{
-		String accessKey = getEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED, "");
-		String secretKey = getEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED, "");
-		String bucket = getEncryptedPropertyValue(BUCKET, BUCKET_DEPRECATED);
-		String folder = getPropertyValue(FOLDER, FOLDER_DEPRECATED, "");
-		String region = getPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION);
-		String endpointValue = getPropertyValue(STORAGE_ENDPOINT, STORAGE_ENDPOINT_DEPRECATED, "");
-		if (endpointValue.equals("custom")) {
-			endpointValue = getPropertyValue(STORAGE_CUSTOM_ENDPOINT, STORAGE_CUSTOM_ENDPOINT_DEPRECATED);
-		}
-
-		try {
-			defaultExpirationMinutes = Integer.parseInt(getPropertyValue(DEFAULT_EXPIRATION, DEFAULT_EXPIRATION_DEPRECATED, Integer.toString(defaultExpirationMinutes)));
-		} catch (Exception e) {
-		}
-
-		if (this.client == null) {
-			if (region.length() == 0) {
-				region = DEFAULT_REGION;
-			}
-
-			this.bucket = bucket;
-			this.folder = folder;
-
-			this.client = buildS3Client(accessKey, secretKey, endpointValue, region);
-			bucketExists();
-		}
-	}
-
-	private AmazonS3 buildS3Client(String accessKey, String secretKey, String endpoint, String region) {
-		AmazonS3 s3Client;
-
-		boolean bUseIAM = !getPropertyValue(USE_IAM, "", "").isEmpty() || (accessKey.equals("") && secretKey.equals(""));
-
-		AmazonS3ClientBuilder builder = bUseIAM ?
-			AmazonS3ClientBuilder.standard():
-			AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
-
-		if (bUseIAM) {
-			logger.debug("Using IAM Credentials");
-		}
-
-		if (endpoint.length() > 0 && !endpoint.contains(".amazonaws.com")) {
-			pathStyleUrls = true;
-
-			s3Client = builder
-				.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
-				.disableChunkedEncoding()
-				.enablePathStyleAccess()
-				.build();
-			endpointUrl = endpoint;
-		}
-		else {
-			pathStyleUrls = false;
-			s3Client = builder
-				.withRegion(region)
-				.build();
-			if (endpoint.equals(ACCELERATED)) {
-				s3Client.setS3ClientOptions(S3ClientOptions.builder().setAccelerateModeEnabled(true).build());
-			}
-			else if (endpoint.equals(DUALSTACK)) {
-				s3Client.setS3ClientOptions(S3ClientOptions.builder().enableDualstack().setAccelerateModeEnabled(true).build());
-			}
-		}
-		return s3Client;
-	}
-
-	private void bucketExists() {
-		if (!client.doesBucketExistV2(bucket)) {
-			logger.debug(String.format("Bucket %s doesn't exist, please create the bucket", bucket));
-		}
-	}
-
-	private String ensureFolder(String... pathPart) {
-		String folderName = buildPath(pathPart);
-		ObjectMetadata metadata = new ObjectMetadata();
-		metadata.setContentLength(0);
-		InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
-		PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, StorageUtils.normalizeDirectoryName(folderName), emptyContent, metadata);
-		client.putObject(putObjectRequest);
-		return folderName;
-	}
-
-	public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
-		try {
-			S3Object object = client.getObject(new GetObjectRequest(bucket, externalFileName));
-			try (InputStream objectData = object.getObjectContent()) {
-				try (OutputStream outputStream = new FileOutputStream(new File(localFile))){
-					int read;
-					byte[] bytes = new byte[1024];
-					while ((read = objectData.read(bytes)) != -1) {
-						outputStream.write(bytes, 0, read);
-					}
-				}
-			}
-		} catch (FileNotFoundException ex) {
-			logger.error("Error while downloading file to the external provider", ex);
-		} catch (IOException ex) {
-			logger.error("Error while downloading file to the external provider", ex);
-		}
-	}
-
-	public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
-		client.putObject(new PutObjectRequest(bucket, externalFileName, new File(localFile)).withCannedAcl(internalToAWSACL(acl)));
-		return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
-	}
-
-	private CannedAccessControlList internalToAWSACL(ResourceAccessControlList acl) {
-		if (acl == ResourceAccessControlList.Default) {
-			acl = this.defaultAcl;
-		}
-
-		CannedAccessControlList accessControl = CannedAccessControlList.Private;
-		if (acl == ResourceAccessControlList.Private) {
-			accessControl = CannedAccessControlList.Private;
-		}
-		else if (acl == ResourceAccessControlList.PublicRead) {
-			accessControl = CannedAccessControlList.PublicRead;
-		}
-		else if (acl == ResourceAccessControlList.PublicReadWrite) {
-			accessControl = CannedAccessControlList.PublicReadWrite;
-		}
-		return accessControl;
-	}
-
-	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
-		byte[] bytes;
-		try {
-			bytes = IOUtils.toByteArray(input);
-			ObjectMetadata metadata = new ObjectMetadata();
-			metadata.setContentLength(bytes.length);
-			if (externalFileName.endsWith(".tmp")) {
-				metadata.setContentType("image/jpeg");
-			}
-			String upload = "";
-			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
-				client.putObject(new PutObjectRequest(bucket, externalFileName, byteArrayInputStream, metadata).withCannedAcl(internalToAWSACL(acl)));
-				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
-			}
-			return upload;
-		} catch (IOException ex) {
-			logger.error("Error while uploading file to the external provider.", ex);
-			return "";
-		}
-	}
-
-	public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-		client.getObjectMetadata(bucket, externalFileName);
-		return getResourceUrl(externalFileName, acl, expirationMinutes);
-	}
-
-	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
-		if (internalToAWSACL(acl) == CannedAccessControlList.Private) {
-			expirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
-			Date expiration = new Date();
-			long msec = expiration.getTime();
-			msec += 60000 * expirationMinutes;
-			expiration.setTime(msec);
-
-			GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, externalFileName);
-			generatePresignedUrlRequest.setMethod(HttpMethod.GET);
-			generatePresignedUrlRequest.setExpiration(expiration);
-			return client.generatePresignedUrl(generatePresignedUrlRequest).toString();
-		} else {
-			return ((AmazonS3Client) client).getResourceUrl(bucket, externalFileName);
-		}
-	}
-
-	public void delete(String objectName, ResourceAccessControlList acl) {
-		client.deleteObject(bucket, objectName);
-	}
-
-	public String rename(String objectName, String newName, ResourceAccessControlList acl) {
-		String url = copy(objectName, newName, acl);
-		delete(objectName, acl);
-		return url;
-	}
-
-	public String copy(String objectName, String newName, ResourceAccessControlList acl) {
-		CopyObjectRequest request = new CopyObjectRequest(bucket, objectName, bucket, newName);
-		request.setCannedAccessControlList(internalToAWSACL(acl));
-		client.copyObject(request);
-		return getResourceUrl(newName, acl, defaultExpirationMinutes);
-	}
-
-	public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) {
-		String resourceFolderName = buildPath(folder, tableName, fieldName);
-		String resourceKey = resourceFolderName + StorageUtils.DELIMITER + newName;
-
-		try {
-			objectUrl = new URI(objectUrl).getPath();
-		}
-		catch (Exception e){
-			logger.error("Failed to Parse Storage Object URI for Copy operation", e);
-		}
-
-		ObjectMetadata metadata = new ObjectMetadata();
-		metadata.addUserMetadata("Table", tableName);
-		metadata.addUserMetadata("Field", fieldName);
-		metadata.addUserMetadata("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
-
-		CopyObjectRequest request = new CopyObjectRequest(bucket, objectUrl, bucket, resourceKey);
-		request.setNewObjectMetadata(metadata);
-		request.setCannedAccessControlList(internalToAWSACL(acl));
-		client.copyObject(request);
-
-		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
-	}
-
-	private String buildPath(String... pathPart) {
-		ArrayList<String> pathParts = new ArrayList<>();
-		for(String part : pathPart){
-			if (part.length() > 0) {
-				pathParts.add(part);
-			}
-		}
-		return String.join(StorageUtils.DELIMITER, pathParts);
-	}
-	public long getLength(String objectName, ResourceAccessControlList acl) {
-		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
-		return obj.getInstanceLength();
-	}
-
-	public Date getLastModified(String objectName, ResourceAccessControlList acl) {
-		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
-		return obj.getLastModified();
-	}
-
-	public boolean exists(String objectName, ResourceAccessControlList acl) {
-		try {
-			client.getObjectMetadata(bucket, objectName);
-		} catch (AmazonS3Exception ex) {
-			if (ex.getStatusCode() == 404) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	public String getDirectory(String directoryName) {
-		if (existsDirectory(directoryName)) {
-			return bucket + ":" + StorageUtils.DELIMITER + directoryName + StorageUtils.DELIMITER;
-		} else {
-			return "";
-		}
-	}
-
-	public boolean existsDirectory(String directoryName) {
-		ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
-			.withBucketName(bucket)
-			.withDelimiter(StorageUtils.DELIMITER)
-			.withPrefix(StorageUtils.normalizeDirectoryName(directoryName))
-			.withMaxKeys(1);
-		return client.listObjectsV2(listObjectsRequest).getKeyCount() > 0;
-	}
-
-	public void createDirectory(String directoryName) {
-		ensureFolder(directoryName);
-	}
-
-	public void deleteDirectory(String directoryName) {
-		for (S3ObjectSummary file : client.listObjects(bucket, directoryName).getObjectSummaries()) {
-			client.deleteObject(bucket, file.getKey());
-		}
-		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-			.withBucketName(bucket).withDelimiter(StorageUtils.DELIMITER);
-		ObjectListing list = client.listObjects(listObjectsRequest);
-		List<String> toRemove = new ArrayList<String>();
-		List<String> prefixes = list.getCommonPrefixes();
-		for (String prefix : prefixes) {
-			if (prefix.startsWith(directoryName)) {
-				toRemove.add(prefix);
-			}
-		}
-		prefixes.removeAll(toRemove);
-		list.setCommonPrefixes(prefixes);
-	}
-
-	public void renameDirectory(String directoryName, String newDirectoryName) {
-		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-		newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
-		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-			.withBucketName(bucket).withPrefix(directoryName);
-		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
-			String newKey = file.getKey().replace(directoryName, newDirectoryName);
-			rename(file.getKey(), newKey, null);
-		}
-		deleteDirectory(directoryName);
-	}
-
-	public List<String> getFiles(String directoryName, String filter) {
-		filter = (filter == null || filter.isEmpty())? null: filter.replace("*", "");
-		List<String> files = new ArrayList<String>();
-		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-			.withBucketName(bucket).withPrefix(directoryName).withDelimiter(StorageUtils.DELIMITER);
-		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
-			String key = file.getKey();
-			if (isFile(directoryName, key) && (filter == null || filter.isEmpty() || key.contains(filter))) {
-				files.add(key);
-			}
-		}
-		return files;
-	}
-
-	private boolean isFile(String directory, String name) {
-		return !name.endsWith(StorageUtils.DELIMITER);
-	}
-
-	public List<String> getFiles(String directoryName) {
-		return getFiles(directoryName, null);
-	}
-
-	public List<String> getSubDirectories(String directoryName) {
-		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
-		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
-			.withBucketName(bucket).withPrefix(directoryName)
-			.withDelimiter(StorageUtils.DELIMITER);
-		ObjectListing objects = client.listObjects(listObjectsRequest);
-		return objects.getCommonPrefixes();
-	}
-
-	public InputStream getStream(String objectName, ResourceAccessControlList acl) {
-		S3Object object = client.getObject(new GetObjectRequest(bucket, objectName));
-		return object.getObjectContent();
-	}
-
-	public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
-		try {
-			AmazonS3Exception aex = (AmazonS3Exception) ex;
-			msg.setId(aex.getErrorCode());
-			return true;
-		} catch (Exception e) {
-			return false;
-		}
-	}
-
-	public String getObjectNameFromURL(String url) {
-		String objectName = null;
-		if (url.startsWith(this.getStorageUri()))
-		{
-			objectName = url.replace(this.getStorageUri(), "");
-		}
-		return objectName;
-	}
-
-	private String getStorageUri()
-	{
-		return (!pathStyleUrls) ? String.format("https://%s%s", this.bucket, this.endpointUrl):
-			String.format("%s/%s/",  this.endpointUrl, this.bucket);
-	}
-}
-//http://192.168.254.78:9000/java-classes-unittests/text.txt

From 600ed1f6655aa9b2f59c699122937ae6074b06a6 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Wed, 3 Jan 2024 09:49:22 -0300
Subject: [PATCH 15/23] Revert "Revert "Keep support for legacy S3 client""

This reverts commit 1fe2180734eeb412dcf7c0bf6476f80b054b7d8c.
---
 gxcloudstorage-awss3/pom.xml                  |   5 +
 .../db/driver/ExternalProviderS3Legacy.java   | 444 ++++++++++++++++++
 2 files changed, 449 insertions(+)
 create mode 100644 gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3/pom.xml
index 62987c89e..a39540c7e 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3/pom.xml
@@ -29,5 +29,10 @@
             <artifactId>s3</artifactId>
             <version>${software.awssdk.version}</version>
         </dependency>
+        <dependency>
+            <groupId>com.amazonaws</groupId>
+            <artifactId>aws-java-sdk-s3</artifactId>
+            <version>1.12.587</version>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
new file mode 100644
index 000000000..299751447
--- /dev/null
+++ b/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
@@ -0,0 +1,444 @@
+package com.genexus.db.driver;
+
+import com.amazonaws.auth.*;
+import com.amazonaws.client.builder.AwsClientBuilder;
+import com.amazonaws.services.s3.model.*;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.S3ClientOptions;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import com.amazonaws.HttpMethod;
+import com.genexus.Application;
+import com.genexus.util.GXService;
+import com.genexus.util.StorageUtils;
+import com.genexus.StructSdtMessages_Message;
+import java.io.File;
+import java.io.InputStream;
+import java.io.ByteArrayInputStream;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3Client;
+import com.amazonaws.util.IOUtils;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+
+public class ExternalProviderS3Legacy extends ExternalProviderBase implements ExternalProvider  {
+	private static Logger logger = LogManager.getLogger(ExternalProviderS3Legacy.class);
+
+	static final String NAME = "AWSS3";
+	static final String ACCESS_KEY = "ACCESS_KEY";
+	static final String SECRET_ACCESS_KEY = "SECRET_KEY";
+	static final String STORAGE_CUSTOM_ENDPOINT = "CUSTOM_ENDPOINT";
+	static final String STORAGE_ENDPOINT = "ENDPOINT";
+	static final String BUCKET = "BUCKET_NAME";
+	static final String REGION = "REGION";
+	static final String USE_IAM = "USE_IAM";
+
+	//Keep it for compatibility reasons
+	@Deprecated
+	static final String ACCESS_KEY_ID_DEPRECATED = "STORAGE_PROVIDER_ACCESSKEYID";
+	@Deprecated
+	static final String SECRET_ACCESS_KEY_DEPRECATED = "STORAGE_PROVIDER_SECRETACCESSKEY";
+	@Deprecated
+	static final String STORAGE_CUSTOM_ENDPOINT_DEPRECATED = "STORAGE_CUSTOM_ENDPOINT";
+	@Deprecated
+	static final String STORAGE_ENDPOINT_DEPRECATED = "STORAGE_ENDPOINT";
+	@Deprecated
+	static final String BUCKET_DEPRECATED = "BUCKET_NAME";
+	@Deprecated
+	static final String FOLDER_DEPRECATED = "FOLDER_NAME";
+	@Deprecated
+	static final String REGION_DEPRECATED = "STORAGE_PROVIDER_REGION";
+
+
+	static final String ACCELERATED = "s3-accelerate.amazonaws.com";
+	static final String DUALSTACK = "s3-accelerate.dualstack.amazonaws.com";
+	static final String DEFAULT_REGION = "us-east-1";
+
+	private AmazonS3 client;
+	private String bucket;
+	private String folder;
+	private String endpointUrl = ".s3.amazonaws.com/";
+	private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES;
+
+	private Boolean pathStyleUrls = false;
+
+	public String getName(){
+		return NAME;
+	}
+
+	public ExternalProviderS3Legacy(String service) throws Exception{
+		this(Application.getGXServices().get(service));
+	}
+
+	public ExternalProviderS3Legacy() throws Exception{
+		super();
+		initialize();
+	}
+
+	public ExternalProviderS3Legacy(GXService providerService) throws Exception{
+		super(providerService);
+		initialize();
+	}
+
+	private void initialize() throws Exception{
+		String accessKey = getEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED, "");
+		String secretKey = getEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED, "");
+		String bucket = getEncryptedPropertyValue(BUCKET, BUCKET_DEPRECATED);
+		String folder = getPropertyValue(FOLDER, FOLDER_DEPRECATED, "");
+		String region = getPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION);
+		String endpointValue = getPropertyValue(STORAGE_ENDPOINT, STORAGE_ENDPOINT_DEPRECATED, "");
+		if (endpointValue.equals("custom")) {
+			endpointValue = getPropertyValue(STORAGE_CUSTOM_ENDPOINT, STORAGE_CUSTOM_ENDPOINT_DEPRECATED);
+		}
+
+		try {
+			defaultExpirationMinutes = Integer.parseInt(getPropertyValue(DEFAULT_EXPIRATION, DEFAULT_EXPIRATION_DEPRECATED, Integer.toString(defaultExpirationMinutes)));
+		} catch (Exception e) {
+		}
+
+		if (this.client == null) {
+			if (region.length() == 0) {
+				region = DEFAULT_REGION;
+			}
+
+			this.bucket = bucket;
+			this.folder = folder;
+
+			this.client = buildS3Client(accessKey, secretKey, endpointValue, region);
+			bucketExists();
+		}
+	}
+
+	private AmazonS3 buildS3Client(String accessKey, String secretKey, String endpoint, String region) {
+		AmazonS3 s3Client;
+
+		boolean bUseIAM = !getPropertyValue(USE_IAM, "", "").isEmpty() || (accessKey.equals("") && secretKey.equals(""));
+
+		AmazonS3ClientBuilder builder = bUseIAM ?
+			AmazonS3ClientBuilder.standard():
+			AmazonS3ClientBuilder.standard().withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)));
+
+		if (bUseIAM) {
+			logger.debug("Using IAM Credentials");
+		}
+
+		if (endpoint.length() > 0 && !endpoint.contains(".amazonaws.com")) {
+			pathStyleUrls = true;
+
+			s3Client = builder
+				.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
+				.disableChunkedEncoding()
+				.enablePathStyleAccess()
+				.build();
+			endpointUrl = endpoint;
+		}
+		else {
+			pathStyleUrls = false;
+			s3Client = builder
+				.withRegion(region)
+				.build();
+			if (endpoint.equals(ACCELERATED)) {
+				s3Client.setS3ClientOptions(S3ClientOptions.builder().setAccelerateModeEnabled(true).build());
+			}
+			else if (endpoint.equals(DUALSTACK)) {
+				s3Client.setS3ClientOptions(S3ClientOptions.builder().enableDualstack().setAccelerateModeEnabled(true).build());
+			}
+		}
+		return s3Client;
+	}
+
+	private void bucketExists() {
+		if (!client.doesBucketExistV2(bucket)) {
+			logger.debug(String.format("Bucket %s doesn't exist, please create the bucket", bucket));
+		}
+	}
+
+	private String ensureFolder(String... pathPart) {
+		String folderName = buildPath(pathPart);
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.setContentLength(0);
+		InputStream emptyContent = new ByteArrayInputStream(new byte[0]);
+		PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, StorageUtils.normalizeDirectoryName(folderName), emptyContent, metadata);
+		client.putObject(putObjectRequest);
+		return folderName;
+	}
+
+	public void download(String externalFileName, String localFile, ResourceAccessControlList acl) {
+		try {
+			S3Object object = client.getObject(new GetObjectRequest(bucket, externalFileName));
+			try (InputStream objectData = object.getObjectContent()) {
+				try (OutputStream outputStream = new FileOutputStream(new File(localFile))){
+					int read;
+					byte[] bytes = new byte[1024];
+					while ((read = objectData.read(bytes)) != -1) {
+						outputStream.write(bytes, 0, read);
+					}
+				}
+			}
+		} catch (FileNotFoundException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		} catch (IOException ex) {
+			logger.error("Error while downloading file to the external provider", ex);
+		}
+	}
+
+	public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) {
+		client.putObject(new PutObjectRequest(bucket, externalFileName, new File(localFile)).withCannedAcl(internalToAWSACL(acl)));
+		return getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+	}
+
+	private CannedAccessControlList internalToAWSACL(ResourceAccessControlList acl) {
+		if (acl == ResourceAccessControlList.Default) {
+			acl = this.defaultAcl;
+		}
+
+		CannedAccessControlList accessControl = CannedAccessControlList.Private;
+		if (acl == ResourceAccessControlList.Private) {
+			accessControl = CannedAccessControlList.Private;
+		}
+		else if (acl == ResourceAccessControlList.PublicRead) {
+			accessControl = CannedAccessControlList.PublicRead;
+		}
+		else if (acl == ResourceAccessControlList.PublicReadWrite) {
+			accessControl = CannedAccessControlList.PublicReadWrite;
+		}
+		return accessControl;
+	}
+
+	public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) {
+		byte[] bytes;
+		try {
+			bytes = IOUtils.toByteArray(input);
+			ObjectMetadata metadata = new ObjectMetadata();
+			metadata.setContentLength(bytes.length);
+			if (externalFileName.endsWith(".tmp")) {
+				metadata.setContentType("image/jpeg");
+			}
+			String upload = "";
+			try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes)) {
+				client.putObject(new PutObjectRequest(bucket, externalFileName, byteArrayInputStream, metadata).withCannedAcl(internalToAWSACL(acl)));
+				upload = getResourceUrl(externalFileName, acl, defaultExpirationMinutes);
+			}
+			return upload;
+		} catch (IOException ex) {
+			logger.error("Error while uploading file to the external provider.", ex);
+			return "";
+		}
+	}
+
+	public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+		client.getObjectMetadata(bucket, externalFileName);
+		return getResourceUrl(externalFileName, acl, expirationMinutes);
+	}
+
+	private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) {
+		if (internalToAWSACL(acl) == CannedAccessControlList.Private) {
+			expirationMinutes = expirationMinutes > 0 ? expirationMinutes: defaultExpirationMinutes;
+			Date expiration = new Date();
+			long msec = expiration.getTime();
+			msec += 60000 * expirationMinutes;
+			expiration.setTime(msec);
+
+			GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, externalFileName);
+			generatePresignedUrlRequest.setMethod(HttpMethod.GET);
+			generatePresignedUrlRequest.setExpiration(expiration);
+			return client.generatePresignedUrl(generatePresignedUrlRequest).toString();
+		} else {
+			return ((AmazonS3Client) client).getResourceUrl(bucket, externalFileName);
+		}
+	}
+
+	public void delete(String objectName, ResourceAccessControlList acl) {
+		client.deleteObject(bucket, objectName);
+	}
+
+	public String rename(String objectName, String newName, ResourceAccessControlList acl) {
+		String url = copy(objectName, newName, acl);
+		delete(objectName, acl);
+		return url;
+	}
+
+	public String copy(String objectName, String newName, ResourceAccessControlList acl) {
+		CopyObjectRequest request = new CopyObjectRequest(bucket, objectName, bucket, newName);
+		request.setCannedAccessControlList(internalToAWSACL(acl));
+		client.copyObject(request);
+		return getResourceUrl(newName, acl, defaultExpirationMinutes);
+	}
+
+	public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) {
+		String resourceFolderName = buildPath(folder, tableName, fieldName);
+		String resourceKey = resourceFolderName + StorageUtils.DELIMITER + newName;
+
+		try {
+			objectUrl = new URI(objectUrl).getPath();
+		}
+		catch (Exception e){
+			logger.error("Failed to Parse Storage Object URI for Copy operation", e);
+		}
+
+		ObjectMetadata metadata = new ObjectMetadata();
+		metadata.addUserMetadata("Table", tableName);
+		metadata.addUserMetadata("Field", fieldName);
+		metadata.addUserMetadata("KeyValue", StorageUtils.encodeNonAsciiCharacters(resourceKey));
+
+		CopyObjectRequest request = new CopyObjectRequest(bucket, objectUrl, bucket, resourceKey);
+		request.setNewObjectMetadata(metadata);
+		request.setCannedAccessControlList(internalToAWSACL(acl));
+		client.copyObject(request);
+
+		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
+	}
+
+	private String buildPath(String... pathPart) {
+		ArrayList<String> pathParts = new ArrayList<>();
+		for(String part : pathPart){
+			if (part.length() > 0) {
+				pathParts.add(part);
+			}
+		}
+		return String.join(StorageUtils.DELIMITER, pathParts);
+	}
+	public long getLength(String objectName, ResourceAccessControlList acl) {
+		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
+		return obj.getInstanceLength();
+	}
+
+	public Date getLastModified(String objectName, ResourceAccessControlList acl) {
+		ObjectMetadata obj = client.getObjectMetadata(bucket, objectName);
+		return obj.getLastModified();
+	}
+
+	public boolean exists(String objectName, ResourceAccessControlList acl) {
+		try {
+			client.getObjectMetadata(bucket, objectName);
+		} catch (AmazonS3Exception ex) {
+			if (ex.getStatusCode() == 404) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	public String getDirectory(String directoryName) {
+		if (existsDirectory(directoryName)) {
+			return bucket + ":" + StorageUtils.DELIMITER + directoryName + StorageUtils.DELIMITER;
+		} else {
+			return "";
+		}
+	}
+
+	public boolean existsDirectory(String directoryName) {
+		ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
+			.withBucketName(bucket)
+			.withDelimiter(StorageUtils.DELIMITER)
+			.withPrefix(StorageUtils.normalizeDirectoryName(directoryName))
+			.withMaxKeys(1);
+		return client.listObjectsV2(listObjectsRequest).getKeyCount() > 0;
+	}
+
+	public void createDirectory(String directoryName) {
+		ensureFolder(directoryName);
+	}
+
+	public void deleteDirectory(String directoryName) {
+		for (S3ObjectSummary file : client.listObjects(bucket, directoryName).getObjectSummaries()) {
+			client.deleteObject(bucket, file.getKey());
+		}
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withDelimiter(StorageUtils.DELIMITER);
+		ObjectListing list = client.listObjects(listObjectsRequest);
+		List<String> toRemove = new ArrayList<String>();
+		List<String> prefixes = list.getCommonPrefixes();
+		for (String prefix : prefixes) {
+			if (prefix.startsWith(directoryName)) {
+				toRemove.add(prefix);
+			}
+		}
+		prefixes.removeAll(toRemove);
+		list.setCommonPrefixes(prefixes);
+	}
+
+	public void renameDirectory(String directoryName, String newDirectoryName) {
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName);
+		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
+			String newKey = file.getKey().replace(directoryName, newDirectoryName);
+			rename(file.getKey(), newKey, null);
+		}
+		deleteDirectory(directoryName);
+	}
+
+	public List<String> getFiles(String directoryName, String filter) {
+		filter = (filter == null || filter.isEmpty())? null: filter.replace("*", "");
+		List<String> files = new ArrayList<String>();
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName).withDelimiter(StorageUtils.DELIMITER);
+		for (S3ObjectSummary file : client.listObjects(listObjectsRequest).getObjectSummaries()) {
+			String key = file.getKey();
+			if (isFile(directoryName, key) && (filter == null || filter.isEmpty() || key.contains(filter))) {
+				files.add(key);
+			}
+		}
+		return files;
+	}
+
+	private boolean isFile(String directory, String name) {
+		return !name.endsWith(StorageUtils.DELIMITER);
+	}
+
+	public List<String> getFiles(String directoryName) {
+		return getFiles(directoryName, null);
+	}
+
+	public List<String> getSubDirectories(String directoryName) {
+		directoryName = StorageUtils.normalizeDirectoryName(directoryName);
+		ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
+			.withBucketName(bucket).withPrefix(directoryName)
+			.withDelimiter(StorageUtils.DELIMITER);
+		ObjectListing objects = client.listObjects(listObjectsRequest);
+		return objects.getCommonPrefixes();
+	}
+
+	public InputStream getStream(String objectName, ResourceAccessControlList acl) {
+		S3Object object = client.getObject(new GetObjectRequest(bucket, objectName));
+		return object.getObjectContent();
+	}
+
+	public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) {
+		try {
+			AmazonS3Exception aex = (AmazonS3Exception) ex;
+			msg.setId(aex.getErrorCode());
+			return true;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+
+	public String getObjectNameFromURL(String url) {
+		String objectName = null;
+		if (url.startsWith(this.getStorageUri()))
+		{
+			objectName = url.replace(this.getStorageUri(), "");
+		}
+		return objectName;
+	}
+
+	private String getStorageUri()
+	{
+		return (!pathStyleUrls) ? String.format("https://%s%s", this.bucket, this.endpointUrl):
+			String.format("%s/%s/",  this.endpointUrl, this.bucket);
+	}
+}
+//http://192.168.254.78:9000/java-classes-unittests/text.txt

From 46d660d2e81d1a7fb6623a0a7efd94a447a3dfb2 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 8 Jan 2024 20:19:11 -0300
Subject: [PATCH 16/23] Keep support for old S3 client

---
 .../pom.xml                                   |  9 ++---
 .../db/driver/ExternalProviderS3V1.java       | 10 +++---
 gxcloudstorage-awss3-v2/pom.xml               | 33 +++++++++++++++++++
 .../db/driver/ExternalProviderS3V2.java       | 10 +++---
 gxcloudstorage-tests/pom.xml                  | 14 +++++---
 .../db/driver/TestExternalProviderMinio.java  |  4 +--
 .../db/driver/TestExternalProviderOracle.java |  4 +--
 ...rS3.java => TestExternalProviderS3V1.java} |  8 ++---
 .../db/driver/TestExternalProviderS3V2.java   | 14 ++++++++
 pom.xml                                       |  5 +--
 10 files changed, 79 insertions(+), 32 deletions(-)
 rename {gxcloudstorage-awss3 => gxcloudstorage-awss3-v1}/pom.xml (77%)
 rename gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java => gxcloudstorage-awss3-v1/src/main/java/com/genexus/db/driver/ExternalProviderS3V1.java (98%)
 create mode 100644 gxcloudstorage-awss3-v2/pom.xml
 rename gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java => gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java (98%)
 rename gxcloudstorage-tests/src/test/java/com/genexus/db/driver/{TestExternalProviderS3.java => TestExternalProviderS3V1.java} (53%)
 create mode 100644 gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V2.java

diff --git a/gxcloudstorage-awss3/pom.xml b/gxcloudstorage-awss3-v1/pom.xml
similarity index 77%
rename from gxcloudstorage-awss3/pom.xml
rename to gxcloudstorage-awss3-v1/pom.xml
index a39540c7e..e5f2ab5ae 100644
--- a/gxcloudstorage-awss3/pom.xml
+++ b/gxcloudstorage-awss3-v1/pom.xml
@@ -10,8 +10,8 @@
 		<version>${revision}${changelist}</version>
 	</parent>
 
-    <artifactId>gxcloudstorage-awss3</artifactId>
-    <name>GeneXus AWS S3 Cloud Storage</name>
+    <artifactId>gxcloudstorage-awss3-v1</artifactId>
+    <name>GeneXus AWS S3 (V1) Cloud Storage</name>
 
 	<dependencies>
 		<dependency>
@@ -24,11 +24,6 @@
             <artifactId>gxcloudstorage-common</artifactId>
             <version>${project.version}</version>
         </dependency>
-        <dependency>
-            <groupId>software.amazon.awssdk</groupId>
-            <artifactId>s3</artifactId>
-            <version>${software.awssdk.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.amazonaws</groupId>
             <artifactId>aws-java-sdk-s3</artifactId>
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java b/gxcloudstorage-awss3-v1/src/main/java/com/genexus/db/driver/ExternalProviderS3V1.java
similarity index 98%
rename from gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
rename to gxcloudstorage-awss3-v1/src/main/java/com/genexus/db/driver/ExternalProviderS3V1.java
index 299751447..6666a8888 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3Legacy.java
+++ b/gxcloudstorage-awss3-v1/src/main/java/com/genexus/db/driver/ExternalProviderS3V1.java
@@ -30,8 +30,8 @@
 import java.util.List;
 
 
-public class ExternalProviderS3Legacy extends ExternalProviderBase implements ExternalProvider  {
-	private static Logger logger = LogManager.getLogger(ExternalProviderS3Legacy.class);
+public class ExternalProviderS3V1 extends ExternalProviderBase implements ExternalProvider  {
+	private static Logger logger = LogManager.getLogger(ExternalProviderS3V1.class);
 
 	static final String NAME = "AWSS3";
 	static final String ACCESS_KEY = "ACCESS_KEY";
@@ -75,16 +75,16 @@ public String getName(){
 		return NAME;
 	}
 
-	public ExternalProviderS3Legacy(String service) throws Exception{
+	public ExternalProviderS3V1(String service) throws Exception{
 		this(Application.getGXServices().get(service));
 	}
 
-	public ExternalProviderS3Legacy() throws Exception{
+	public ExternalProviderS3V1() throws Exception{
 		super();
 		initialize();
 	}
 
-	public ExternalProviderS3Legacy(GXService providerService) throws Exception{
+	public ExternalProviderS3V1(GXService providerService) throws Exception{
 		super(providerService);
 		initialize();
 	}
diff --git a/gxcloudstorage-awss3-v2/pom.xml b/gxcloudstorage-awss3-v2/pom.xml
new file mode 100644
index 000000000..c90d80c16
--- /dev/null
+++ b/gxcloudstorage-awss3-v2/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>com.genexus</groupId>
+        <artifactId>parent</artifactId>
+        <version>${revision}${changelist}</version>
+    </parent>
+
+    <artifactId>gxcloudstorage-awss3-v2</artifactId>
+    <name>GeneXus AWS S3 (V2) Cloud Storage</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gxclassR</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.genexus</groupId>
+            <artifactId>gxcloudstorage-common</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>software.amazon.awssdk</groupId>
+            <artifactId>s3</artifactId>
+            <version>${software.awssdk.version}</version>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
similarity index 98%
rename from gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
rename to gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
index 6f68e00b0..9025f54a2 100644
--- a/gxcloudstorage-awss3/src/main/java/com/genexus/db/driver/ExternalProviderS3.java
+++ b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
@@ -36,8 +36,8 @@
 import java.time.Instant;
 import java.util.*;
 
-public class ExternalProviderS3 extends ExternalProviderBase implements ExternalProvider {
-	private static Logger logger = LogManager.getLogger(ExternalProviderS3.class);
+public class ExternalProviderS3V2 extends ExternalProviderBase implements ExternalProvider {
+	private static Logger logger = LogManager.getLogger(ExternalProviderS3V2.class);
 
 	static final String NAME = "AWSS3";
 	static final String ACCESS_KEY = "ACCESS_KEY";
@@ -80,16 +80,16 @@ public String getName() {
 		return NAME;
 	}
 
-	public ExternalProviderS3(String service) throws Exception {
+	public ExternalProviderS3V2(String service) throws Exception {
 		this(Application.getGXServices().get(service));
 	}
 
-	public ExternalProviderS3() throws Exception {
+	public ExternalProviderS3V2() throws Exception {
 		super();
 		initialize();
 	}
 
-	public ExternalProviderS3(GXService providerService) throws Exception {
+	public ExternalProviderS3V2(GXService providerService) throws Exception {
 		super(providerService);
 		initialize();
 	}
diff --git a/gxcloudstorage-tests/pom.xml b/gxcloudstorage-tests/pom.xml
index 62d396c93..ce7d00c0a 100644
--- a/gxcloudstorage-tests/pom.xml
+++ b/gxcloudstorage-tests/pom.xml
@@ -26,25 +26,31 @@
         </dependency>
         <dependency>
             <groupId>com.genexus</groupId>
-            <artifactId>gxcloudstorage-awss3</artifactId>
+            <artifactId>gxcloudstorage-ibmcos</artifactId>
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.genexus</groupId>
-            <artifactId>gxcloudstorage-ibmcos</artifactId>
+            <artifactId>gxcloudstorage-googlecloudstorage</artifactId>
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.genexus</groupId>
-            <artifactId>gxcloudstorage-googlecloudstorage</artifactId>
+            <artifactId>gxcloudstorage-azureblob</artifactId>
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>com.genexus</groupId>
-            <artifactId>gxcloudstorage-azureblob</artifactId>
+            <artifactId>gxcloudstorage-awss3-v1</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.genexus</groupId>
+            <artifactId>gxcloudstorage-awss3-v2</artifactId>
             <version>${project.version}</version>
             <scope>test</scope>
         </dependency>
diff --git a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderMinio.java b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderMinio.java
index 6eab8b9fa..76e3a5f0b 100644
--- a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderMinio.java
+++ b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderMinio.java
@@ -13,12 +13,12 @@ public class TestExternalProviderMinio extends TestExternalProvider {
 
 	@Override
 	public String getProviderName(){
-		return ExternalProviderS3.NAME;
+		return ExternalProviderS3V1.NAME;
 	}
 
 	@Override
 	public ExternalProvider getExternalProvider() throws Exception {
-		return new ExternalProviderS3();
+		return new ExternalProviderS3V1();
 	}
 
 
diff --git a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderOracle.java b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderOracle.java
index f04c9b110..5c70f2dc4 100644
--- a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderOracle.java
+++ b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderOracle.java
@@ -5,12 +5,12 @@ public class TestExternalProviderOracle extends TestExternalProvider {
 
 	@Override
 	public String getProviderName(){
-		return ExternalProviderS3.NAME;
+		return ExternalProviderS3V1.NAME;
 	}
 
 	@Override
 	public ExternalProvider getExternalProvider() throws Exception {
-		return new ExternalProviderS3();
+		return new ExternalProviderS3V1();
 	}
 
 }
diff --git a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3.java b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V1.java
similarity index 53%
rename from gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3.java
rename to gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V1.java
index 1aa99d28f..9d1f81ed8 100644
--- a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3.java
+++ b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V1.java
@@ -1,16 +1,14 @@
 package com.genexus.db.driver;
 
-
-public class TestExternalProviderS3 extends TestExternalProvider {
-
+public class TestExternalProviderS3V1 extends TestExternalProvider {
 	@Override
 	public String getProviderName(){
-		return ExternalProviderS3.NAME;
+		return ExternalProviderS3V1.NAME;
 	}
 
 	@Override
 	public ExternalProvider getExternalProvider() throws Exception {
-		return new ExternalProviderS3();
+		return new ExternalProviderS3V1();
 	}
 
 }
diff --git a/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V2.java b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V2.java
new file mode 100644
index 000000000..e6619023c
--- /dev/null
+++ b/gxcloudstorage-tests/src/test/java/com/genexus/db/driver/TestExternalProviderS3V2.java
@@ -0,0 +1,14 @@
+package com.genexus.db.driver;
+public class TestExternalProviderS3V2 extends TestExternalProvider {
+
+	@Override
+	public String getProviderName(){
+		return ExternalProviderS3V2.NAME;
+	}
+
+	@Override
+	public ExternalProvider getExternalProvider() throws Exception {
+		return new ExternalProviderS3V2();
+	}
+
+}
diff --git a/pom.xml b/pom.xml
index abcf62685..b7aeca4ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,12 +107,13 @@
 		<module>gxcache-redis</module>
 		<module>gxcache-memcached</module>
 		<module>gxcloudstorage-common</module>
-		<module>gxcloudstorage-awss3</module>
+		<module>gxcloudstorage-awss3-v1</module>
 		<module>gxcloudstorage-googlecloudstorage</module>
 		<module>gxcloudstorage-azureblob</module>
 		<module>gxcloudstorage-ibmcos</module>
 		<module>gxcloudstorage-tests</module>
-	</modules>
+        <module>gxcloudstorage-awss3-v2</module>
+    </modules>
 
 	<dependencies>
 		<dependency>

From cdee1b173d63a24d7ced5811de14a19a207e1886 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Wed, 17 Jan 2024 12:03:38 -0300
Subject: [PATCH 17/23] Fix SLF4J failed to load class error

---
 gxcloudstorage-awss3-v2/pom.xml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/gxcloudstorage-awss3-v2/pom.xml b/gxcloudstorage-awss3-v2/pom.xml
index c90d80c16..283598786 100644
--- a/gxcloudstorage-awss3-v2/pom.xml
+++ b/gxcloudstorage-awss3-v2/pom.xml
@@ -29,5 +29,10 @@
             <artifactId>s3</artifactId>
             <version>${software.awssdk.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-1.2-api</artifactId>
+            <version>${log4j.version}</version>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file

From d04887c08ddceff891ade3a3aff1c69f822a06e4 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 22 Jan 2024 12:17:20 -0300
Subject: [PATCH 18/23] URL-encode external file name when getting resource url

---
 .../db/driver/ExternalProviderS3V2.java       | 23 ++++++++++++++-----
 1 file changed, 17 insertions(+), 6 deletions(-)

diff --git a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
index 9025f54a2..61c2b6f29 100644
--- a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
+++ b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
@@ -24,13 +24,11 @@
 import com.genexus.util.StorageUtils;
 import com.genexus.StructSdtMessages_Message;
 
-import java.io.InputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
+import java.io.*;
 import java.net.URI;
+import java.net.URLEncoder;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.time.Duration;
 import java.time.Instant;
@@ -321,7 +319,20 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList
 
 			return presignedGetObjectRequest.url().toString();
 		} else {
-			return String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, clientRegion, externalFileName);
+			try {
+				return String.format(
+					"https://%s.s3.%s.amazonaws.com/%s",
+					bucket,
+					clientRegion,
+					URLEncoder.encode(
+						externalFileName,
+						StandardCharsets.UTF_8.toString()
+					)
+				);
+			} catch (UnsupportedEncodingException uee) {
+				logger.error("Failed to encode resource URL for " + externalFileName, uee);
+				return "";
+			}
 		}
 	}
 

From f2ae814ff3b5fabb78035912baa06821ff11f0c4 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 22 Jan 2024 15:13:21 -0300
Subject: [PATCH 19/23] Try to determine content type before sending resource
 to bucket

---
 .../db/driver/ExternalProviderS3V2.java       | 63 ++++++++++++++++++-
 1 file changed, 62 insertions(+), 1 deletion(-)

diff --git a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
index 61c2b6f29..fd413f25e 100644
--- a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
+++ b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
@@ -29,6 +29,8 @@
 import java.net.URLEncoder;
 import java.nio.ByteBuffer;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Duration;
 import java.time.Instant;
@@ -387,7 +389,8 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 		PutObjectRequest.Builder putObjectRequestBuilder = PutObjectRequest.builder()
 			.bucket(bucket)
 			.key(resourceKey)
-			.metadata(metadata);
+			.metadata(metadata)
+			.contentType(getContentType(newName));
 		if (endpointUrl.contains(".amazonaws.com"))
 			putObjectRequestBuilder = putObjectRequestBuilder.acl(internalToAWSACL(acl));
 		PutObjectRequest putObjectRequest = putObjectRequestBuilder.build();
@@ -396,6 +399,64 @@ public String copy(String objectUrl, String newName, String tableName, String fi
 		return getResourceUrl(resourceKey, acl, defaultExpirationMinutes);
 	}
 
+	private String getContentType(String fileName) {
+		Path path = Paths.get(fileName);
+		String defaultContentType = "application/octet-stream";
+
+		try {
+			String probedContentType = Files.probeContentType(path);
+			if (probedContentType == null || probedContentType.equals(defaultContentType))
+				return findContentTypeByExtension(fileName);
+			return probedContentType;
+		} catch (IOException ioe) {
+			return findContentTypeByExtension(fileName);
+		}
+	}
+
+	private String findContentTypeByExtension(String fileName) {
+		String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
+		for (String[] mapping : contentTypes)
+			if (mapping[0].equals(fileExtension))
+				return mapping[1];
+		return "application/octet-stream";
+	}
+
+	private static final String contentTypes[][] = {
+		{"txt" 	, "text/plain"},
+		{"rtx" 	, "text/richtext"},
+		{"htm" 	, "text/html"},
+		{"html" , "text/html"},
+		{"xml" 	, "text/xml"},
+		{"aif"	, "audio/x-aiff"},
+		{"au"	, "audio/basic"},
+		{"wav"	, "audio/wav"},
+		{"bmp"	, "image/bmp"},
+		{"gif"	, "image/gif"},
+		{"jpe"	, "image/jpeg"},
+		{"jpeg"	, "image/jpeg"},
+		{"jpg"	, "image/jpeg"},
+		{"jfif"	, "image/pjpeg"},
+		{"tif"	, "image/tiff"},
+		{"tiff"	, "image/tiff"},
+		{"png"	, "image/x-png"},
+		{"3gp"	, "video/3gpp"},
+		{"3g2"	, "video/3gpp2"},
+		{"mpg"	, "video/mpeg"},
+		{"mpeg"	, "video/mpeg"},
+		{"mov"	, "video/quicktime"},
+		{"qt"	, "video/quicktime"},
+		{"avi"	, "video/x-msvideo"},
+		{"exe"	, "application/octet-stream"},
+		{"dll"	, "application/x-msdownload"},
+		{"ps"	, "application/postscript"},
+		{"pdf"	, "application/pdf"},
+		{"svg"	, "image/svg+xml"},
+		{"tgz"	, "application/x-compressed"},
+		{"zip"	, "application/x-zip-compressed"},
+		{"gz"	, "application/x-gzip"},
+		{"json"	, "application/json"}
+	};
+
 	private String buildPath(String... pathPart) {
 		ArrayList<String> pathParts = new ArrayList<>();
 		for (String part : pathPart) {

From 24ec2960bea36aad226edbd3a356f1bc6eef6dee Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 22 Jan 2024 15:30:24 -0300
Subject: [PATCH 20/23] Use hashmap instead of matrix

---
 .../db/driver/ExternalProviderS3V2.java       | 80 +++++++++----------
 1 file changed, 39 insertions(+), 41 deletions(-)

diff --git a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
index fd413f25e..3bb82b302 100644
--- a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
+++ b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
@@ -415,47 +415,45 @@ private String getContentType(String fileName) {
 
 	private String findContentTypeByExtension(String fileName) {
 		String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
-		for (String[] mapping : contentTypes)
-			if (mapping[0].equals(fileExtension))
-				return mapping[1];
-		return "application/octet-stream";
-	}
-
-	private static final String contentTypes[][] = {
-		{"txt" 	, "text/plain"},
-		{"rtx" 	, "text/richtext"},
-		{"htm" 	, "text/html"},
-		{"html" , "text/html"},
-		{"xml" 	, "text/xml"},
-		{"aif"	, "audio/x-aiff"},
-		{"au"	, "audio/basic"},
-		{"wav"	, "audio/wav"},
-		{"bmp"	, "image/bmp"},
-		{"gif"	, "image/gif"},
-		{"jpe"	, "image/jpeg"},
-		{"jpeg"	, "image/jpeg"},
-		{"jpg"	, "image/jpeg"},
-		{"jfif"	, "image/pjpeg"},
-		{"tif"	, "image/tiff"},
-		{"tiff"	, "image/tiff"},
-		{"png"	, "image/x-png"},
-		{"3gp"	, "video/3gpp"},
-		{"3g2"	, "video/3gpp2"},
-		{"mpg"	, "video/mpeg"},
-		{"mpeg"	, "video/mpeg"},
-		{"mov"	, "video/quicktime"},
-		{"qt"	, "video/quicktime"},
-		{"avi"	, "video/x-msvideo"},
-		{"exe"	, "application/octet-stream"},
-		{"dll"	, "application/x-msdownload"},
-		{"ps"	, "application/postscript"},
-		{"pdf"	, "application/pdf"},
-		{"svg"	, "image/svg+xml"},
-		{"tgz"	, "application/x-compressed"},
-		{"zip"	, "application/x-zip-compressed"},
-		{"gz"	, "application/x-gzip"},
-		{"json"	, "application/json"}
-	};
+		String contentType = contentTypes.get(fileExtension);
+		return contentType != null ? contentTypes.get(fileExtension) : "application/octet-stream";
+	}
+
+	private static Map<String, String> contentTypes  = new HashMap<String, String>() {{
+			put("txt" 	, "text/plain");
+			put("rtx" 	, "text/richtext");
+			put("htm" 	, "text/html");
+			put("html" , "text/html");
+			put("xml" 	, "text/xml");
+			put("aif"	, "audio/x-aiff");
+			put("au"	, "audio/basic");
+			put("wav"	, "audio/wav");
+			put("bmp"	, "image/bmp");
+			put("gif"	, "image/gif");
+			put("jpe"	, "image/jpeg");
+			put("jpeg"	, "image/jpeg");
+			put("jpg"	, "image/jpeg");
+			put("jfif"	, "image/pjpeg");
+			put("tif"	, "image/tiff");
+			put("tiff"	, "image/tiff");
+			put("png"	, "image/x-png");
+			put("3gp"	, "video/3gpp");
+			put("3g2"	, "video/3gpp2");
+			put("mpg"	, "video/mpeg");
+			put("mpeg"	, "video/mpeg");
+			put("mov"	, "video/quicktime");
+			put("qt"	, "video/quicktime");
+			put("avi"	, "video/x-msvideo");
+			put("exe"	, "application/octet-stream");
+			put("dll"	, "application/x-msdownload");
+			put("ps"	, "application/postscript");
+			put("pdf"	, "application/pdf");
+			put("svg"	, "image/svg+xml");
+			put("tgz"	, "application/x-compressed");
+			put("zip"	, "application/x-zip-compressed");
+			put("gz"	, "application/x-gzip");
+			put("json"	, "application/json");
+	}};
 
 	private String buildPath(String... pathPart) {
 		ArrayList<String> pathParts = new ArrayList<>();

From f68fc3750aea59b35a5cae8074c3a78bf33d2f81 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Mon, 29 Jan 2024 17:55:17 -0300
Subject: [PATCH 21/23] Only encode file name and not the entire path

---
 .../genexus/db/driver/ExternalProviderS3V2.java | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
index 3bb82b302..310be4501 100644
--- a/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
+++ b/gxcloudstorage-awss3-v2/src/main/java/com/genexus/db/driver/ExternalProviderS3V2.java
@@ -322,15 +322,20 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList
 			return presignedGetObjectRequest.url().toString();
 		} else {
 			try {
-				return String.format(
-					"https://%s.s3.%s.amazonaws.com/%s",
+				int lastIndex = Math.max(externalFileName.lastIndexOf('/'), externalFileName.lastIndexOf('\\'));
+				String path = externalFileName.substring(0, lastIndex + 1);
+				String fileName = externalFileName.substring(lastIndex + 1);
+				String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString());
+
+				String url = String.format(
+					"https://%s.s3.%s.amazonaws.com/%s%s",
 					bucket,
 					clientRegion,
-					URLEncoder.encode(
-						externalFileName,
-						StandardCharsets.UTF_8.toString()
-					)
+					path,
+					encodedFileName
 				);
+
+				return url;
 			} catch (UnsupportedEncodingException uee) {
 				logger.error("Failed to encode resource URL for " + externalFileName, uee);
 				return "";

From 44f8667912888cf2ff2de60c685a8083253cbf82 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Thu, 1 Feb 2024 15:43:58 -0300
Subject: [PATCH 22/23] Update class names in preprocess

---
 .../com/genexus/configuration/ExternalStorage.java    | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/java/src/main/java/com/genexus/configuration/ExternalStorage.java b/java/src/main/java/com/genexus/configuration/ExternalStorage.java
index ce6cd51ae..28d1d7664 100644
--- a/java/src/main/java/com/genexus/configuration/ExternalStorage.java
+++ b/java/src/main/java/com/genexus/configuration/ExternalStorage.java
@@ -100,7 +100,16 @@ private void preprocess(String name, GXProperties properties)
 		{
 
 			case "AMAZONS3":
-				className = "com.genexus.db.driver.ExternalProviderS3";
+				className = "com.genexus.db.driver.ExternalProviderS3V1";
+				setDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "us-east-1");
+				setDefaultProperty(properties, "STORAGE_ENDPOINT", "s3.amazonaws.com");
+				setEncryptProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID");
+				setEncryptProperty(properties, "STORAGE_PROVIDER_SECRETACCESSKEY");
+				setEncryptProperty(properties, "BUCKET_NAME");
+				break;
+
+			case "AMAZONS3V2":
+				className = "com.genexus.db.driver.ExternalProviderS3V2";
 				setDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "us-east-1");
 				setDefaultProperty(properties, "STORAGE_ENDPOINT", "s3.amazonaws.com");
 				setEncryptProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID");

From 54d4ff3a2aa60dda09fb1ca44e6c663deb1505a2 Mon Sep 17 00:00:00 2001
From: tomas-sexenian <tsexenian@genexus.com>
Date: Tue, 6 Feb 2024 15:53:29 -0300
Subject: [PATCH 23/23] Swich default s3 storage provider implementation

---
 .../java/com/genexus/configuration/ExternalStorage.java     | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/java/src/main/java/com/genexus/configuration/ExternalStorage.java b/java/src/main/java/com/genexus/configuration/ExternalStorage.java
index 28d1d7664..7b14295ae 100644
--- a/java/src/main/java/com/genexus/configuration/ExternalStorage.java
+++ b/java/src/main/java/com/genexus/configuration/ExternalStorage.java
@@ -100,7 +100,7 @@ private void preprocess(String name, GXProperties properties)
 		{
 
 			case "AMAZONS3":
-				className = "com.genexus.db.driver.ExternalProviderS3V1";
+				className = "com.genexus.db.driver.ExternalProviderS3V2";
 				setDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "us-east-1");
 				setDefaultProperty(properties, "STORAGE_ENDPOINT", "s3.amazonaws.com");
 				setEncryptProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID");
@@ -108,8 +108,8 @@ private void preprocess(String name, GXProperties properties)
 				setEncryptProperty(properties, "BUCKET_NAME");
 				break;
 
-			case "AMAZONS3V2":
-				className = "com.genexus.db.driver.ExternalProviderS3V2";
+			case "AMAZONS3V1":
+				className = "com.genexus.db.driver.ExternalProviderS3V1";
 				setDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "us-east-1");
 				setDefaultProperty(properties, "STORAGE_ENDPOINT", "s3.amazonaws.com");
 				setEncryptProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID");
