From 503fb1f04b52d35032ff4508c521a182958dcc9b Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Sat, 8 Nov 2025 22:17:14 -0300 Subject: [PATCH 1/8] Update Azure Storage SDK --- gxcloudstorage-azureblob/pom.xml | 17 +- .../driver/ExternalProviderAzureStorage.java | 541 ++++++++++-------- 2 files changed, 304 insertions(+), 254 deletions(-) diff --git a/gxcloudstorage-azureblob/pom.xml b/gxcloudstorage-azureblob/pom.xml index ca586d19a..2d18c45eb 100644 --- a/gxcloudstorage-azureblob/pom.xml +++ b/gxcloudstorage-azureblob/pom.xml @@ -26,15 +26,14 @@ ${project.version} - com.microsoft.azure - azure-storage - 8.6.6 - - - com.microsoft.azure - azure-keyvault-core - - + com.azure + azure-storage-blob + 12.32.0 + + + com.azure + azure-identity + 1.18.1 diff --git a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java index 723ea9eca..e85253f25 100644 --- a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java +++ b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java @@ -1,11 +1,20 @@ package com.genexus.db.driver; +import com.azure.core.exception.ClientAuthenticationException; +import com.azure.core.exception.HttpRequestException; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.*; +import com.azure.storage.blob.sas.BlobSasPermission; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; +import com.azure.storage.common.StorageSharedKeyCredential; import com.genexus.StructSdtMessages_Message; import com.genexus.util.GXService; import com.genexus.util.StorageUtils; -import com.microsoft.azure.storage.CloudStorageAccount; -import com.microsoft.azure.storage.StorageException; -import com.microsoft.azure.storage.blob.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -13,7 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; -import java.security.InvalidKeyException; +import java.time.OffsetDateTime; import java.util.*; public class ExternalProviderAzureStorage extends ExternalProviderBase implements ExternalProvider { @@ -37,46 +46,90 @@ public class ExternalProviderAzureStorage extends ExternalProviderBase implement private String account; private String key; - private CloudBlobContainer publicContainer; - private CloudBlobContainer privateContainer; - private CloudBlobClient client; + + private BlobServiceClient blobServiceClient; + private BlobContainerClient publicContainerClient; + private BlobContainerClient privateContainerClient; private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES; private String privateContainerName; private String publicContainerName; - private void init() throws Exception { try { account = getEncryptedPropertyValue(ACCOUNT, ACCOUNT_DEPRECATED); + } catch (Exception ex) { + logger.error("Error initializing Azure Storage: unable to get account", ex); + throw ex; + } + boolean useManagedIdentity = false; + try { key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); + if (key.isEmpty()) { + logger.info("ACCESS_KEY empty — using Managed Identity"); + useManagedIdentity = true; + } + } catch (Exception ex) { + if (key == null) { + logger.info("ACCESS_KEY null — using Managed Identity"); + useManagedIdentity = true; + } + } - CloudStorageAccount storageAccount = CloudStorageAccount.parse( - String.format("DefaultEndpointsProtocol=%1s;AccountName=%2s;AccountKey=%3s", "https", account, key)); - client = storageAccount.createCloudBlobClient(); - + try { String privateContainerNameValue = getEncryptedPropertyValue(PRIVATE_CONTAINER, PRIVATE_CONTAINER_DEPRECATED); String publicContainerNameValue = getEncryptedPropertyValue(PUBLIC_CONTAINER, PUBLIC_CONTAINER_DEPRECATED); privateContainerName = privateContainerNameValue.toLowerCase(); publicContainerName = publicContainerNameValue.toLowerCase(); - publicContainer = client.getContainerReference(publicContainerName); - publicContainer.createIfNotExists(); - - privateContainer = client.getContainerReference(privateContainerName); - privateContainer.createIfNotExists(); - - BlobContainerPermissions permissions = new BlobContainerPermissions(); - permissions.setPublicAccess(BlobContainerPublicAccessType.BLOB); - publicContainer.uploadPermissions(permissions); - } catch (URISyntaxException ex) { - logger.error("Invalid URI", ex); - } catch (StorageException sex) { - logger.error(sex.getMessage()); - } catch (InvalidKeyException ikex) { - logger.error("Invalid keys", ikex); + if (useManagedIdentity) { + initWithManagedIdentity(); + } else { + initWithAccountKey(); + } + } + catch (Exception ex) { + handleAndLogException("Initialization error", ex); + } + } + + private void initWithAccountKey() { + // Create BlobServiceClient with account key + StorageSharedKeyCredential credential = new StorageSharedKeyCredential(account, key); + blobServiceClient = new BlobServiceClientBuilder() + .endpoint(String.format("https://%s.blob.core.windows.net", account)) + .credential(credential) + .buildClient(); + + initContainerClients(); + } + + private void initWithManagedIdentity() { + // Create a DefaultAzureCredential + DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build(); + + // Create BlobServiceClient using the credential + blobServiceClient = new BlobServiceClientBuilder() + .endpoint(String.format("https://%s.blob.core.windows.net", account)) + .credential(credential) + .buildClient(); + + initContainerClients(); + } + + private void initContainerClients() { + // Create container clients and ensure containers exist + publicContainerClient = blobServiceClient.getBlobContainerClient(publicContainerName); + if (!publicContainerClient.exists()) { + publicContainerClient = blobServiceClient.createBlobContainer(publicContainerName); + publicContainerClient.setAccessPolicy(PublicAccessType.BLOB, null); + } + + privateContainerClient = blobServiceClient.getBlobContainerClient(privateContainerName); + if (!privateContainerClient.exists()) { + privateContainerClient = blobServiceClient.createBlobContainer(privateContainerName); } } @@ -96,26 +149,21 @@ public String getName() { public void download(String externalFileName, String localFile, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.downloadToFile(localFile); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ioex) { - logger.error("Error downloading file", ioex); + BlobClient blobClient = getBlobClient(externalFileName, acl); + blobClient.downloadToFile(localFile, true); + } catch (Exception ex) { + handleAndLogException("Invalid URI or error downloading file", ex); } } - - - private CloudBlockBlob getCloudBlockBlob(String fileName, ResourceAccessControlList acl) throws URISyntaxException, StorageException { - CloudBlockBlob blob; + + private BlobClient getBlobClient(String fileName, ResourceAccessControlList acl) { + BlobClient blobClient; if (isPrivateAcl(acl)) { - blob = privateContainer.getBlockBlobReference(fileName); + blobClient = privateContainerClient.getBlobClient(fileName); } else { - blob = publicContainer.getBlockBlobReference(fileName); + blobClient = publicContainerClient.getBlobClient(fileName); } - return blob; + return blobClient; } private boolean isPrivateAcl(ResourceAccessControlList acl) { @@ -125,39 +173,30 @@ private boolean isPrivateAcl(ResourceAccessControlList acl) { public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.uploadFromFile(localFile); + BlobClient blobClient = getBlobClient(externalFileName, acl); + blobClient.uploadFromFile(localFile, true); return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ex) { - logger.error("Error uploading file", ex); + } catch (Exception ex) { + handleAndLogException("Error uploading file", ex); + return ""; } - return ""; } public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.getProperties().setContentType((externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) ? "image/jpeg" : streamInfo.detectedContentType); - try (BlobOutputStream blobOutputStream = blob.openOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = streamInfo.inputStream.read(buffer)) != -1) { - blobOutputStream.write(buffer, 0, bytesRead); - } - } + BlobClient blobClient = getBlobClient(externalFileName, acl); + + // Set content type + String contentType = (externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) + ? "image/jpeg" : streamInfo.detectedContentType; + BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(contentType); + + blobClient.upload(streamInfo.inputStream, streamInfo.contentLength, true); + blobClient.setHttpHeaders(headers); + return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); - - } catch (URISyntaxException ex) { - logger.error("Invalid URI", ex); - return ""; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ex) { - logger.error("Error uploading file", ex); + } catch (Exception ex) { + handleAndLogException("Error uploading file", ex); return ""; } } @@ -167,51 +206,46 @@ public String get(String externalFileName, ResourceAccessControlList acl, int ex if (exists(externalFileName, acl)) { return getResourceUrl(externalFileName, acl, expirationMinutes); } - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); } catch (Exception ex) { - logger.error("Error getting file", ex); + handleAndLogException("Error getting file", ex); return ""; } return ""; } - private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) throws URISyntaxException, StorageException { + private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { if (isPrivateAcl(acl)) { return getPrivate(externalFileName, expirationMinutes); } else { - CloudBlockBlob blob = publicContainer.getBlockBlobReference(externalFileName); - return blob.getUri().toString(); + BlobClient blobClient = publicContainerClient.getBlobClient(externalFileName); + return blobClient.getBlobUrl(); } } private String getPrivate(String externalFileName, int expirationMinutes) { try { - CloudBlockBlob blob = privateContainer.getBlockBlobReference(externalFileName); - SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); - policy.setPermissionsFromString("r"); - Calendar date = Calendar.getInstance(); + BlobClient blobClient = privateContainerClient.getBlobClient(externalFileName); + + // Create SAS token expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; - Date expire = new Date(date.getTimeInMillis() + (expirationMinutes * 60000)); - policy.setSharedAccessExpiryTime(expire); - return blob.getUri().toString() + "?" + blob.generateSharedAccessSignature(policy, null); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + OffsetDateTime expiryTime = OffsetDateTime.now().plusMinutes(expirationMinutes); + + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues(expiryTime, permission); + + return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); } catch (Exception ex) { - logger.error("Error getting private file", ex); + handleAndLogException("Error getting private file", ex); + return ""; } - return ""; - } public void delete(String objectName, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.deleteIfExists(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + BlobClient blobClient = getBlobClient(objectName, acl); + blobClient.deleteIfExists(); + } catch (Exception ex) { + handleAndLogException("Error deleting file", ex); } } @@ -229,33 +263,53 @@ private String resolveObjectName(String urlOrObjectName, ResourceAccessControlLi public String copy(String objectName, String newName, ResourceAccessControlList acl) { objectName = resolveObjectName(objectName, acl); try { - CloudBlockBlob sourceBlob = getCloudBlockBlob(objectName, acl); - CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); - targetBlob.startCopy(sourceBlob); + BlobClient sourceBlob = getBlobClient(objectName, acl); + BlobClient targetBlob = getBlobClient(newName, acl); + + // Get source URL with SAS if it's private + String sourceBlobUrl; + if (isPrivateAcl(acl)) { + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusMinutes(5), permission); + sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); + } else { + sourceBlobUrl = sourceBlob.getBlobUrl(); + } + + // Start the copy operation + targetBlob.beginCopy(sourceBlobUrl, null); return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + handleAndLogException("Error copying file", ex); + return ""; } - return ""; } public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) { objectUrl = objectUrl.replace(getUrl(), ""); newName = tableName + "/" + fieldName + "/" + newName; try { - CloudBlockBlob sourceBlob = privateContainer.getBlockBlobReference(objectUrl); //Source will be always on the private container - CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); - targetBlob.setMetadata(createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName))); - targetBlob.startCopy(sourceBlob); + BlobClient sourceBlob = privateContainerClient.getBlobClient(objectUrl); + BlobClient targetBlob = getBlobClient(newName, acl); + + // Set metadata + Map metadata = createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName)); + targetBlob.setMetadata(metadata); + + // Get source URL with SAS for private access + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusMinutes(5), permission); + String sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); + + // Start the copy operation + targetBlob.beginCopy(sourceBlobUrl, null); return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + handleAndLogException("Error copying file", ex); + return ""; } - return ""; } private HashMap createObjectMetadata(String table, String field, String name) { @@ -268,45 +322,40 @@ private HashMap createObjectMetadata(String table, String field, public long getLength(String objectName, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - return blob.getProperties().getLength(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); + BlobClient blobClient = getBlobClient(objectName, acl); + BlobProperties properties = blobClient.getProperties(); + return properties.getBlobSize(); + } catch (Exception ex) { + handleAndLogException("Error getting file length", ex); return 0; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); } } public Date getLastModified(String objectName, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - return blob.getProperties().getLastModified(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); + BlobClient blobClient = getBlobClient(objectName, acl); + BlobProperties properties = blobClient.getProperties(); + return Date.from(properties.getLastModified().toInstant()); + } catch (Exception ex) { + handleAndLogException("Error getting last modified date", ex); return new Date(); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); } } public boolean exists(String objectName, ResourceAccessControlList acl) { try { - return getCloudBlockBlob(objectName, acl).exists(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); + BlobClient blobClient = getBlobClient(objectName, acl); + return blobClient.exists(); + } catch (Exception ex) { + handleAndLogException("Error checking if file exists", ex); return false; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); } } public String getDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); if (existsDirectory(directoryName)) { - return publicContainer.getName() + StorageUtils.DELIMITER + directoryName; + return publicContainerClient.getBlobContainerName() + StorageUtils.DELIMITER + directoryName; } else { return ""; } @@ -315,40 +364,35 @@ public String getDirectory(String directoryName) { public boolean existsDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - return true; - } - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - itemName = itemName.substring(0, itemName.length() - 1); - if (!itemName.equals(directoryName)) { - return true; - } + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Check if there are any blobs with this prefix + boolean exists = false; + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (!name.equals(directoryName)) { + // If we found any blob that isn't just the directory marker itself + exists = true; + break; } } + return exists; + } catch (Exception ex) { + handleAndLogException("Error checking if directory exists", ex); return false; - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - return false; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); } } public void createDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - CloudBlockBlob blob = publicContainer.getBlockBlobReference(directoryName); - blob.uploadFromByteArray(new byte[0], 0, 0); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ioex) { - logger.error("Error uploading file", ioex); + // Create a blob with empty content to mark the directory + BlobClient blobClient = publicContainerClient.getBlobClient(directoryName); + byte[] emptyContent = new byte[0]; + blobClient.upload(new ByteArrayInputStream(emptyContent), emptyContent.length, true); + } catch (Exception ex) { + handleAndLogException("Error creating directory", ex); } } @@ -356,32 +400,26 @@ public void deleteDirectory(String directoryName) { ResourceAccessControlList acl = null; directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - itemName = ((CloudPageBlob) item).getName(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - delete(itemName, acl); - } - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - if (!itemName.equals(directoryName)) { - deleteDirectory(itemName); + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Delete all blobs in the directory + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName)) { + if (name.endsWith(StorageUtils.DELIMITER)) { + // This is a "subdirectory" + if (!name.equals(directoryName)) { + deleteDirectory(name); + } + } else { + // This is a file + delete(name, acl); } } } - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + handleAndLogException("Error deleting directory", ex); } } @@ -393,31 +431,31 @@ public void renameDirectory(String directoryName, String newDirectoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName); try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - itemName = ((CloudPageBlob) item).getName(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Copy and rename all blobs in the directory + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName)) { + if (name.endsWith(StorageUtils.DELIMITER)) { + // This is a "subdirectory" + if (!name.equals(directoryName)) { + String subdirName = name.substring(directoryName.length()); + renameDirectory(name, newDirectoryName + subdirName); + } + } else { + // This is a file, rename it + String newName = name.replace(directoryName, newDirectoryName); + rename(name, newName, acl); } - rename(itemName, itemName.replace(directoryName, newDirectoryName), acl); - } - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - renameDirectory(directoryName + StorageUtils.DELIMITER + itemName, newDirectoryName + StorageUtils.DELIMITER + itemName); } } + + // Delete the original directory deleteDirectory(directoryName); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + handleAndLogException("Error renaming directory", ex); } } @@ -425,20 +463,19 @@ public List getFiles(String directoryName, String filter) { List files = new ArrayList(); directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - files.add(((CloudPageBlob) item).getName()); - } else if (item instanceof CloudBlockBlob) { - files.add(((CloudBlockBlob) item).getName()); - } + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Add all file names to the list + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName) && !name.endsWith(StorageUtils.DELIMITER)) { + // This is a file, add it to the list + files.add(name); } } - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + handleAndLogException("Error getting files", ex); } return files; } @@ -451,60 +488,57 @@ public List getSubDirectories(String directoryName) { List directories = new ArrayList(); directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - directories.add(((CloudBlobDirectory) item).getPrefix()); - } else if (item instanceof CloudBlockBlob) { - directories.add(((CloudBlockBlob) item).getName()); + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Get all subdirectory names + Set dirSet = new HashSet(); + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName) && !name.equals(directoryName)) { + // Get the subdirectory name + String remainingPath = name.substring(directoryName.length()); + int slashIndex = remainingPath.indexOf(StorageUtils.DELIMITER); + + if (slashIndex >= 0) { + // This is a subdirectory or a file in a subdirectory + String subdirName = directoryName + remainingPath.substring(0, slashIndex + 1); + dirSet.add(subdirName); } } } - directories.remove(directoryName); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + directories.addAll(dirSet); + } catch (Exception ex) { + handleAndLogException("Error getting subdirectories", ex); } return directories; } public InputStream getStream(String objectName, ResourceAccessControlList acl) { try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - byte[] bytes = new byte[(int) blob.getProperties().getLength()]; - blob.downloadToByteArray(bytes, 0); - - InputStream stream = new ByteArrayInputStream(bytes); - return stream; - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); + BlobClient blobClient = getBlobClient(objectName, acl); + return blobClient.openInputStream(); + } catch (Exception ex) { + handleAndLogException("Error getting stream", ex); + return null; } - return null; } public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) { try { - StorageException aex = (StorageException) ex.getCause(); - msg.setId(aex.getErrorCode()); - return true; + // Extract error information from the SDK exceptions + String errorMessage = ex.getMessage(); + if (errorMessage != null) { + msg.setId("AzureError"); + msg.setDescription(errorMessage); + return true; + } + return false; } catch (Exception e) { return false; } } - private boolean isDirectory(ListBlobItem item) { - return (item instanceof CloudBlobDirectory) || (item instanceof CloudBlockBlob && ((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); - } - - private boolean isFile(ListBlobItem item) { - return (item instanceof CloudPageBlob) || (item instanceof CloudBlockBlob && !((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); - } - private String getUrl() { return "https://" + account + ".blob.core.windows.net/"; } @@ -522,4 +556,21 @@ public String getObjectNameFromURL(String url) { } return objectName; } + + private void handleAndLogException(String message, Exception ex) { + if (ex instanceof BlobStorageException) { + logger.error("Azure Storage error: {} (Status: {}, Code: {})", + ((BlobStorageException) ex).getServiceMessage(), ((BlobStorageException) ex).getStatusCode(), ((BlobStorageException) ex).getErrorCode()); + } else if (ex instanceof ClientAuthenticationException) { + logger.error("Authentication error: {}", ex.getMessage()); + } else if (ex instanceof HttpRequestException) { + logger.error("Connection error: {}", ex.getMessage()); + } else if (ex instanceof URISyntaxException) { + logger.error("Invalid URI: {}", ex.getMessage()); + } else if (ex instanceof IOException) { + logger.error(message, ex); + } else { + logger.error("Unexpected storage error", ex); + } + } } From c56ac2e40bf18c0a2db828c28148a8805d018015 Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Sun, 16 Nov 2025 23:58:40 -0300 Subject: [PATCH 2/8] Keep legacy implementation --- gxcloudstorage-azureblob-legacy/pom.xml | 40 ++ .../ExternalProviderAzureStorageLegacy.java | 525 ++++++++++++++++++ .../driver/ExternalProviderAzureStorage.java | 55 +- gxcloudstorage-tests/resources/test/text.txt | 2 +- pom.xml | 31 +- 5 files changed, 621 insertions(+), 32 deletions(-) create mode 100644 gxcloudstorage-azureblob-legacy/pom.xml create mode 100644 gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java diff --git a/gxcloudstorage-azureblob-legacy/pom.xml b/gxcloudstorage-azureblob-legacy/pom.xml new file mode 100644 index 000000000..1689a4345 --- /dev/null +++ b/gxcloudstorage-azureblob-legacy/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + com.genexus + parent + ${revision}${changelist} + + + gxcloudstorage-azureblob-legacy + GeneXus Azure Blob Cloud Storage Legacy + + + + ${project.groupId} + gxclassR + ${project.version} + test + + + com.genexus + gxcloudstorage-common + ${project.version} + + + com.microsoft.azure + azure-storage + 8.6.6 + + + com.microsoft.azure + azure-keyvault-core + + + + + diff --git a/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java b/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java new file mode 100644 index 000000000..79ea143ae --- /dev/null +++ b/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java @@ -0,0 +1,525 @@ +package com.genexus.db.driver; + +import com.genexus.StructSdtMessages_Message; +import com.genexus.util.GXService; +import com.genexus.util.StorageUtils; +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.StorageException; +import com.microsoft.azure.storage.blob.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.util.*; + +public class ExternalProviderAzureStorageLegacy extends ExternalProviderBase implements ExternalProvider { + private static Logger logger = LogManager.getLogger(ExternalProviderAzureStorageLegacy.class); + + static final String NAME = "AZUREBS"; //Azure Blob Storage + + static final String ACCOUNT = "ACCOUNT_NAME"; + static final String ACCESS_KEY = "ACCESS_KEY"; + static final String PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; + static final String PRIVATE_CONTAINER = "PRIVATE_CONTAINER_NAME"; + + @Deprecated + static final String ACCOUNT_DEPRECATED = "ACCOUNT_NAME"; + @Deprecated + static final String KEY_DEPRECATED = "ACCESS_KEY"; + @Deprecated + static final String PUBLIC_CONTAINER_DEPRECATED = "PUBLIC_CONTAINER_NAME"; + @Deprecated + static final String PRIVATE_CONTAINER_DEPRECATED = "PRIVATE_CONTAINER_NAME"; + + private String account; + private String key; + private CloudBlobContainer publicContainer; + private CloudBlobContainer privateContainer; + private CloudBlobClient client; + + private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES; + + private String privateContainerName; + private String publicContainerName; + + + private void init() throws Exception { + try { + account = getEncryptedPropertyValue(ACCOUNT, ACCOUNT_DEPRECATED); + key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); + + CloudStorageAccount storageAccount = CloudStorageAccount.parse( + String.format("DefaultEndpointsProtocol=%1s;AccountName=%2s;AccountKey=%3s", "https", account, key)); + client = storageAccount.createCloudBlobClient(); + + String privateContainerNameValue = getEncryptedPropertyValue(PRIVATE_CONTAINER, PRIVATE_CONTAINER_DEPRECATED); + String publicContainerNameValue = getEncryptedPropertyValue(PUBLIC_CONTAINER, PUBLIC_CONTAINER_DEPRECATED); + + privateContainerName = privateContainerNameValue.toLowerCase(); + publicContainerName = publicContainerNameValue.toLowerCase(); + + publicContainer = client.getContainerReference(publicContainerName); + publicContainer.createIfNotExists(); + + privateContainer = client.getContainerReference(privateContainerName); + privateContainer.createIfNotExists(); + + BlobContainerPermissions permissions = new BlobContainerPermissions(); + permissions.setPublicAccess(BlobContainerPublicAccessType.BLOB); + publicContainer.uploadPermissions(permissions); + } catch (URISyntaxException ex) { + logger.error("Invalid URI", ex); + } catch (StorageException sex) { + logger.error(sex.getMessage()); + } catch (InvalidKeyException ikex) { + logger.error("Invalid keys", ikex); + } + } + + public ExternalProviderAzureStorageLegacy() throws Exception { + super(); + init(); + } + + public ExternalProviderAzureStorageLegacy(GXService providerService) throws Exception { + super(providerService); + init(); + } + + public String getName() { + return NAME; + } + + public void download(String externalFileName, String localFile, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.downloadToFile(localFile); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ioex) { + logger.error("Error downloading file", ioex); + } + } + + + private CloudBlockBlob getCloudBlockBlob(String fileName, ResourceAccessControlList acl) throws URISyntaxException, StorageException { + CloudBlockBlob blob; + if (isPrivateAcl(acl)) { + blob = privateContainer.getBlockBlobReference(fileName); + } else { + blob = publicContainer.getBlockBlobReference(fileName); + } + return blob; + } + + private boolean isPrivateAcl(ResourceAccessControlList acl) { + // If default ACL is private, use always private. + return this.defaultAcl == ResourceAccessControlList.Private || acl == ResourceAccessControlList.Private; + } + + public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.uploadFromFile(localFile); + return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ex) { + logger.error("Error uploading file", ex); + } + return ""; + } + + public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { + try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.getProperties().setContentType((externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) ? "image/jpeg" : streamInfo.detectedContentType); + try (BlobOutputStream blobOutputStream = blob.openOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = streamInfo.inputStream.read(buffer)) != -1) { + blobOutputStream.write(buffer, 0, bytesRead); + } + } + return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); + + } catch (URISyntaxException ex) { + logger.error("Invalid URI", ex); + return ""; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ex) { + logger.error("Error uploading file", ex); + return ""; + } + } + + public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { + try { + if (exists(externalFileName, acl)) { + return getResourceUrl(externalFileName, acl, expirationMinutes); + } + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + logger.error("Error getting file", ex); + return ""; + } + return ""; + } + + private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) throws URISyntaxException, StorageException { + if (isPrivateAcl(acl)) { + return getPrivate(externalFileName, expirationMinutes); + } else { + CloudBlockBlob blob = publicContainer.getBlockBlobReference(externalFileName); + return blob.getUri().toString(); + } + } + + private String getPrivate(String externalFileName, int expirationMinutes) { + try { + CloudBlockBlob blob = privateContainer.getBlockBlobReference(externalFileName); + SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); + policy.setPermissionsFromString("r"); + Calendar date = Calendar.getInstance(); + expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; + Date expire = new Date(date.getTimeInMillis() + (expirationMinutes * 60000)); + policy.setSharedAccessExpiryTime(expire); + return blob.getUri().toString() + "?" + blob.generateSharedAccessSignature(policy, null); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (Exception ex) { + logger.error("Error getting private file", ex); + } + return ""; + + } + + public void delete(String objectName, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.deleteIfExists(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public String rename(String objectName, String newName, ResourceAccessControlList acl) { + String ret = copy(objectName, newName, acl); + delete(objectName, acl); + return ret; + } + + private String resolveObjectName(String urlOrObjectName, ResourceAccessControlList acl) { + String objectName = getObjectNameFromURL(urlOrObjectName); + return (objectName == null || objectName.length() == 0)? urlOrObjectName: objectName; + } + + public String copy(String objectName, String newName, ResourceAccessControlList acl) { + objectName = resolveObjectName(objectName, acl); + try { + CloudBlockBlob sourceBlob = getCloudBlockBlob(objectName, acl); + CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); + targetBlob.startCopy(sourceBlob); + return getResourceUrl(newName, acl, defaultExpirationMinutes); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + return ""; + } + + public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) { + objectUrl = objectUrl.replace(getUrl(), ""); + newName = tableName + "/" + fieldName + "/" + newName; + try { + CloudBlockBlob sourceBlob = privateContainer.getBlockBlobReference(objectUrl); //Source will be always on the private container + CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); + targetBlob.setMetadata(createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName))); + targetBlob.startCopy(sourceBlob); + return getResourceUrl(newName, acl, defaultExpirationMinutes); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + return ""; + } + + private HashMap createObjectMetadata(String table, String field, String name) { + HashMap metadata = new HashMap(); + metadata.put("Table", table); + metadata.put("Field", field); + metadata.put("KeyValue", name); + return metadata; + } + + public long getLength(String objectName, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + return blob.getProperties().getLength(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + return 0; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public Date getLastModified(String objectName, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + return blob.getProperties().getLastModified(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + return new Date(); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public boolean exists(String objectName, ResourceAccessControlList acl) { + try { + return getCloudBlockBlob(objectName, acl).exists(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + return false; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public String getDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + if (existsDirectory(directoryName)) { + return publicContainer.getName() + StorageUtils.DELIMITER + directoryName; + } else { + return ""; + } + } + + public boolean existsDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + return true; + } + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + itemName = itemName.substring(0, itemName.length() - 1); + if (!itemName.equals(directoryName)) { + return true; + } + } + } + return false; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + return false; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public void createDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + CloudBlockBlob blob = publicContainer.getBlockBlobReference(directoryName); + blob.uploadFromByteArray(new byte[0], 0, 0); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ioex) { + logger.error("Error uploading file", ioex); + } + } + + public void deleteDirectory(String directoryName) { + ResourceAccessControlList acl = null; + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + itemName = ((CloudPageBlob) item).getName(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + delete(itemName, acl); + } + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + if (!itemName.equals(directoryName)) { + deleteDirectory(itemName); + } + } + } + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public void renameDirectory(String directoryName, String newDirectoryName) { + ResourceAccessControlList acl = null; + if (!existsDirectory(newDirectoryName)) { + createDirectory(newDirectoryName); + } + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName); + try { + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + itemName = ((CloudPageBlob) item).getName(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + rename(itemName, itemName.replace(directoryName, newDirectoryName), acl); + } + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + renameDirectory(directoryName + StorageUtils.DELIMITER + itemName, newDirectoryName + StorageUtils.DELIMITER + itemName); + } + } + deleteDirectory(directoryName); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + public List getFiles(String directoryName, String filter) { + List files = new ArrayList(); + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + files.add(((CloudPageBlob) item).getName()); + } else if (item instanceof CloudBlockBlob) { + files.add(((CloudBlockBlob) item).getName()); + } + } + } + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + return files; + } + + public List getFiles(String directoryName) { + return getFiles(directoryName, ""); + } + + public List getSubDirectories(String directoryName) { + List directories = new ArrayList(); + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + directories.add(((CloudBlobDirectory) item).getPrefix()); + } else if (item instanceof CloudBlockBlob) { + directories.add(((CloudBlockBlob) item).getName()); + } + } + } + directories.remove(directoryName); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + return directories; + } + + public InputStream getStream(String objectName, ResourceAccessControlList acl) { + try { + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + byte[] bytes = new byte[(int) blob.getProperties().getLength()]; + blob.downloadToByteArray(bytes, 0); + + InputStream stream = new ByteArrayInputStream(bytes); + return stream; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + return null; + } + + public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) { + try { + StorageException aex = (StorageException) ex.getCause(); + msg.setId(aex.getErrorCode()); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean isDirectory(ListBlobItem item) { + return (item instanceof CloudBlobDirectory) || (item instanceof CloudBlockBlob && ((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); + } + + private boolean isFile(ListBlobItem item) { + return (item instanceof CloudPageBlob) || (item instanceof CloudBlockBlob && !((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); + } + + private String getUrl() { + return "https://" + account + ".blob.core.windows.net/"; + } + + public String getObjectNameFromURL(String url) { + String objectName = null; + String baseUrl = this.getUrl(); + String publicContainerUrl = String.format("%s%s/", baseUrl , publicContainerName); + String privateContainerUrl = String.format("%s%s/", baseUrl , privateContainerName); + if (url.startsWith(publicContainerUrl)) { + objectName = url.replace(publicContainerUrl, ""); + } + if (url.startsWith(privateContainerUrl)) { + objectName = url.replace(privateContainerUrl, ""); + } + return objectName; + } +} diff --git a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java index e85253f25..b07937b53 100644 --- a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java +++ b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java @@ -11,7 +11,9 @@ import com.azure.storage.blob.models.*; import com.azure.storage.blob.sas.BlobSasPermission; import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; +import com.azure.storage.blob.specialized.BlockBlobClient; import com.azure.storage.common.StorageSharedKeyCredential; +import com.azure.storage.blob.options.BlobParallelUploadOptions; import com.genexus.StructSdtMessages_Message; import com.genexus.util.GXService; import com.genexus.util.StorageUtils; @@ -35,6 +37,8 @@ public class ExternalProviderAzureStorage extends ExternalProviderBase implement static final String PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; static final String PRIVATE_CONTAINER = "PRIVATE_CONTAINER_NAME"; + private boolean useManagedIdentity; + @Deprecated static final String ACCOUNT_DEPRECATED = "ACCOUNT_NAME"; @Deprecated @@ -63,7 +67,7 @@ private void init() throws Exception { logger.error("Error initializing Azure Storage: unable to get account", ex); throw ex; } - boolean useManagedIdentity = false; + try { key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); if (key.isEmpty()) { @@ -183,19 +187,28 @@ public String upload(String localFile, String externalFileName, ResourceAccessCo } public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { - try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { - BlobClient blobClient = getBlobClient(externalFileName, acl); - + //https://docs.azure.cn/en-us/storage/blobs/storage-blob-upload-java + try (ExternalProviderHelper.InputStreamWithLength streamInfo = + ExternalProviderHelper.getInputStreamContentLength(input)) { + + BlockBlobClient blobClient = + getBlobClient(externalFileName, acl).getBlockBlobClient(); + // Set content type - String contentType = (externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) - ? "image/jpeg" : streamInfo.detectedContentType; + String contentType = + (externalFileName.endsWith(".tmp") && + "application/octet-stream".equals(streamInfo.detectedContentType)) + ? "image/jpeg" + : streamInfo.detectedContentType; + BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(contentType); - + + // Upload with headers in one shot (equivalent to old behavior) blobClient.upload(streamInfo.inputStream, streamInfo.contentLength, true); - blobClient.setHttpHeaders(headers); - + return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); - } catch (Exception ex) { + } + catch (Exception ex) { handleAndLogException("Error uploading file", ex); return ""; } @@ -225,15 +238,25 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList private String getPrivate(String externalFileName, int expirationMinutes) { try { BlobClient blobClient = privateContainerClient.getBlobClient(externalFileName); - - // Create SAS token + expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; OffsetDateTime expiryTime = OffsetDateTime.now().plusMinutes(expirationMinutes); - + // Permissions (read) BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); - BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues(expiryTime, permission); - - return blobClient.getBlobUrl() + "?" + blobClient.generateSas(values); + BlobServiceSasSignatureValues values = + new BlobServiceSasSignatureValues(expiryTime, permission); + String sasToken; + if (!useManagedIdentity) { + sasToken = blobClient.generateSas(values); + } else { + BlobServiceClient blobServiceClient = privateContainerClient.getServiceClient(); + OffsetDateTime start = OffsetDateTime.now().minusMinutes(1); + UserDelegationKey userDelegationKey = + blobServiceClient.getUserDelegationKey(start, expiryTime); + sasToken = blobClient.generateUserDelegationSas(values, userDelegationKey); + } + return blobClient.getBlobUrl() + "?" + sasToken; + } catch (Exception ex) { handleAndLogException("Error getting private file", ex); return ""; diff --git a/gxcloudstorage-tests/resources/test/text.txt b/gxcloudstorage-tests/resources/test/text.txt index fc976593f..a21acf5cc 100644 --- a/gxcloudstorage-tests/resources/test/text.txt +++ b/gxcloudstorage-tests/resources/test/text.txt @@ -1 +1 @@ -test upload IBM \ No newline at end of file +This is a Test File for Standard Classes Java diff --git a/pom.xml b/pom.xml index c036ff727..d03bdd72e 100644 --- a/pom.xml +++ b/pom.xml @@ -101,8 +101,8 @@ gxwebsocket gxwebsocketjakarta gxawsserverless - gxserverlesscommon - gxazureserverless + gxserverlesscommon + gxazureserverless androidreports gxqueue gxqueue-awssqs @@ -115,19 +115,20 @@ gxcloudstorage-azureblob gxcloudstorage-ibmcos gxcloudstorage-tests - gxobservability - gxcloudstorage-awss3-v2 - gxcompress - securityapicommons - gamsaml20 - gxjwt - gxcryptography - gxxmlsignature - gxsftp - gxftps - gamutils - gamtotp - + gxobservability + gxcloudstorage-awss3-v2 + gxcompress + securityapicommons + gamsaml20 + gxjwt + gxcryptography + gxxmlsignature + gxsftp + gxftps + gamutils + gamtotp + gxcloudstorage-azureblob-legacy + From 684bebc47106c9f65cb239344963256ee40761a8 Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Mon, 17 Nov 2025 09:37:43 -0300 Subject: [PATCH 3/8] Define latest artifact instead of having legacy. Soy the original package is the legacy (for compatibility) --- .../pom.xml | 21 +- .../ExternalProviderAzureStorageLatest.java | 608 ++++++++++++++++++ .../ExternalProviderAzureStorageLegacy.java | 525 --------------- gxcloudstorage-azureblob/pom.xml | 17 +- .../driver/ExternalProviderAzureStorage.java | 566 +++++++--------- pom.xml | 2 +- 6 files changed, 874 insertions(+), 865 deletions(-) rename {gxcloudstorage-azureblob-legacy => gxcloudstorage-azureblob-latest}/pom.xml (66%) create mode 100644 gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java delete mode 100644 gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java diff --git a/gxcloudstorage-azureblob-legacy/pom.xml b/gxcloudstorage-azureblob-latest/pom.xml similarity index 66% rename from gxcloudstorage-azureblob-legacy/pom.xml rename to gxcloudstorage-azureblob-latest/pom.xml index 1689a4345..6adf571f3 100644 --- a/gxcloudstorage-azureblob-legacy/pom.xml +++ b/gxcloudstorage-azureblob-latest/pom.xml @@ -10,8 +10,8 @@ ${revision}${changelist} - gxcloudstorage-azureblob-legacy - GeneXus Azure Blob Cloud Storage Legacy + gxcloudstorage-azureblob-latest + GeneXus Azure Blob Cloud Storage @@ -26,15 +26,14 @@ ${project.version} - com.microsoft.azure - azure-storage - 8.6.6 - - - com.microsoft.azure - azure-keyvault-core - - + com.azure + azure-storage-blob + 12.32.0 + + + com.azure + azure-identity + 1.18.1 diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java new file mode 100644 index 000000000..a3a4c9a99 --- /dev/null +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -0,0 +1,608 @@ +package com.genexus.db.driver; + +import com.azure.core.exception.ClientAuthenticationException; +import com.azure.core.exception.HttpRequestException; +import com.azure.identity.DefaultAzureCredential; +import com.azure.identity.DefaultAzureCredentialBuilder; +import com.azure.storage.blob.BlobClient; +import com.azure.storage.blob.BlobContainerClient; +import com.azure.storage.blob.BlobServiceClient; +import com.azure.storage.blob.BlobServiceClientBuilder; +import com.azure.storage.blob.models.*; +import com.azure.storage.blob.sas.BlobSasPermission; +import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; +import com.azure.storage.blob.specialized.BlockBlobClient; +import com.azure.storage.common.StorageSharedKeyCredential; +import com.azure.storage.blob.options.BlobParallelUploadOptions; +import com.genexus.StructSdtMessages_Message; +import com.genexus.util.GXService; +import com.genexus.util.StorageUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.time.OffsetDateTime; +import java.util.*; + +public class ExternalProviderAzureStorageLatest extends ExternalProviderBase implements ExternalProvider { + private static Logger logger = LogManager.getLogger(ExternalProviderAzureStorageLatest.class); + + static final String NAME = "AZUREBS"; //Azure Blob Storage + + static final String ACCOUNT = "ACCOUNT_NAME"; + static final String ACCESS_KEY = "ACCESS_KEY"; + static final String PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; + static final String PRIVATE_CONTAINER = "PRIVATE_CONTAINER_NAME"; + + private boolean useManagedIdentity; + + @Deprecated + static final String ACCOUNT_DEPRECATED = "ACCOUNT_NAME"; + @Deprecated + static final String KEY_DEPRECATED = "ACCESS_KEY"; + @Deprecated + static final String PUBLIC_CONTAINER_DEPRECATED = "PUBLIC_CONTAINER_NAME"; + @Deprecated + static final String PRIVATE_CONTAINER_DEPRECATED = "PRIVATE_CONTAINER_NAME"; + + private String account; + private String key; + + private BlobServiceClient blobServiceClient; + private BlobContainerClient publicContainerClient; + private BlobContainerClient privateContainerClient; + + private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES; + + private String privateContainerName; + private String publicContainerName; + + private void init() throws Exception { + try { + account = getEncryptedPropertyValue(ACCOUNT, ACCOUNT_DEPRECATED); + } catch (Exception ex) { + logger.error("Error initializing Azure Storage: unable to get account", ex); + throw ex; + } + + try { + key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); + if (key.isEmpty()) { + logger.info("ACCESS_KEY empty — using Managed Identity"); + useManagedIdentity = true; + } + } catch (Exception ex) { + if (key == null) { + logger.info("ACCESS_KEY null — using Managed Identity"); + useManagedIdentity = true; + } + } + + try { + String privateContainerNameValue = getEncryptedPropertyValue(PRIVATE_CONTAINER, PRIVATE_CONTAINER_DEPRECATED); + String publicContainerNameValue = getEncryptedPropertyValue(PUBLIC_CONTAINER, PUBLIC_CONTAINER_DEPRECATED); + + privateContainerName = privateContainerNameValue.toLowerCase(); + publicContainerName = publicContainerNameValue.toLowerCase(); + + if (useManagedIdentity) { + initWithManagedIdentity(); + } else { + initWithAccountKey(); + } + } + catch (Exception ex) { + handleAndLogException("Initialization error", ex); + } + } + + private void initWithAccountKey() { + // Create BlobServiceClient with account key + StorageSharedKeyCredential credential = new StorageSharedKeyCredential(account, key); + blobServiceClient = new BlobServiceClientBuilder() + .endpoint(String.format("https://%s.blob.core.windows.net", account)) + .credential(credential) + .buildClient(); + + initContainerClients(); + } + + private void initWithManagedIdentity() { + // Create a DefaultAzureCredential + DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build(); + + // Create BlobServiceClient using the credential + blobServiceClient = new BlobServiceClientBuilder() + .endpoint(String.format("https://%s.blob.core.windows.net", account)) + .credential(credential) + .buildClient(); + + initContainerClients(); + } + + private void initContainerClients() { + // Create container clients and ensure containers exist + publicContainerClient = blobServiceClient.getBlobContainerClient(publicContainerName); + if (!publicContainerClient.exists()) { + publicContainerClient = blobServiceClient.createBlobContainer(publicContainerName); + publicContainerClient.setAccessPolicy(PublicAccessType.BLOB, null); + } + + privateContainerClient = blobServiceClient.getBlobContainerClient(privateContainerName); + if (!privateContainerClient.exists()) { + privateContainerClient = blobServiceClient.createBlobContainer(privateContainerName); + } + } + + public ExternalProviderAzureStorageLatest() throws Exception { + super(); + init(); + } + + public ExternalProviderAzureStorageLatest(GXService providerService) throws Exception { + super(providerService); + init(); + } + + public String getName() { + return NAME; + } + + public void download(String externalFileName, String localFile, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(externalFileName, acl); + blobClient.downloadToFile(localFile, true); + } catch (Exception ex) { + handleAndLogException("Invalid URI or error downloading file", ex); + } + } + + private BlobClient getBlobClient(String fileName, ResourceAccessControlList acl) { + BlobClient blobClient; + if (isPrivateAcl(acl)) { + blobClient = privateContainerClient.getBlobClient(fileName); + } else { + blobClient = publicContainerClient.getBlobClient(fileName); + } + return blobClient; + } + + private boolean isPrivateAcl(ResourceAccessControlList acl) { + // If default ACL is private, use always private. + return this.defaultAcl == ResourceAccessControlList.Private || acl == ResourceAccessControlList.Private; + } + + public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(externalFileName, acl); + blobClient.uploadFromFile(localFile, true); + return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); + } catch (Exception ex) { + handleAndLogException("Error uploading file", ex); + return ""; + } + } + + public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { + //https://docs.azure.cn/en-us/storage/blobs/storage-blob-upload-java + try (ExternalProviderHelper.InputStreamWithLength streamInfo = + ExternalProviderHelper.getInputStreamContentLength(input)) { + BlockBlobClient blobClient = + getBlobClient(externalFileName, acl).getBlockBlobClient(); + // Set content type + String contentType = + (externalFileName.endsWith(".tmp") && + "application/octet-stream".equals(streamInfo.detectedContentType)) + ? "image/jpeg" + : streamInfo.detectedContentType; + BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(contentType); + blobClient.upload(streamInfo.inputStream, streamInfo.contentLength, true); + return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); + } + catch (Exception ex) { + handleAndLogException("Error uploading file", ex); + return ""; + } + } + + public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { + try { + if (exists(externalFileName, acl)) { + return getResourceUrl(externalFileName, acl, expirationMinutes); + } + } catch (Exception ex) { + handleAndLogException("Error getting file", ex); + return ""; + } + return ""; + } + + private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { + if (isPrivateAcl(acl)) { + return getPrivate(externalFileName, expirationMinutes); + } else { + BlobClient blobClient = publicContainerClient.getBlobClient(externalFileName); + return blobClient.getBlobUrl(); + } + } + + private String getPrivate(String externalFileName, int expirationMinutes) { + try { + BlobClient blobClient = privateContainerClient.getBlobClient(externalFileName); + expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; + OffsetDateTime expiryTime = OffsetDateTime.now().plusMinutes(expirationMinutes); + // Permissions (read) + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = + new BlobServiceSasSignatureValues(expiryTime, permission); + String sasToken; + if (!useManagedIdentity) { + sasToken = blobClient.generateSas(values); + } else { + BlobServiceClient blobServiceClient = privateContainerClient.getServiceClient(); + OffsetDateTime start = OffsetDateTime.now().minusMinutes(5); + //https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-user-delegation-sas-create-java?tabs=container + UserDelegationKey userDelegationKey = + blobServiceClient.getUserDelegationKey(start, expiryTime); + sasToken = blobClient.generateUserDelegationSas(values, userDelegationKey); + } + return blobClient.getBlobUrl() + "?" + sasToken; + } catch (Exception ex) { + handleAndLogException("Error getting private file", ex); + return ""; + } + } + + public void delete(String objectName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(objectName, acl); + blobClient.deleteIfExists(); + } catch (Exception ex) { + handleAndLogException("Error deleting file", ex); + } + } + + public String rename(String objectName, String newName, ResourceAccessControlList acl) { + String ret = copy(objectName, newName, acl); + delete(objectName, acl); + return ret; + } + + private String resolveObjectName(String urlOrObjectName, ResourceAccessControlList acl) { + String objectName = getObjectNameFromURL(urlOrObjectName); + return (objectName == null || objectName.length() == 0)? urlOrObjectName: objectName; + } + + public String copy(String objectName, String newName, ResourceAccessControlList acl) { + objectName = resolveObjectName(objectName, acl); + try { + BlobClient sourceBlob = getBlobClient(objectName, acl); + BlobClient targetBlob = getBlobClient(newName, acl); + + String sourceBlobUrl; + + if (useManagedIdentity) { + //Needs RBAC permissions: https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory + sourceBlobUrl = sourceBlob.getBlobUrl(); + } else { + if (isPrivateAcl(acl)) { + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = + new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusMinutes(5), + permission + ); + + sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); + } else { + sourceBlobUrl = sourceBlob.getBlobUrl(); + } + } + targetBlob.beginCopy(sourceBlobUrl, null); + return getResourceUrl(newName, acl, defaultExpirationMinutes); + } catch (Exception ex) { + handleAndLogException("Error copying file", ex); + return ""; + } + } + + public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) { + + objectUrl = objectUrl.replace(getUrl(), ""); + newName = tableName + "/" + fieldName + "/" + newName; + + try { + // Source is always in the private container + BlobClient sourceBlob = privateContainerClient.getBlobClient(objectUrl); + BlobClient targetBlob = getBlobClient(newName, acl); + + Map metadata = + createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName)); + targetBlob.setMetadata(metadata); + + String sourceBlobUrl; + if (useManagedIdentity) { + sourceBlobUrl = sourceBlob.getBlobUrl(); + } else { + + BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); + BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( + OffsetDateTime.now().plusMinutes(5), permission); + + sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); + } + + targetBlob.beginCopy(sourceBlobUrl, null); + return getResourceUrl(newName, acl, defaultExpirationMinutes); + + } catch (Exception ex) { + handleAndLogException("Error copying file", ex); + return ""; + } + } + + private HashMap createObjectMetadata(String table, String field, String name) { + HashMap metadata = new HashMap(); + metadata.put("Table", table); + metadata.put("Field", field); + metadata.put("KeyValue", name); + return metadata; + } + + public long getLength(String objectName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(objectName, acl); + BlobProperties properties = blobClient.getProperties(); + return properties.getBlobSize(); + } catch (Exception ex) { + handleAndLogException("Error getting file length", ex); + return 0; + } + } + + public Date getLastModified(String objectName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(objectName, acl); + BlobProperties properties = blobClient.getProperties(); + return Date.from(properties.getLastModified().toInstant()); + } catch (Exception ex) { + handleAndLogException("Error getting last modified date", ex); + return new Date(); + } + } + + public boolean exists(String objectName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(objectName, acl); + return blobClient.exists(); + } catch (Exception ex) { + handleAndLogException("Error checking if file exists", ex); + return false; + } + } + + public String getDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + if (existsDirectory(directoryName)) { + return publicContainerClient.getBlobContainerName() + StorageUtils.DELIMITER + directoryName; + } else { + return ""; + } + } + + public boolean existsDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Check if there are any blobs with this prefix + boolean exists = false; + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (!name.equals(directoryName)) { + // If we found any blob that isn't just the directory marker itself + exists = true; + break; + } + } + return exists; + } catch (Exception ex) { + handleAndLogException("Error checking if directory exists", ex); + return false; + } + } + + public void createDirectory(String directoryName) { + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + // Create a blob with empty content to mark the directory + BlobClient blobClient = publicContainerClient.getBlobClient(directoryName); + byte[] emptyContent = new byte[0]; + blobClient.upload(new ByteArrayInputStream(emptyContent), emptyContent.length, true); + } catch (Exception ex) { + handleAndLogException("Error creating directory", ex); + } + } + + public void deleteDirectory(String directoryName) { + ResourceAccessControlList acl = null; + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Delete all blobs in the directory + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName)) { + if (name.endsWith(StorageUtils.DELIMITER)) { + // This is a "subdirectory" + if (!name.equals(directoryName)) { + deleteDirectory(name); + } + } else { + // This is a file + delete(name, acl); + } + } + } + } catch (Exception ex) { + handleAndLogException("Error deleting directory", ex); + } + } + + public void renameDirectory(String directoryName, String newDirectoryName) { + ResourceAccessControlList acl = null; + if (!existsDirectory(newDirectoryName)) { + createDirectory(newDirectoryName); + } + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName); + try { + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Copy and rename all blobs in the directory + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName)) { + if (name.endsWith(StorageUtils.DELIMITER)) { + // This is a "subdirectory" + if (!name.equals(directoryName)) { + String subdirName = name.substring(directoryName.length()); + renameDirectory(name, newDirectoryName + subdirName); + } + } else { + // This is a file, rename it + String newName = name.replace(directoryName, newDirectoryName); + rename(name, newName, acl); + } + } + } + + // Delete the original directory + deleteDirectory(directoryName); + } catch (Exception ex) { + handleAndLogException("Error renaming directory", ex); + } + } + + public List getFiles(String directoryName, String filter) { + List files = new ArrayList(); + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Add all file names to the list + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName) && !name.endsWith(StorageUtils.DELIMITER)) { + // This is a file, add it to the list + files.add(name); + } + } + } catch (Exception ex) { + handleAndLogException("Error getting files", ex); + } + return files; + } + + public List getFiles(String directoryName) { + return getFiles(directoryName, ""); + } + + public List getSubDirectories(String directoryName) { + List directories = new ArrayList(); + directoryName = StorageUtils.normalizeDirectoryName(directoryName); + try { + // List all blobs with the directory prefix + ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); + + // Get all subdirectory names + Set dirSet = new HashSet(); + for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { + String name = blobItem.getName(); + if (name.startsWith(directoryName) && !name.equals(directoryName)) { + // Get the subdirectory name + String remainingPath = name.substring(directoryName.length()); + int slashIndex = remainingPath.indexOf(StorageUtils.DELIMITER); + + if (slashIndex >= 0) { + // This is a subdirectory or a file in a subdirectory + String subdirName = directoryName + remainingPath.substring(0, slashIndex + 1); + dirSet.add(subdirName); + } + } + } + directories.addAll(dirSet); + } catch (Exception ex) { + handleAndLogException("Error getting subdirectories", ex); + } + return directories; + } + + public InputStream getStream(String objectName, ResourceAccessControlList acl) { + try { + BlobClient blobClient = getBlobClient(objectName, acl); + return blobClient.openInputStream(); + } catch (Exception ex) { + handleAndLogException("Error getting stream", ex); + return null; + } + } + + public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) { + try { + // Extract error information from the SDK exceptions + String errorMessage = ex.getMessage(); + if (errorMessage != null) { + msg.setId("AzureError"); + msg.setDescription(errorMessage); + return true; + } + return false; + } catch (Exception e) { + return false; + } + } + + private String getUrl() { + return "https://" + account + ".blob.core.windows.net/"; + } + + public String getObjectNameFromURL(String url) { + String objectName = null; + String baseUrl = this.getUrl(); + String publicContainerUrl = String.format("%s%s/", baseUrl , publicContainerName); + String privateContainerUrl = String.format("%s%s/", baseUrl , privateContainerName); + if (url.startsWith(publicContainerUrl)) { + objectName = url.replace(publicContainerUrl, ""); + } + if (url.startsWith(privateContainerUrl)) { + objectName = url.replace(privateContainerUrl, ""); + } + return objectName; + } + + private void handleAndLogException(String message, Exception ex) { + if (ex instanceof BlobStorageException) { + logger.error("Azure Storage error: {} (Status: {}, Code: {})", + ((BlobStorageException) ex).getServiceMessage(), ((BlobStorageException) ex).getStatusCode(), ((BlobStorageException) ex).getErrorCode()); + } else if (ex instanceof ClientAuthenticationException) { + logger.error("Authentication error: {}", ex.getMessage()); + } else if (ex instanceof HttpRequestException) { + logger.error("Connection error: {}", ex.getMessage()); + } else if (ex instanceof URISyntaxException) { + logger.error("Invalid URI: {}", ex.getMessage()); + } else if (ex instanceof IOException) { + logger.error(message, ex); + } else { + logger.error("Unexpected storage error", ex); + } + } +} diff --git a/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java b/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java deleted file mode 100644 index 79ea143ae..000000000 --- a/gxcloudstorage-azureblob-legacy/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLegacy.java +++ /dev/null @@ -1,525 +0,0 @@ -package com.genexus.db.driver; - -import com.genexus.StructSdtMessages_Message; -import com.genexus.util.GXService; -import com.genexus.util.StorageUtils; -import com.microsoft.azure.storage.CloudStorageAccount; -import com.microsoft.azure.storage.StorageException; -import com.microsoft.azure.storage.blob.*; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; -import java.security.InvalidKeyException; -import java.util.*; - -public class ExternalProviderAzureStorageLegacy extends ExternalProviderBase implements ExternalProvider { - private static Logger logger = LogManager.getLogger(ExternalProviderAzureStorageLegacy.class); - - static final String NAME = "AZUREBS"; //Azure Blob Storage - - static final String ACCOUNT = "ACCOUNT_NAME"; - static final String ACCESS_KEY = "ACCESS_KEY"; - static final String PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; - static final String PRIVATE_CONTAINER = "PRIVATE_CONTAINER_NAME"; - - @Deprecated - static final String ACCOUNT_DEPRECATED = "ACCOUNT_NAME"; - @Deprecated - static final String KEY_DEPRECATED = "ACCESS_KEY"; - @Deprecated - static final String PUBLIC_CONTAINER_DEPRECATED = "PUBLIC_CONTAINER_NAME"; - @Deprecated - static final String PRIVATE_CONTAINER_DEPRECATED = "PRIVATE_CONTAINER_NAME"; - - private String account; - private String key; - private CloudBlobContainer publicContainer; - private CloudBlobContainer privateContainer; - private CloudBlobClient client; - - private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES; - - private String privateContainerName; - private String publicContainerName; - - - private void init() throws Exception { - try { - account = getEncryptedPropertyValue(ACCOUNT, ACCOUNT_DEPRECATED); - key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); - - CloudStorageAccount storageAccount = CloudStorageAccount.parse( - String.format("DefaultEndpointsProtocol=%1s;AccountName=%2s;AccountKey=%3s", "https", account, key)); - client = storageAccount.createCloudBlobClient(); - - String privateContainerNameValue = getEncryptedPropertyValue(PRIVATE_CONTAINER, PRIVATE_CONTAINER_DEPRECATED); - String publicContainerNameValue = getEncryptedPropertyValue(PUBLIC_CONTAINER, PUBLIC_CONTAINER_DEPRECATED); - - privateContainerName = privateContainerNameValue.toLowerCase(); - publicContainerName = publicContainerNameValue.toLowerCase(); - - publicContainer = client.getContainerReference(publicContainerName); - publicContainer.createIfNotExists(); - - privateContainer = client.getContainerReference(privateContainerName); - privateContainer.createIfNotExists(); - - BlobContainerPermissions permissions = new BlobContainerPermissions(); - permissions.setPublicAccess(BlobContainerPublicAccessType.BLOB); - publicContainer.uploadPermissions(permissions); - } catch (URISyntaxException ex) { - logger.error("Invalid URI", ex); - } catch (StorageException sex) { - logger.error(sex.getMessage()); - } catch (InvalidKeyException ikex) { - logger.error("Invalid keys", ikex); - } - } - - public ExternalProviderAzureStorageLegacy() throws Exception { - super(); - init(); - } - - public ExternalProviderAzureStorageLegacy(GXService providerService) throws Exception { - super(providerService); - init(); - } - - public String getName() { - return NAME; - } - - public void download(String externalFileName, String localFile, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.downloadToFile(localFile); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ioex) { - logger.error("Error downloading file", ioex); - } - } - - - private CloudBlockBlob getCloudBlockBlob(String fileName, ResourceAccessControlList acl) throws URISyntaxException, StorageException { - CloudBlockBlob blob; - if (isPrivateAcl(acl)) { - blob = privateContainer.getBlockBlobReference(fileName); - } else { - blob = publicContainer.getBlockBlobReference(fileName); - } - return blob; - } - - private boolean isPrivateAcl(ResourceAccessControlList acl) { - // If default ACL is private, use always private. - return this.defaultAcl == ResourceAccessControlList.Private || acl == ResourceAccessControlList.Private; - } - - public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.uploadFromFile(localFile); - return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ex) { - logger.error("Error uploading file", ex); - } - return ""; - } - - public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { - try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { - CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); - blob.getProperties().setContentType((externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) ? "image/jpeg" : streamInfo.detectedContentType); - try (BlobOutputStream blobOutputStream = blob.openOutputStream()) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = streamInfo.inputStream.read(buffer)) != -1) { - blobOutputStream.write(buffer, 0, bytesRead); - } - } - return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); - - } catch (URISyntaxException ex) { - logger.error("Invalid URI", ex); - return ""; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ex) { - logger.error("Error uploading file", ex); - return ""; - } - } - - public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { - try { - if (exists(externalFileName, acl)) { - return getResourceUrl(externalFileName, acl, expirationMinutes); - } - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (Exception ex) { - logger.error("Error getting file", ex); - return ""; - } - return ""; - } - - private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) throws URISyntaxException, StorageException { - if (isPrivateAcl(acl)) { - return getPrivate(externalFileName, expirationMinutes); - } else { - CloudBlockBlob blob = publicContainer.getBlockBlobReference(externalFileName); - return blob.getUri().toString(); - } - } - - private String getPrivate(String externalFileName, int expirationMinutes) { - try { - CloudBlockBlob blob = privateContainer.getBlockBlobReference(externalFileName); - SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); - policy.setPermissionsFromString("r"); - Calendar date = Calendar.getInstance(); - expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; - Date expire = new Date(date.getTimeInMillis() + (expirationMinutes * 60000)); - policy.setSharedAccessExpiryTime(expire); - return blob.getUri().toString() + "?" + blob.generateSharedAccessSignature(policy, null); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (Exception ex) { - logger.error("Error getting private file", ex); - } - return ""; - - } - - public void delete(String objectName, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.deleteIfExists(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public String rename(String objectName, String newName, ResourceAccessControlList acl) { - String ret = copy(objectName, newName, acl); - delete(objectName, acl); - return ret; - } - - private String resolveObjectName(String urlOrObjectName, ResourceAccessControlList acl) { - String objectName = getObjectNameFromURL(urlOrObjectName); - return (objectName == null || objectName.length() == 0)? urlOrObjectName: objectName; - } - - public String copy(String objectName, String newName, ResourceAccessControlList acl) { - objectName = resolveObjectName(objectName, acl); - try { - CloudBlockBlob sourceBlob = getCloudBlockBlob(objectName, acl); - CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); - targetBlob.startCopy(sourceBlob); - return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - return ""; - } - - public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) { - objectUrl = objectUrl.replace(getUrl(), ""); - newName = tableName + "/" + fieldName + "/" + newName; - try { - CloudBlockBlob sourceBlob = privateContainer.getBlockBlobReference(objectUrl); //Source will be always on the private container - CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); - targetBlob.setMetadata(createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName))); - targetBlob.startCopy(sourceBlob); - return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - return ""; - } - - private HashMap createObjectMetadata(String table, String field, String name) { - HashMap metadata = new HashMap(); - metadata.put("Table", table); - metadata.put("Field", field); - metadata.put("KeyValue", name); - return metadata; - } - - public long getLength(String objectName, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - return blob.getProperties().getLength(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - return 0; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public Date getLastModified(String objectName, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - return blob.getProperties().getLastModified(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - return new Date(); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public boolean exists(String objectName, ResourceAccessControlList acl) { - try { - return getCloudBlockBlob(objectName, acl).exists(); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - return false; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public String getDirectory(String directoryName) { - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - if (existsDirectory(directoryName)) { - return publicContainer.getName() + StorageUtils.DELIMITER + directoryName; - } else { - return ""; - } - } - - public boolean existsDirectory(String directoryName) { - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - return true; - } - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - itemName = itemName.substring(0, itemName.length() - 1); - if (!itemName.equals(directoryName)) { - return true; - } - } - } - return false; - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - return false; - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public void createDirectory(String directoryName) { - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - try { - CloudBlockBlob blob = publicContainer.getBlockBlobReference(directoryName); - blob.uploadFromByteArray(new byte[0], 0, 0); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } catch (IOException ioex) { - logger.error("Error uploading file", ioex); - } - } - - public void deleteDirectory(String directoryName) { - ResourceAccessControlList acl = null; - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - itemName = ((CloudPageBlob) item).getName(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - delete(itemName, acl); - } - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - if (!itemName.equals(directoryName)) { - deleteDirectory(itemName); - } - } - } - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public void renameDirectory(String directoryName, String newDirectoryName) { - ResourceAccessControlList acl = null; - if (!existsDirectory(newDirectoryName)) { - createDirectory(newDirectoryName); - } - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName); - try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - String itemName = ""; - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - itemName = ((CloudPageBlob) item).getName(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - rename(itemName, itemName.replace(directoryName, newDirectoryName), acl); - } - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - itemName = ((CloudBlobDirectory) item).getPrefix(); - } else if (item instanceof CloudBlockBlob) { - itemName = ((CloudBlockBlob) item).getName(); - } - renameDirectory(directoryName + StorageUtils.DELIMITER + itemName, newDirectoryName + StorageUtils.DELIMITER + itemName); - } - } - deleteDirectory(directoryName); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - public List getFiles(String directoryName, String filter) { - List files = new ArrayList(); - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - if (isFile(item)) { - if (item instanceof CloudPageBlob) { - files.add(((CloudPageBlob) item).getName()); - } else if (item instanceof CloudBlockBlob) { - files.add(((CloudBlockBlob) item).getName()); - } - } - } - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - return files; - } - - public List getFiles(String directoryName) { - return getFiles(directoryName, ""); - } - - public List getSubDirectories(String directoryName) { - List directories = new ArrayList(); - directoryName = StorageUtils.normalizeDirectoryName(directoryName); - try { - CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); - for (ListBlobItem item : directory.listBlobs()) { - if (isDirectory(item)) { - if (item instanceof CloudBlobDirectory) { - directories.add(((CloudBlobDirectory) item).getPrefix()); - } else if (item instanceof CloudBlockBlob) { - directories.add(((CloudBlockBlob) item).getName()); - } - } - } - directories.remove(directoryName); - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - return directories; - } - - public InputStream getStream(String objectName, ResourceAccessControlList acl) { - try { - CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); - blob.downloadAttributes(); - byte[] bytes = new byte[(int) blob.getProperties().getLength()]; - blob.downloadToByteArray(bytes, 0); - - InputStream stream = new ByteArrayInputStream(bytes); - return stream; - } catch (URISyntaxException ex) { - logger.error("Invalid URI ", ex.getMessage()); - } catch (StorageException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - return null; - } - - public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) { - try { - StorageException aex = (StorageException) ex.getCause(); - msg.setId(aex.getErrorCode()); - return true; - } catch (Exception e) { - return false; - } - } - - private boolean isDirectory(ListBlobItem item) { - return (item instanceof CloudBlobDirectory) || (item instanceof CloudBlockBlob && ((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); - } - - private boolean isFile(ListBlobItem item) { - return (item instanceof CloudPageBlob) || (item instanceof CloudBlockBlob && !((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); - } - - private String getUrl() { - return "https://" + account + ".blob.core.windows.net/"; - } - - public String getObjectNameFromURL(String url) { - String objectName = null; - String baseUrl = this.getUrl(); - String publicContainerUrl = String.format("%s%s/", baseUrl , publicContainerName); - String privateContainerUrl = String.format("%s%s/", baseUrl , privateContainerName); - if (url.startsWith(publicContainerUrl)) { - objectName = url.replace(publicContainerUrl, ""); - } - if (url.startsWith(privateContainerUrl)) { - objectName = url.replace(privateContainerUrl, ""); - } - return objectName; - } -} diff --git a/gxcloudstorage-azureblob/pom.xml b/gxcloudstorage-azureblob/pom.xml index 2d18c45eb..ca586d19a 100644 --- a/gxcloudstorage-azureblob/pom.xml +++ b/gxcloudstorage-azureblob/pom.xml @@ -26,14 +26,15 @@ ${project.version} - com.azure - azure-storage-blob - 12.32.0 - - - com.azure - azure-identity - 1.18.1 + com.microsoft.azure + azure-storage + 8.6.6 + + + com.microsoft.azure + azure-keyvault-core + + diff --git a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java index b07937b53..723ea9eca 100644 --- a/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java +++ b/gxcloudstorage-azureblob/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorage.java @@ -1,22 +1,11 @@ package com.genexus.db.driver; -import com.azure.core.exception.ClientAuthenticationException; -import com.azure.core.exception.HttpRequestException; -import com.azure.identity.DefaultAzureCredential; -import com.azure.identity.DefaultAzureCredentialBuilder; -import com.azure.storage.blob.BlobClient; -import com.azure.storage.blob.BlobContainerClient; -import com.azure.storage.blob.BlobServiceClient; -import com.azure.storage.blob.BlobServiceClientBuilder; -import com.azure.storage.blob.models.*; -import com.azure.storage.blob.sas.BlobSasPermission; -import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; -import com.azure.storage.blob.specialized.BlockBlobClient; -import com.azure.storage.common.StorageSharedKeyCredential; -import com.azure.storage.blob.options.BlobParallelUploadOptions; import com.genexus.StructSdtMessages_Message; import com.genexus.util.GXService; import com.genexus.util.StorageUtils; +import com.microsoft.azure.storage.CloudStorageAccount; +import com.microsoft.azure.storage.StorageException; +import com.microsoft.azure.storage.blob.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -24,7 +13,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; -import java.time.OffsetDateTime; +import java.security.InvalidKeyException; import java.util.*; public class ExternalProviderAzureStorage extends ExternalProviderBase implements ExternalProvider { @@ -37,8 +26,6 @@ public class ExternalProviderAzureStorage extends ExternalProviderBase implement static final String PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; static final String PRIVATE_CONTAINER = "PRIVATE_CONTAINER_NAME"; - private boolean useManagedIdentity; - @Deprecated static final String ACCOUNT_DEPRECATED = "ACCOUNT_NAME"; @Deprecated @@ -50,90 +37,46 @@ public class ExternalProviderAzureStorage extends ExternalProviderBase implement private String account; private String key; - - private BlobServiceClient blobServiceClient; - private BlobContainerClient publicContainerClient; - private BlobContainerClient privateContainerClient; + private CloudBlobContainer publicContainer; + private CloudBlobContainer privateContainer; + private CloudBlobClient client; private int defaultExpirationMinutes = DEFAULT_EXPIRATION_MINUTES; private String privateContainerName; private String publicContainerName; + private void init() throws Exception { try { account = getEncryptedPropertyValue(ACCOUNT, ACCOUNT_DEPRECATED); - } catch (Exception ex) { - logger.error("Error initializing Azure Storage: unable to get account", ex); - throw ex; - } - - try { key = getEncryptedPropertyValue(ACCESS_KEY, KEY_DEPRECATED); - if (key.isEmpty()) { - logger.info("ACCESS_KEY empty — using Managed Identity"); - useManagedIdentity = true; - } - } catch (Exception ex) { - if (key == null) { - logger.info("ACCESS_KEY null — using Managed Identity"); - useManagedIdentity = true; - } - } - try { + CloudStorageAccount storageAccount = CloudStorageAccount.parse( + String.format("DefaultEndpointsProtocol=%1s;AccountName=%2s;AccountKey=%3s", "https", account, key)); + client = storageAccount.createCloudBlobClient(); + String privateContainerNameValue = getEncryptedPropertyValue(PRIVATE_CONTAINER, PRIVATE_CONTAINER_DEPRECATED); String publicContainerNameValue = getEncryptedPropertyValue(PUBLIC_CONTAINER, PUBLIC_CONTAINER_DEPRECATED); privateContainerName = privateContainerNameValue.toLowerCase(); publicContainerName = publicContainerNameValue.toLowerCase(); - if (useManagedIdentity) { - initWithManagedIdentity(); - } else { - initWithAccountKey(); - } - } - catch (Exception ex) { - handleAndLogException("Initialization error", ex); - } - } - - private void initWithAccountKey() { - // Create BlobServiceClient with account key - StorageSharedKeyCredential credential = new StorageSharedKeyCredential(account, key); - blobServiceClient = new BlobServiceClientBuilder() - .endpoint(String.format("https://%s.blob.core.windows.net", account)) - .credential(credential) - .buildClient(); - - initContainerClients(); - } - - private void initWithManagedIdentity() { - // Create a DefaultAzureCredential - DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build(); - - // Create BlobServiceClient using the credential - blobServiceClient = new BlobServiceClientBuilder() - .endpoint(String.format("https://%s.blob.core.windows.net", account)) - .credential(credential) - .buildClient(); - - initContainerClients(); - } - - private void initContainerClients() { - // Create container clients and ensure containers exist - publicContainerClient = blobServiceClient.getBlobContainerClient(publicContainerName); - if (!publicContainerClient.exists()) { - publicContainerClient = blobServiceClient.createBlobContainer(publicContainerName); - publicContainerClient.setAccessPolicy(PublicAccessType.BLOB, null); - } - - privateContainerClient = blobServiceClient.getBlobContainerClient(privateContainerName); - if (!privateContainerClient.exists()) { - privateContainerClient = blobServiceClient.createBlobContainer(privateContainerName); + publicContainer = client.getContainerReference(publicContainerName); + publicContainer.createIfNotExists(); + + privateContainer = client.getContainerReference(privateContainerName); + privateContainer.createIfNotExists(); + + BlobContainerPermissions permissions = new BlobContainerPermissions(); + permissions.setPublicAccess(BlobContainerPublicAccessType.BLOB); + publicContainer.uploadPermissions(permissions); + } catch (URISyntaxException ex) { + logger.error("Invalid URI", ex); + } catch (StorageException sex) { + logger.error(sex.getMessage()); + } catch (InvalidKeyException ikex) { + logger.error("Invalid keys", ikex); } } @@ -153,21 +96,26 @@ public String getName() { public void download(String externalFileName, String localFile, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(externalFileName, acl); - blobClient.downloadToFile(localFile, true); - } catch (Exception ex) { - handleAndLogException("Invalid URI or error downloading file", ex); + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.downloadToFile(localFile); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ioex) { + logger.error("Error downloading file", ioex); } } - - private BlobClient getBlobClient(String fileName, ResourceAccessControlList acl) { - BlobClient blobClient; + + + private CloudBlockBlob getCloudBlockBlob(String fileName, ResourceAccessControlList acl) throws URISyntaxException, StorageException { + CloudBlockBlob blob; if (isPrivateAcl(acl)) { - blobClient = privateContainerClient.getBlobClient(fileName); + blob = privateContainer.getBlockBlobReference(fileName); } else { - blobClient = publicContainerClient.getBlobClient(fileName); + blob = publicContainer.getBlockBlobReference(fileName); } - return blobClient; + return blob; } private boolean isPrivateAcl(ResourceAccessControlList acl) { @@ -177,39 +125,39 @@ private boolean isPrivateAcl(ResourceAccessControlList acl) { public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(externalFileName, acl); - blobClient.uploadFromFile(localFile, true); + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.uploadFromFile(localFile); return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); - } catch (Exception ex) { - handleAndLogException("Error uploading file", ex); - return ""; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ex) { + logger.error("Error uploading file", ex); } + return ""; } public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { - //https://docs.azure.cn/en-us/storage/blobs/storage-blob-upload-java - try (ExternalProviderHelper.InputStreamWithLength streamInfo = - ExternalProviderHelper.getInputStreamContentLength(input)) { - - BlockBlobClient blobClient = - getBlobClient(externalFileName, acl).getBlockBlobClient(); - - // Set content type - String contentType = - (externalFileName.endsWith(".tmp") && - "application/octet-stream".equals(streamInfo.detectedContentType)) - ? "image/jpeg" - : streamInfo.detectedContentType; - - BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(contentType); - - // Upload with headers in one shot (equivalent to old behavior) - blobClient.upload(streamInfo.inputStream, streamInfo.contentLength, true); - + try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { + CloudBlockBlob blob = getCloudBlockBlob(externalFileName, acl); + blob.getProperties().setContentType((externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) ? "image/jpeg" : streamInfo.detectedContentType); + try (BlobOutputStream blobOutputStream = blob.openOutputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = streamInfo.inputStream.read(buffer)) != -1) { + blobOutputStream.write(buffer, 0, bytesRead); + } + } return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); - } - catch (Exception ex) { - handleAndLogException("Error uploading file", ex); + + } catch (URISyntaxException ex) { + logger.error("Invalid URI", ex); + return ""; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ex) { + logger.error("Error uploading file", ex); return ""; } } @@ -219,56 +167,51 @@ public String get(String externalFileName, ResourceAccessControlList acl, int ex if (exists(externalFileName, acl)) { return getResourceUrl(externalFileName, acl, expirationMinutes); } + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } catch (Exception ex) { - handleAndLogException("Error getting file", ex); + logger.error("Error getting file", ex); return ""; } return ""; } - private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { + private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) throws URISyntaxException, StorageException { if (isPrivateAcl(acl)) { return getPrivate(externalFileName, expirationMinutes); } else { - BlobClient blobClient = publicContainerClient.getBlobClient(externalFileName); - return blobClient.getBlobUrl(); + CloudBlockBlob blob = publicContainer.getBlockBlobReference(externalFileName); + return blob.getUri().toString(); } } private String getPrivate(String externalFileName, int expirationMinutes) { try { - BlobClient blobClient = privateContainerClient.getBlobClient(externalFileName); - + CloudBlockBlob blob = privateContainer.getBlockBlobReference(externalFileName); + SharedAccessBlobPolicy policy = new SharedAccessBlobPolicy(); + policy.setPermissionsFromString("r"); + Calendar date = Calendar.getInstance(); expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; - OffsetDateTime expiryTime = OffsetDateTime.now().plusMinutes(expirationMinutes); - // Permissions (read) - BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); - BlobServiceSasSignatureValues values = - new BlobServiceSasSignatureValues(expiryTime, permission); - String sasToken; - if (!useManagedIdentity) { - sasToken = blobClient.generateSas(values); - } else { - BlobServiceClient blobServiceClient = privateContainerClient.getServiceClient(); - OffsetDateTime start = OffsetDateTime.now().minusMinutes(1); - UserDelegationKey userDelegationKey = - blobServiceClient.getUserDelegationKey(start, expiryTime); - sasToken = blobClient.generateUserDelegationSas(values, userDelegationKey); - } - return blobClient.getBlobUrl() + "?" + sasToken; - + Date expire = new Date(date.getTimeInMillis() + (expirationMinutes * 60000)); + policy.setSharedAccessExpiryTime(expire); + return blob.getUri().toString() + "?" + blob.generateSharedAccessSignature(policy, null); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } catch (Exception ex) { - handleAndLogException("Error getting private file", ex); - return ""; + logger.error("Error getting private file", ex); } + return ""; + } public void delete(String objectName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(objectName, acl); - blobClient.deleteIfExists(); - } catch (Exception ex) { - handleAndLogException("Error deleting file", ex); + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.deleteIfExists(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } @@ -286,53 +229,33 @@ private String resolveObjectName(String urlOrObjectName, ResourceAccessControlLi public String copy(String objectName, String newName, ResourceAccessControlList acl) { objectName = resolveObjectName(objectName, acl); try { - BlobClient sourceBlob = getBlobClient(objectName, acl); - BlobClient targetBlob = getBlobClient(newName, acl); - - // Get source URL with SAS if it's private - String sourceBlobUrl; - if (isPrivateAcl(acl)) { - BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); - BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( - OffsetDateTime.now().plusMinutes(5), permission); - sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); - } else { - sourceBlobUrl = sourceBlob.getBlobUrl(); - } - - // Start the copy operation - targetBlob.beginCopy(sourceBlobUrl, null); + CloudBlockBlob sourceBlob = getCloudBlockBlob(objectName, acl); + CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); + targetBlob.startCopy(sourceBlob); return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (Exception ex) { - handleAndLogException("Error copying file", ex); - return ""; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } + return ""; } public String copy(String objectUrl, String newName, String tableName, String fieldName, ResourceAccessControlList acl) { objectUrl = objectUrl.replace(getUrl(), ""); newName = tableName + "/" + fieldName + "/" + newName; try { - BlobClient sourceBlob = privateContainerClient.getBlobClient(objectUrl); - BlobClient targetBlob = getBlobClient(newName, acl); - - // Set metadata - Map metadata = createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName)); - targetBlob.setMetadata(metadata); - - // Get source URL with SAS for private access - BlobSasPermission permission = new BlobSasPermission().setReadPermission(true); - BlobServiceSasSignatureValues values = new BlobServiceSasSignatureValues( - OffsetDateTime.now().plusMinutes(5), permission); - String sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); - - // Start the copy operation - targetBlob.beginCopy(sourceBlobUrl, null); + CloudBlockBlob sourceBlob = privateContainer.getBlockBlobReference(objectUrl); //Source will be always on the private container + CloudBlockBlob targetBlob = getCloudBlockBlob(newName, acl); + targetBlob.setMetadata(createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName))); + targetBlob.startCopy(sourceBlob); return getResourceUrl(newName, acl, defaultExpirationMinutes); - } catch (Exception ex) { - handleAndLogException("Error copying file", ex); - return ""; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } + return ""; } private HashMap createObjectMetadata(String table, String field, String name) { @@ -345,40 +268,45 @@ private HashMap createObjectMetadata(String table, String field, public long getLength(String objectName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(objectName, acl); - BlobProperties properties = blobClient.getProperties(); - return properties.getBlobSize(); - } catch (Exception ex) { - handleAndLogException("Error getting file length", ex); + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + return blob.getProperties().getLength(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); return 0; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } public Date getLastModified(String objectName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(objectName, acl); - BlobProperties properties = blobClient.getProperties(); - return Date.from(properties.getLastModified().toInstant()); - } catch (Exception ex) { - handleAndLogException("Error getting last modified date", ex); + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + return blob.getProperties().getLastModified(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); return new Date(); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } public boolean exists(String objectName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(objectName, acl); - return blobClient.exists(); - } catch (Exception ex) { - handleAndLogException("Error checking if file exists", ex); + return getCloudBlockBlob(objectName, acl).exists(); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); return false; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } public String getDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); if (existsDirectory(directoryName)) { - return publicContainerClient.getBlobContainerName() + StorageUtils.DELIMITER + directoryName; + return publicContainer.getName() + StorageUtils.DELIMITER + directoryName; } else { return ""; } @@ -387,35 +315,40 @@ public String getDirectory(String directoryName) { public boolean existsDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - // List all blobs with the directory prefix - ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - - // Check if there are any blobs with this prefix - boolean exists = false; - for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { - String name = blobItem.getName(); - if (!name.equals(directoryName)) { - // If we found any blob that isn't just the directory marker itself - exists = true; - break; + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + return true; + } + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + itemName = itemName.substring(0, itemName.length() - 1); + if (!itemName.equals(directoryName)) { + return true; + } } } - return exists; - } catch (Exception ex) { - handleAndLogException("Error checking if directory exists", ex); return false; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + return false; + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } public void createDirectory(String directoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - // Create a blob with empty content to mark the directory - BlobClient blobClient = publicContainerClient.getBlobClient(directoryName); - byte[] emptyContent = new byte[0]; - blobClient.upload(new ByteArrayInputStream(emptyContent), emptyContent.length, true); - } catch (Exception ex) { - handleAndLogException("Error creating directory", ex); + CloudBlockBlob blob = publicContainer.getBlockBlobReference(directoryName); + blob.uploadFromByteArray(new byte[0], 0, 0); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } catch (IOException ioex) { + logger.error("Error uploading file", ioex); } } @@ -423,26 +356,32 @@ public void deleteDirectory(String directoryName) { ResourceAccessControlList acl = null; directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - // List all blobs with the directory prefix - ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - - // Delete all blobs in the directory - for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { - String name = blobItem.getName(); - if (name.startsWith(directoryName)) { - if (name.endsWith(StorageUtils.DELIMITER)) { - // This is a "subdirectory" - if (!name.equals(directoryName)) { - deleteDirectory(name); - } - } else { - // This is a file - delete(name, acl); + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + itemName = ((CloudPageBlob) item).getName(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + delete(itemName, acl); + } + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + if (!itemName.equals(directoryName)) { + deleteDirectory(itemName); } } } - } catch (Exception ex) { - handleAndLogException("Error deleting directory", ex); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } @@ -454,31 +393,31 @@ public void renameDirectory(String directoryName, String newDirectoryName) { directoryName = StorageUtils.normalizeDirectoryName(directoryName); newDirectoryName = StorageUtils.normalizeDirectoryName(newDirectoryName); try { - // List all blobs with the directory prefix - ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - - // Copy and rename all blobs in the directory - for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { - String name = blobItem.getName(); - if (name.startsWith(directoryName)) { - if (name.endsWith(StorageUtils.DELIMITER)) { - // This is a "subdirectory" - if (!name.equals(directoryName)) { - String subdirName = name.substring(directoryName.length()); - renameDirectory(name, newDirectoryName + subdirName); - } - } else { - // This is a file, rename it - String newName = name.replace(directoryName, newDirectoryName); - rename(name, newName, acl); + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + String itemName = ""; + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + itemName = ((CloudPageBlob) item).getName(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); } + rename(itemName, itemName.replace(directoryName, newDirectoryName), acl); + } + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + itemName = ((CloudBlobDirectory) item).getPrefix(); + } else if (item instanceof CloudBlockBlob) { + itemName = ((CloudBlockBlob) item).getName(); + } + renameDirectory(directoryName + StorageUtils.DELIMITER + itemName, newDirectoryName + StorageUtils.DELIMITER + itemName); } } - - // Delete the original directory deleteDirectory(directoryName); - } catch (Exception ex) { - handleAndLogException("Error renaming directory", ex); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } } @@ -486,19 +425,20 @@ public List getFiles(String directoryName, String filter) { List files = new ArrayList(); directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - // List all blobs with the directory prefix - ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - - // Add all file names to the list - for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { - String name = blobItem.getName(); - if (name.startsWith(directoryName) && !name.endsWith(StorageUtils.DELIMITER)) { - // This is a file, add it to the list - files.add(name); + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + if (isFile(item)) { + if (item instanceof CloudPageBlob) { + files.add(((CloudPageBlob) item).getName()); + } else if (item instanceof CloudBlockBlob) { + files.add(((CloudBlockBlob) item).getName()); + } } } - } catch (Exception ex) { - handleAndLogException("Error getting files", ex); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } return files; } @@ -511,57 +451,60 @@ public List getSubDirectories(String directoryName) { List directories = new ArrayList(); directoryName = StorageUtils.normalizeDirectoryName(directoryName); try { - // List all blobs with the directory prefix - ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - - // Get all subdirectory names - Set dirSet = new HashSet(); - for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { - String name = blobItem.getName(); - if (name.startsWith(directoryName) && !name.equals(directoryName)) { - // Get the subdirectory name - String remainingPath = name.substring(directoryName.length()); - int slashIndex = remainingPath.indexOf(StorageUtils.DELIMITER); - - if (slashIndex >= 0) { - // This is a subdirectory or a file in a subdirectory - String subdirName = directoryName + remainingPath.substring(0, slashIndex + 1); - dirSet.add(subdirName); + CloudBlobDirectory directory = publicContainer.getDirectoryReference(directoryName); + for (ListBlobItem item : directory.listBlobs()) { + if (isDirectory(item)) { + if (item instanceof CloudBlobDirectory) { + directories.add(((CloudBlobDirectory) item).getPrefix()); + } else if (item instanceof CloudBlockBlob) { + directories.add(((CloudBlockBlob) item).getName()); } } } - directories.addAll(dirSet); - } catch (Exception ex) { - handleAndLogException("Error getting subdirectories", ex); + directories.remove(directoryName); + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } return directories; } public InputStream getStream(String objectName, ResourceAccessControlList acl) { try { - BlobClient blobClient = getBlobClient(objectName, acl); - return blobClient.openInputStream(); - } catch (Exception ex) { - handleAndLogException("Error getting stream", ex); - return null; + CloudBlockBlob blob = getCloudBlockBlob(objectName, acl); + blob.downloadAttributes(); + byte[] bytes = new byte[(int) blob.getProperties().getLength()]; + blob.downloadToByteArray(bytes, 0); + + InputStream stream = new ByteArrayInputStream(bytes); + return stream; + } catch (URISyntaxException ex) { + logger.error("Invalid URI ", ex.getMessage()); + } catch (StorageException ex) { + throw new RuntimeException(ex.getMessage(), ex); } + return null; } public boolean getMessageFromException(Exception ex, StructSdtMessages_Message msg) { try { - // Extract error information from the SDK exceptions - String errorMessage = ex.getMessage(); - if (errorMessage != null) { - msg.setId("AzureError"); - msg.setDescription(errorMessage); - return true; - } - return false; + StorageException aex = (StorageException) ex.getCause(); + msg.setId(aex.getErrorCode()); + return true; } catch (Exception e) { return false; } } + private boolean isDirectory(ListBlobItem item) { + return (item instanceof CloudBlobDirectory) || (item instanceof CloudBlockBlob && ((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); + } + + private boolean isFile(ListBlobItem item) { + return (item instanceof CloudPageBlob) || (item instanceof CloudBlockBlob && !((CloudBlockBlob) item).getName().endsWith(StorageUtils.DELIMITER)); + } + private String getUrl() { return "https://" + account + ".blob.core.windows.net/"; } @@ -579,21 +522,4 @@ public String getObjectNameFromURL(String url) { } return objectName; } - - private void handleAndLogException(String message, Exception ex) { - if (ex instanceof BlobStorageException) { - logger.error("Azure Storage error: {} (Status: {}, Code: {})", - ((BlobStorageException) ex).getServiceMessage(), ((BlobStorageException) ex).getStatusCode(), ((BlobStorageException) ex).getErrorCode()); - } else if (ex instanceof ClientAuthenticationException) { - logger.error("Authentication error: {}", ex.getMessage()); - } else if (ex instanceof HttpRequestException) { - logger.error("Connection error: {}", ex.getMessage()); - } else if (ex instanceof URISyntaxException) { - logger.error("Invalid URI: {}", ex.getMessage()); - } else if (ex instanceof IOException) { - logger.error(message, ex); - } else { - logger.error("Unexpected storage error", ex); - } - } } diff --git a/pom.xml b/pom.xml index d03bdd72e..a988bcb59 100644 --- a/pom.xml +++ b/pom.xml @@ -127,7 +127,7 @@ gxftps gamutils gamtotp - gxcloudstorage-azureblob-legacy + gxcloudstorage-azureblob-latest From 12972d9dfb9c5b44908352fbf2f52bcd89bae22e Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Thu, 20 Nov 2025 16:18:19 -0300 Subject: [PATCH 4/8] Azure SDK v12.x encodes URI special characters. Decode before getting the file name. --- .../ExternalProviderAzureStorageLatest.java | 35 ++++++++++++------- java/src/main/java/com/genexus/GXDbFile.java | 19 ++++++++++ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java index a3a4c9a99..fb7b0b49a 100644 --- a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -2,6 +2,7 @@ import com.azure.core.exception.ClientAuthenticationException; import com.azure.core.exception.HttpRequestException; +import com.azure.core.util.Context; import com.azure.identity.DefaultAzureCredential; import com.azure.identity.DefaultAzureCredentialBuilder; import com.azure.storage.blob.BlobClient; @@ -189,17 +190,30 @@ public String upload(String localFile, String externalFileName, ResourceAccessCo public String upload(String externalFileName, InputStream input, ResourceAccessControlList acl) { //https://docs.azure.cn/en-us/storage/blobs/storage-blob-upload-java try (ExternalProviderHelper.InputStreamWithLength streamInfo = - ExternalProviderHelper.getInputStreamContentLength(input)) { + ExternalProviderHelper.getInputStreamContentLength(input)) { + BlockBlobClient blobClient = - getBlobClient(externalFileName, acl).getBlockBlobClient(); - // Set content type + getBlobClient(externalFileName, acl).getBlockBlobClient(); + String contentType = - (externalFileName.endsWith(".tmp") && - "application/octet-stream".equals(streamInfo.detectedContentType)) - ? "image/jpeg" - : streamInfo.detectedContentType; + (externalFileName.endsWith(".tmp") && + "application/octet-stream".equals(streamInfo.detectedContentType)) + ? "image/jpeg" + : streamInfo.detectedContentType; + BlobHttpHeaders headers = new BlobHttpHeaders().setContentType(contentType); - blobClient.upload(streamInfo.inputStream, streamInfo.contentLength, true); + blobClient.uploadWithResponse( + streamInfo.inputStream, + streamInfo.contentLength, + headers, + null, + null, + null, + null, + null, + Context.NONE + ); + return getResourceUrl(externalFileName, acl, DEFAULT_EXPIRATION_MINUTES); } catch (Exception ex) { @@ -281,9 +295,7 @@ public String copy(String objectName, String newName, ResourceAccessControlList try { BlobClient sourceBlob = getBlobClient(objectName, acl); BlobClient targetBlob = getBlobClient(newName, acl); - String sourceBlobUrl; - if (useManagedIdentity) { //Needs RBAC permissions: https://learn.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory sourceBlobUrl = sourceBlob.getBlobUrl(); @@ -334,7 +346,6 @@ public String copy(String objectUrl, String newName, String tableName, String fi sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); } - targetBlob.beginCopy(sourceBlobUrl, null); return getResourceUrl(newName, acl, defaultExpirationMinutes); @@ -398,7 +409,6 @@ public boolean existsDirectory(String directoryName) { try { // List all blobs with the directory prefix ListBlobsOptions options = new ListBlobsOptions().setPrefix(directoryName); - // Check if there are any blobs with this prefix boolean exists = false; for (BlobItem blobItem : publicContainerClient.listBlobs(options, null)) { @@ -483,7 +493,6 @@ public void renameDirectory(String directoryName, String newDirectoryName) { } } } - // Delete the original directory deleteDirectory(directoryName); } catch (Exception ex) { diff --git a/java/src/main/java/com/genexus/GXDbFile.java b/java/src/main/java/com/genexus/GXDbFile.java index b93ca607e..7e2291638 100644 --- a/java/src/main/java/com/genexus/GXDbFile.java +++ b/java/src/main/java/com/genexus/GXDbFile.java @@ -5,6 +5,8 @@ import com.genexus.util.GXServices; import java.io.File; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +28,7 @@ public static String getFileName(String uri) { try { + uri = safeDecodeUrl(uri); return CommonUtil.getFileName(uri); } catch (Exception e) @@ -235,4 +238,20 @@ public static String pathToUrl(String path, boolean forceAbsPath) return pathToUrl(path, webContext, forceAbsPath); } + private static String safeDecodeUrl(String uri) { + if (uri == null || uri.isEmpty()) { + return uri; + } + boolean hasEncodedSegments = + uri.matches(".*%[0-9A-Fa-f]{2}.*"); + + if (!hasEncodedSegments) { + return uri; + } + try { + return URLDecoder.decode(uri); + } catch (IllegalArgumentException e) { + return uri; + } + } } \ No newline at end of file From 7f2eaf04e854ec74ec28bf0163d693be200878af Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Thu, 20 Nov 2025 22:26:42 -0300 Subject: [PATCH 5/8] New Java SDK for Azure encodes the URL when it gets a BLOB Url. Decode it only for public containers. --- .../ExternalProviderAzureStorageLatest.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java index fb7b0b49a..c1e4639e0 100644 --- a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -239,7 +239,14 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList return getPrivate(externalFileName, expirationMinutes); } else { BlobClient blobClient = publicContainerClient.getBlobClient(externalFileName); - return blobClient.getBlobUrl(); + //getBlobClient method returns URL encoded + //https://azuresdkdocs.z19.web.core.windows.net/java/azure-storage-blob/12.30.0/com/azure/storage/blob/BlobContainerClient.html#getBlobClient(java.lang.String) + ////https://github.com/Azure/azure-sdk-for-java/issues/21610 + + String url = blobClient.getBlobUrl(); + + //Decode only when not SAS + return safeDecodeUrl(url); } } @@ -597,7 +604,18 @@ public String getObjectNameFromURL(String url) { } return objectName; } - + + private static String safeDecodeUrl(String uri) { + if (uri == null || uri.isEmpty()) return uri; + boolean hasEncodedSegments = uri.matches(".*%[0-9A-Fa-f]{2}.*"); + if (!hasEncodedSegments) return uri; + try { + return java.net.URLDecoder.decode(uri); + } catch (IllegalArgumentException e) { + return uri; + } + } + private void handleAndLogException(String message, Exception ex) { if (ex instanceof BlobStorageException) { logger.error("Azure Storage error: {} (Status: {}, Code: {})", From 7a238ad6db82d9aa49391b7e3ddcaf8beb232924 Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Fri, 21 Nov 2025 19:53:25 -0300 Subject: [PATCH 6/8] Fix error in saving image uri to database in web transactions. --- .../genexus/db/driver/ExternalProviderAzureStorageLatest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java index c1e4639e0..160531358 100644 --- a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -340,7 +340,6 @@ public String copy(String objectUrl, String newName, String tableName, String fi Map metadata = createObjectMetadata(tableName, fieldName, StorageUtils.encodeName(newName)); - targetBlob.setMetadata(metadata); String sourceBlobUrl; if (useManagedIdentity) { @@ -354,6 +353,7 @@ public String copy(String objectUrl, String newName, String tableName, String fi sourceBlobUrl = sourceBlob.getBlobUrl() + "?" + sourceBlob.generateSas(values); } targetBlob.beginCopy(sourceBlobUrl, null); + targetBlob.setMetadata(metadata); return getResourceUrl(newName, acl, defaultExpirationMinutes); } catch (Exception ex) { From 7c853f1c9bf8982de1e3d4e733b584b1a90647c6 Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Sat, 22 Nov 2025 10:08:16 -0300 Subject: [PATCH 7/8] Defensive code add as externalFileName may have a leading / and this causes a double / at blob uri --- .../ExternalProviderAzureStorageLatest.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java index 160531358..7d9c8ee6e 100644 --- a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -14,7 +14,6 @@ import com.azure.storage.blob.sas.BlobServiceSasSignatureValues; import com.azure.storage.blob.specialized.BlockBlobClient; import com.azure.storage.common.StorageSharedKeyCredential; -import com.azure.storage.blob.options.BlobParallelUploadOptions; import com.genexus.StructSdtMessages_Message; import com.genexus.util.GXService; import com.genexus.util.StorageUtils; @@ -178,6 +177,7 @@ private boolean isPrivateAcl(ResourceAccessControlList acl) { public String upload(String localFile, String externalFileName, ResourceAccessControlList acl) { try { + externalFileName = getExternalFileName(externalFileName); BlobClient blobClient = getBlobClient(externalFileName, acl); blobClient.uploadFromFile(localFile, true); return getResourceUrl(externalFileName, acl, defaultExpirationMinutes); @@ -191,10 +191,9 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC //https://docs.azure.cn/en-us/storage/blobs/storage-blob-upload-java try (ExternalProviderHelper.InputStreamWithLength streamInfo = ExternalProviderHelper.getInputStreamContentLength(input)) { - + externalFileName = getExternalFileName(externalFileName); BlockBlobClient blobClient = getBlobClient(externalFileName, acl).getBlockBlobClient(); - String contentType = (externalFileName.endsWith(".tmp") && "application/octet-stream".equals(streamInfo.detectedContentType)) @@ -224,6 +223,7 @@ public String upload(String externalFileName, InputStream input, ResourceAccessC public String get(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { try { + externalFileName = getExternalFileName(externalFileName); if (exists(externalFileName, acl)) { return getResourceUrl(externalFileName, acl, expirationMinutes); } @@ -235,6 +235,8 @@ public String get(String externalFileName, ResourceAccessControlList acl, int ex } private String getResourceUrl(String externalFileName, ResourceAccessControlList acl, int expirationMinutes) { + + externalFileName = getExternalFileName(externalFileName); if (isPrivateAcl(acl)) { return getPrivate(externalFileName, expirationMinutes); } else { @@ -244,14 +246,14 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList ////https://github.com/Azure/azure-sdk-for-java/issues/21610 String url = blobClient.getBlobUrl(); - - //Decode only when not SAS - return safeDecodeUrl(url); + return url; + //return safeDecodeUrl(url); } } private String getPrivate(String externalFileName, int expirationMinutes) { try { + externalFileName = getExternalFileName(externalFileName); BlobClient blobClient = privateContainerClient.getBlobClient(externalFileName); expirationMinutes = expirationMinutes > 0 ? expirationMinutes : defaultExpirationMinutes; OffsetDateTime expiryTime = OffsetDateTime.now().plusMinutes(expirationMinutes); @@ -616,6 +618,15 @@ private static String safeDecodeUrl(String uri) { } } + private String getExternalFileName(String externalFileName) + { + //Defensive code, as externalFileName may have a leading / and this causes a double / at blob uri + //The latest Azure SDK is strict at uri format and encodes special characters + if (externalFileName == "") + return externalFileName; + return externalFileName.startsWith("/") ? externalFileName.substring(1) : externalFileName; + } + private void handleAndLogException(String message, Exception ex) { if (ex instanceof BlobStorageException) { logger.error("Azure Storage error: {} (Status: {}, Code: {})", From 4715c4ced40612fdf7f95ce79ef0649086d88f36 Mon Sep 17 00:00:00 2001 From: Sabrina Juarez Garcia Date: Sat, 22 Nov 2025 10:12:57 -0300 Subject: [PATCH 8/8] getBlobClient SDK method returns URL encoded Do not decode URL as the recommendation is to leave it as is to be passed to other methods --- .../ExternalProviderAzureStorageLatest.java | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java index 7d9c8ee6e..21e732638 100644 --- a/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java +++ b/gxcloudstorage-azureblob-latest/src/main/java/com/genexus/db/driver/ExternalProviderAzureStorageLatest.java @@ -244,10 +244,7 @@ private String getResourceUrl(String externalFileName, ResourceAccessControlList //getBlobClient method returns URL encoded //https://azuresdkdocs.z19.web.core.windows.net/java/azure-storage-blob/12.30.0/com/azure/storage/blob/BlobContainerClient.html#getBlobClient(java.lang.String) ////https://github.com/Azure/azure-sdk-for-java/issues/21610 - - String url = blobClient.getBlobUrl(); - return url; - //return safeDecodeUrl(url); + return blobClient.getBlobUrl(); } } @@ -607,17 +604,6 @@ public String getObjectNameFromURL(String url) { return objectName; } - private static String safeDecodeUrl(String uri) { - if (uri == null || uri.isEmpty()) return uri; - boolean hasEncodedSegments = uri.matches(".*%[0-9A-Fa-f]{2}.*"); - if (!hasEncodedSegments) return uri; - try { - return java.net.URLDecoder.decode(uri); - } catch (IllegalArgumentException e) { - return uri; - } - } - private String getExternalFileName(String externalFileName) { //Defensive code, as externalFileName may have a leading / and this causes a double / at blob uri