diff --git a/.github/workflows/External-Storage-Tests.yml b/.github/workflows/External-Storage-Tests.yml new file mode 100644 index 000000000..fdd2ecc3c --- /dev/null +++ b/.github/workflows/External-Storage-Tests.yml @@ -0,0 +1,100 @@ +name: External Storage Tests + +on: + pull_request: + branches: + - 'master' + - 'release-*' + push: + branches: + - 'master' + - 'beta' + - 'release-*' + +jobs: + build: + env: + GIT_REF: ${{ github.ref }} + GIT_SHA: ${{ github.sha }} + Configuration: Release + SolutionFile: dotnet\DotNetStandardClasses.sln + + runs-on: windows-latest + environment: external-storage-tests + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install .NET Core 3.1 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Install .NET 6 + uses: actions/setup-dotnet@v1.7.2 + with: + dotnet-version: '6.0.100-preview.3.21202.5' + + - uses: actions/setup-dotnet@v1 + with: + source-url: https://nuget.pkg.github.com/genexuslabs/index.json + env: + NUGET_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Calculate environment variables + run: | + $IsPrerelease = !($Env:GIT_REF -match 'release-[0-9]+(?:\.[0-9]+)?$') + echo "IsPrerelease=$IsPrerelease" >> $env:GITHUB_ENV + + $COMMIT_NUMBER = @($(git rev-list --count origin/master..), $(git rev-list --count HEAD))[$IsPrerelease] + + echo "COMMIT_NUMBER=$COMMIT_NUMBER" >> $env:GITHUB_ENV + + - name: Calculate package version + env: + PackageVersionString: ./.github/generatePackageVersion.ps1 + run: | + $NuGetPackageVersion = & "$Env:PackageVersionString" + + Write-Output "Packge version to be used: $NuGetPackageVersion" + + echo "NuGetPackageVersion=$NuGetPackageVersion" >> $env:GITHUB_ENV + + - name: Restore packages + run: dotnet restore $Env:SolutionFile + + - name: Build + run: dotnet build $Env:SolutionFile --no-restore --configuration $Env:Configuration + + - name: Test External Storage + run: | + $Env:AWSS3_TEST_ENABLED="true" + $Env:STORAGE_AWSS3_ACCESS_KEY="${{ secrets.AWSS3_ACCESS_KEY }}" + $Env:STORAGE_AWSS3_SECRET_KEY="${{ secrets.AWSS3_SECRET_KEY }}" + $Env:STORAGE_AWSS3_BUCKET_NAME="genexus-s3-test" + $Env:STORAGE_AWSS3_FOLDER_NAME="gxclasses" + $Env:STORAGE_AWSS3_REGION="us-east-1" + $Env:IBMCOS_TEST_ENABLED="true" + $Env:STORAGE_IBMCOS_ACCESS_KEY="${{ secrets.IBMCOS_ACCESS_KEY }}" + $Env:STORAGE_IBMCOS_SECRET_KEY="${{ secrets.IBMCOS_SECRET_KEY }}" + $Env:STORAGE_IBMCOS_BUCKET_NAME="gxclasses-unit-tests" + $Env:STORAGE_IBMCOS_FOLDER_NAME="tests" + $Env:STORAGE_IBMCOS_REGION="us-south" + $Env:AZUREBS_TEST_ENABLED="true" + $Env:STORAGE_AZUREBS_ACCESS_KEY="${{ secrets.AZUREBS_ACCESS_KEY }}" + $Env:STORAGE_AZUREBS_ACCOUNT_NAME="${{ secrets.AZUREBS_ACCOUNT_NAME }}" + $Env:STORAGE_AZUREBS_FOLDER_NAME="tests" + $Env:STORAGE_AZUREBS_PUBLIC_CONTAINER_NAME="contluispublic" + $Env:STORAGE_AZUREBS_PRIVATE_CONTAINER_NAME="contluisprivate" + $Env:GOOGLECS_TEST_ENABLED="true" + $Env:STORAGE_GOOGLECS_KEY='${{ secrets.GOOGLECS_KEY }}' + $Env:STORAGE_GOOGLECS_PROJECT_ID="gxjavacloudstorageunittests" + $Env:STORAGE_GOOGLECS_BUCKET_NAME="javaclasses-unittests" + $Env:STORAGE_GOOGLECS_FOLDER_NAME="gxclasses" + $Env:STORAGE_GOOGLECS_APPLICATION_NAME="gxjavacloudstorageunittests" + + dotnet test $Env:SolutionFile --no-restore --no-build --configuration $Env:Configuration + diff --git a/dotnet/DotNetStandardClasses.sln b/dotnet/DotNetStandardClasses.sln index b52cfbd94..d754b07fd 100644 --- a/dotnet/DotNetStandardClasses.sln +++ b/dotnet/DotNetStandardClasses.sln @@ -65,8 +65,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GXAmazonS3", "src\dotnetfra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GXAzureStorage", "src\dotnetframework\Providers\Storage\GXAzureStorage\GXAzureStorage.csproj", "{F6372249-AF37-4455-B572-5BDF8DE2ACC8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GXBluemix", "src\dotnetframework\Providers\Storage\GXBluemix\GXBluemix.csproj", "{FCA5536D-542E-480E-92B0-316E0290553B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GXGoogleCloud", "src\dotnetframework\Providers\Storage\GXGoogleCloud\GXGoogleCloud.csproj", "{E9072D95-D116-4D4B-B981-46146BCDE052}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GXOpenStack", "src\dotnetframework\Providers\Storage\GXOpenStack\GXOpenStack.csproj", "{64E958D8-CE7B-482D-B339-3B52E26CA4AC}" @@ -241,10 +239,6 @@ Global {F6372249-AF37-4455-B572-5BDF8DE2ACC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {F6372249-AF37-4455-B572-5BDF8DE2ACC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {F6372249-AF37-4455-B572-5BDF8DE2ACC8}.Release|Any CPU.Build.0 = Release|Any CPU - {FCA5536D-542E-480E-92B0-316E0290553B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FCA5536D-542E-480E-92B0-316E0290553B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FCA5536D-542E-480E-92B0-316E0290553B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FCA5536D-542E-480E-92B0-316E0290553B}.Release|Any CPU.Build.0 = Release|Any CPU {E9072D95-D116-4D4B-B981-46146BCDE052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E9072D95-D116-4D4B-B981-46146BCDE052}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9072D95-D116-4D4B-B981-46146BCDE052}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -378,7 +372,6 @@ Global {F82842DA-F15E-49C5-993E-4C269818FF1F} = {F900A4AD-7249-41B4-B918-CB9E8C73747C} {3701565F-1A95-4592-B90D-291CCDE5505D} = {F82842DA-F15E-49C5-993E-4C269818FF1F} {F6372249-AF37-4455-B572-5BDF8DE2ACC8} = {F82842DA-F15E-49C5-993E-4C269818FF1F} - {FCA5536D-542E-480E-92B0-316E0290553B} = {F82842DA-F15E-49C5-993E-4C269818FF1F} {E9072D95-D116-4D4B-B981-46146BCDE052} = {F82842DA-F15E-49C5-993E-4C269818FF1F} {64E958D8-CE7B-482D-B339-3B52E26CA4AC} = {F82842DA-F15E-49C5-993E-4C269818FF1F} {A14C2C2C-ACE3-4712-A527-E4E5F02729FA} = {F900A4AD-7249-41B4-B918-CB9E8C73747C} diff --git a/dotnet/src/dotnetcore/GxClasses/GxClasses.csproj b/dotnet/src/dotnetcore/GxClasses/GxClasses.csproj index 5287d3e2e..ad96ce5a5 100644 --- a/dotnet/src/dotnetcore/GxClasses/GxClasses.csproj +++ b/dotnet/src/dotnetcore/GxClasses/GxClasses.csproj @@ -78,6 +78,7 @@ + diff --git a/dotnet/src/dotnetframework/GxClasses/Configuration/ExternalStorage.cs b/dotnet/src/dotnetframework/GxClasses/Configuration/ExternalStorage.cs index 37b103595..d37cfcfbb 100644 --- a/dotnet/src/dotnetframework/GxClasses/Configuration/ExternalStorage.cs +++ b/dotnet/src/dotnetframework/GxClasses/Configuration/ExternalStorage.cs @@ -95,7 +95,7 @@ public bool Connect(string profileName, GXProperties properties, ref GxStoragePr private void preprocess(String name, GXProperties properties) { - string className = null; + string className; switch (name) { @@ -104,37 +104,27 @@ private void preprocess(String name, GXProperties properties) className = "GeneXus.Storage.GXAmazonS3.ExternalProviderS3"; SetDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "us-east-1"); SetDefaultProperty(properties, "STORAGE_ENDPOINT", "s3.amazonaws.com"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_SECRETACCESSKEY"); - SetEncryptProperty(properties, "BUCKET_NAME"); + SetEncryptedProperty(properties, "STORAGE_PROVIDER_ACCESSKEYID"); + SetEncryptedProperty(properties, "STORAGE_PROVIDER_SECRETACCESSKEY"); + SetEncryptedProperty(properties, "BUCKET_NAME"); break; case "AZURESTORAGE": className = "GeneXus.Storage.GXAzureStorage.AzureStorageExternalProvider"; - SetEncryptProperty(properties, "PUBLIC_CONTAINER_NAME"); - SetEncryptProperty(properties, "PRIVATE_CONTAINER_NAME"); - SetEncryptProperty(properties, "ACCOUNT_NAME"); - SetEncryptProperty(properties, "ACCESS_KEY"); + SetEncryptedProperty(properties, "PUBLIC_CONTAINER_NAME"); + SetEncryptedProperty(properties, "PRIVATE_CONTAINER_NAME"); + SetEncryptedProperty(properties, "ACCOUNT_NAME"); + SetEncryptedProperty(properties, "ACCESS_KEY"); break; - - case "BLUEMIXSTORAGE": - className = "GeneXus.Storage.GXBluemix.ExternalProviderBluemix"; - SetDefaultProperty(properties, "SERVER_URL", "https://identity.open.softlayer.com"); - SetDefaultProperty(properties, "STORAGE_PROVIDER_REGION", "dallas"); - SetEncryptProperty(properties, "PUBLIC_BUCKET_NAME"); - SetEncryptProperty(properties, "PRIVATE_BUCKET_NAME"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_USER"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_PASSWORD"); - break; - + //case "BOX": // className = "{class}"; // break; case "GOOGLE": className = "GeneXus.Storage.GXGoogleCloud.ExternalProviderGoogle"; - SetEncryptProperty(properties, "KEY"); - SetEncryptProperty(properties, "BUCKET_NAME"); + SetEncryptedProperty(properties, "KEY"); + SetEncryptedProperty(properties, "BUCKET_NAME"); break; //case "IBMCOS": @@ -146,9 +136,9 @@ private void preprocess(String name, GXProperties properties) case "OPENSTACKSTORAGE": className = "GeneXus.Storage.GXOpenStack.ExternalProviderOpenStack"; - SetEncryptProperty(properties, "BUCKET_NAME"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_USER"); - SetEncryptProperty(properties, "STORAGE_PROVIDER_PASSWORD"); + SetEncryptedProperty(properties, "BUCKET_NAME"); + SetEncryptedProperty(properties, "STORAGE_PROVIDER_USER"); + SetEncryptedProperty(properties, "STORAGE_PROVIDER_PASSWORD"); break; default: @@ -171,7 +161,7 @@ private void SetDefaultProperty(GXProperties properties, String prop, String val properties.Set(prop, value); } - private void SetEncryptProperty(GXProperties properties, String prop) + private void SetEncryptedProperty(GXProperties properties, String prop) { String value = properties.Get(prop); if (string.IsNullOrEmpty(value)) diff --git a/dotnet/src/dotnetframework/GxClasses/Core/GXApplication.cs b/dotnet/src/dotnetframework/GxClasses/Core/GXApplication.cs index a31c42067..2cd62a6f1 100644 --- a/dotnet/src/dotnetframework/GxClasses/Core/GXApplication.cs +++ b/dotnet/src/dotnetframework/GxClasses/Core/GXApplication.cs @@ -679,13 +679,10 @@ public static bool GetHttpRequestPostedFile(IGxContext gxContext, string varName if (httpContext != null) { HttpPostedFile pf = httpContext.Request.GetFile(varName); - if (pf != null) + if (pf != null && pf.ContentLength > 0) { -#pragma warning disable SCS0018 // Path traversal: injection possible in {1} argument passed to '{0}' - FileInfo fi = new FileInfo(pf.FileName); -#pragma warning restore SCS0018 // Path traversal: injection possible in {1} argument passed to '{0}' string tempDir = Preferences.getTMP_MEDIA_PATH(); - string ext = fi.Extension; + string ext = Path.GetExtension(pf.FileName); if (ext != null) ext = ext.TrimStart('.'); string filePath = FileUtil.getTempFileName(tempDir); @@ -695,7 +692,7 @@ public static bool GetHttpRequestPostedFile(IGxContext gxContext, string varName string fileGuid = GxUploadHelper.GetUploadFileGuid(); fileToken = GxUploadHelper.GetUploadFileId(fileGuid); - GxUploadHelper.CacheUploadFile(fileGuid, filePath, Path.GetFileName(pf.FileName), ext, file, gxContext); + GxUploadHelper.CacheUploadFile(fileGuid, Path.GetFileName(pf.FileName), ext, file, gxContext); return true; } diff --git a/dotnet/src/dotnetframework/GxClasses/Core/GXUtils.cs b/dotnet/src/dotnetframework/GxClasses/Core/GXUtils.cs index 20e1ce69d..23d3d8c80 100644 --- a/dotnet/src/dotnetframework/GxClasses/Core/GXUtils.cs +++ b/dotnet/src/dotnetframework/GxClasses/Core/GXUtils.cs @@ -1116,12 +1116,13 @@ public bool Upload(string filefullpath, string storageobjectfullname, GxFile upl try { ValidProvider(); + GxFileType acl = GxFileType.PublicRead; if (String.IsNullOrEmpty(storageobjectfullname)) { storageobjectfullname = Path.GetFileName(filefullpath); } - string url = provider.Upload(filefullpath, storageobjectfullname, GxFileType.Public); - uploadedFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider); + string url = provider.Upload(filefullpath, storageobjectfullname, acl); + uploadedFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider, acl); return true; } catch (Exception ex) @@ -1139,8 +1140,9 @@ public bool UploadPrivate(string filefullpath, string storageobjectfullname, GxF { storageobjectfullname=Path.GetFileName(filefullpath); } - string url = provider.Upload(filefullpath, storageobjectfullname, GxFileType.Private); - uploadedFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider); + GxFileType acl = GxFileType.Private; + string url = provider.Upload(filefullpath, storageobjectfullname, acl); + uploadedFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider, acl); return true; } catch (Exception ex) @@ -1160,7 +1162,7 @@ public bool Download(string storageobjectfullname, GxFile localFile, GXBaseColle destFileName = localFile.GetAbsoluteName(); else destFileName = Path.Combine(GxContext.StaticPhysicalPath(), localFile.Source); - provider.Download(storageobjectfullname, destFileName, GxFileType.Public); + provider.Download(storageobjectfullname, destFileName, GxFileType.PublicRead); return true; } catch (Exception ex) @@ -1197,7 +1199,8 @@ public bool Get(string storageobjectfullname, GxFile externalFile, GXBaseCollect try { ValidProvider(); - string url = provider.Get(storageobjectfullname, GxFileType.Public, 0); + GxFileType acl = GxFileType.PublicRead; + string url = provider.Get(storageobjectfullname, acl, 0); if (String.IsNullOrEmpty(url)) { GXUtil.ErrorToMessages("Get Error", "File doesn't exists", messages); @@ -1205,7 +1208,7 @@ public bool Get(string storageobjectfullname, GxFile externalFile, GXBaseCollect } else { - externalFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider); + externalFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider, acl); return true; } } @@ -1222,7 +1225,8 @@ public bool GetPrivate(string storageobjectfullname, GxFile externalFile, int ex try { ValidProvider(); - string url = provider.Get(storageobjectfullname, GxFileType.Private, expirationMinutes); + GxFileType acl = GxFileType.Private; + string url = provider.Get(storageobjectfullname, acl, expirationMinutes); if (String.IsNullOrEmpty(url)) { GXUtil.ErrorToMessages("Get Error", "File doesn't exists", messages); @@ -1230,7 +1234,7 @@ public bool GetPrivate(string storageobjectfullname, GxFile externalFile, int ex } else { - externalFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider, GxFileType.Private); + externalFile.FileInfo = new GxExternalFileInfo(storageobjectfullname, url, provider, acl); return true; } } diff --git a/dotnet/src/dotnetframework/GxClasses/Core/GXUtilsCommon.cs b/dotnet/src/dotnetframework/GxClasses/Core/GXUtilsCommon.cs index 49b3376d8..9b8e42068 100644 --- a/dotnet/src/dotnetframework/GxClasses/Core/GXUtilsCommon.cs +++ b/dotnet/src/dotnetframework/GxClasses/Core/GXUtilsCommon.cs @@ -37,6 +37,8 @@ using System.Security.Cryptography; using System.Collections.Concurrent; using System.Drawing.Drawing2D; +using GeneXus.Storage; +using GeneXus.Services; namespace GeneXus.Utils { @@ -3364,7 +3366,7 @@ public static string UriToPath(string uriString) return uriString; } } - public static string getTempFileName(string baseDir, string name="", string extension="tmp", GxFileType fileType = GxFileType.Public) + public static string getTempFileName(string baseDir, string name="", string extension="tmp", GxFileType fileType = GxFileType.Private) { name = FixFileName(FileUtil.FileNamePrettify(name), string.Empty); return tempFileName(baseDir, name, extension); @@ -3409,7 +3411,7 @@ public static string GetFileType(string FileName) if (GxUploadHelper.IsUpload(FileName)) { - return new GxFile(string.Empty, FileName, GxFileType.Private).GetExtension(); + return new GxFile(string.Empty, FileName, GxFileType.PrivateAttribute).GetExtension(); } string extension = string.Empty; @@ -3454,7 +3456,7 @@ public static string GetFileName(string FileName) if (GxUploadHelper.IsUpload(FileName)) { - FileName = new GxFile(string.Empty, FileName, GxFileType.Private).GetName(); + FileName = new GxFile(string.Empty, FileName, GxFileType.PrivateAttribute).GetName(); } try { @@ -5257,7 +5259,14 @@ public static string ResolveUri(string uriString, IGxContext context = null) public static string ResolveUri(string uriString, bool absUrl, IGxContext context = null) { if (String.IsNullOrEmpty(uriString)) - return ""; + return string.Empty; + + string providerObjectName; + if (PathUtil.IsAbsoluteUrl(uriString) && StorageFactory.TryGetProviderObjectName(ServiceFactory.GetExternalProvider(), uriString, out providerObjectName)) + { + return new GxFile(string.Empty, providerObjectName, GxFileType.DefaultAttribute).GetURI(); + } + if (schemeRegex.IsMatch(uriString)) { string fileName = schemeRegex.Replace(uriString, ""); @@ -5265,7 +5274,7 @@ public static string ResolveUri(string uriString, bool absUrl, IGxContext contex string basePath = Path.Combine(Path.Combine(Preferences.getBLOB_PATH(), MultimediaDirectory)); try { - GxFile file = new GxFile(string.Empty, PathUtil.SafeCombine(basePath, fileName), GxFileType.PublicAttribute); + GxFile file = new GxFile(string.Empty, PathUtil.SafeCombine(basePath, fileName), GxFileType.PrivateAttribute); return PathToUrl(file.GetURI(), absUrl, context); } catch (ArgumentException ex) @@ -5318,7 +5327,7 @@ public static string GetUriFromFile(string name, string type, string path) { if (GxUploadHelper.IsUpload(path)) { - return new GxFile(string.Empty, path, GxFileType.Private).GetName(); + return new GxFile(string.Empty, path, GxFileType.PrivateAttribute).GetName(); } string fromPathType = Path.GetExtension(path); if (!String.IsNullOrEmpty(fromPathType) && fromPathType != "tmp") diff --git a/dotnet/src/dotnetframework/GxClasses/Data/GXDataCommon.cs b/dotnet/src/dotnetframework/GxClasses/Data/GXDataCommon.cs index dae28864a..122c48a7b 100644 --- a/dotnet/src/dotnetframework/GxClasses/Data/GXDataCommon.cs +++ b/dotnet/src/dotnetframework/GxClasses/Data/GXDataCommon.cs @@ -903,7 +903,7 @@ protected static byte[] GetBinary(string fileNameParm, bool dbBlob) { if (ServiceFactory.GetExternalProvider() != null) { - GxFile file = new GxFile(string.Empty, fileNameParm, GxFileType.Private); + GxFile file = new GxFile(string.Empty, fileNameParm, GxFileType.PrivateAttribute); if (file.Exists()) { binary = file.ToByteArray(); diff --git a/dotnet/src/dotnetframework/GxClasses/Data/GXDataNTierADO.cs b/dotnet/src/dotnetframework/GxClasses/Data/GXDataNTierADO.cs index 0219ffb9d..eab6eebd3 100644 --- a/dotnet/src/dotnetframework/GxClasses/Data/GXDataNTierADO.cs +++ b/dotnet/src/dotnetframework/GxClasses/Data/GXDataNTierADO.cs @@ -10,8 +10,7 @@ using System.Collections.Generic; using GeneXus.Services; using System.Net; -using System.Diagnostics; -using System.Linq; +using GeneXus.Storage; namespace GeneXus.Data.NTier.ADO { @@ -105,7 +104,7 @@ public string getBLOBFile(int id) public string getBLOBFile(int id, string extension, string name) { - string fileName = FileUtil.getTempFileName(_gxDbCommand.Conn.BlobPath, name, extension, GxFileType.PrivateAttribute); + string fileName = FileUtil.getTempFileName(_gxDbCommand.Conn.BlobPath, name, extension, GxFileType.Private); return getBLOBFile(id, extension, name, fileName, true); } @@ -149,7 +148,7 @@ public string getMultimediaFile(int id, string gxdbFileUri) string filePath = PathUtil.SafeCombine(_gxDbCommand.Conn.MultimediaPath, fileName); try { - GxFile file = new GxFile(string.Empty, filePath, GxFileType.PublicAttribute); + GxFile file = new GxFile(string.Empty, filePath, GxFileType.DefaultAttribute); if (file.Exists()) { return filePath; @@ -331,7 +330,7 @@ public string getBLOBFile(int id) public string getBLOBFile(int id, string extension, string name) { - string fileName = FileUtil.getTempFileName(_gxDbCommand.Conn.BlobPath, name, extension, GxFileType.PrivateAttribute); + string fileName = FileUtil.getTempFileName(_gxDbCommand.Conn.BlobPath, name, extension, GxFileType.Private); String value = getBLOBFile(id, extension, name, fileName, true); TraceRow("getBLOBFile - index : ", id.ToString(), " value:", (value!=null ? value.ToString() : string.Empty)); return value; @@ -345,7 +344,7 @@ private string getBLOBFile(int id, string extension, string name, string fileNam int bufferSize = 4096; byte[] outbyte = new byte[bufferSize]; long retval; - long startIndex = 0; + long startIndex; bool streamClosed = false; try { @@ -433,7 +432,7 @@ public string getMultimediaFile(int id, string gxdbFileUri) try { - GxFile file = new GxFile(string.Empty, filePath, GxFileType.PublicAttribute); + GxFile file = new GxFile(string.Empty, filePath, GxFileType.DefaultAttribute); if (file.Exists()) { @@ -441,7 +440,7 @@ public string getMultimediaFile(int id, string gxdbFileUri) } else { - return getBLOBFile(id, FileUtil.GetFileType(gxdbFileUri), FileUtil.GetFileName(gxdbFileUri), filePath, false, GxFileType.PublicAttribute); + return getBLOBFile(id, FileUtil.GetFileType(gxdbFileUri), FileUtil.GetFileName(gxdbFileUri), filePath, false, GxFileType.DefaultAttribute); } } catch (ArgumentException) @@ -456,7 +455,7 @@ public string getMultimediaFile(int id, string gxdbFileUri) public string getMultimediaUri(int id) { - return GXDbFile.ResolveUri(getVarchar(id), true, _gxDbCommand.Conn.DataStore.Context); + return getMultimediaUri(id, true); } public string getMultimediaUri(int id, bool absUrl) @@ -613,11 +612,11 @@ public void SetParameterMultimedia(int id, string image_gxi, string image, strin string objectName; //file is already on the cloud p.e. https://s3.amazonaws.com/Test/PublicTempStorage/multimedia/Image_ad013b5b050c4bf199f544b5561d9b92.png //Must be copied to https://s3.amazonaws.com/Test/TableName/FieldName/Image_ad013b5b050c4bf199f544b5561d9b92.png - if (ServiceFactory.GetExternalProvider().GetObjectNameFromURL(image_gxi, out objectName)) + if (ServiceFactory.GetExternalProvider().TryGetObjectNameFromURL(image_gxi, out objectName)) { try { - multimediaUri = ServiceFactory.GetExternalProvider().Copy(image_gxi, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.PublicAttribute); + multimediaUri = ServiceFactory.GetExternalProvider().Copy(image_gxi, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.DefaultAttribute); GXLogging.Debug(log, "Copy file already in ExternalProvider:", multimediaUri); } catch (Exception ex) @@ -635,7 +634,7 @@ public void SetParameterMultimedia(int id, string image_gxi, string image, strin using (var fileStream = new MemoryStream(new WebClient().DownloadData(image_gxi))) { //Cannot pass Http Stream directly, because some Providers (AWS S3) does not support Http Stream. - multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.Public); + multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.DefaultAttribute); GXLogging.Debug(log, "Upload external file to ExternalProvider:", multimediaUri); } #pragma warning disable SYSLIB0014 // WebClient @@ -657,7 +656,7 @@ public void SetParameterMultimedia(int id, string image_gxi, string image, strin String fileName = PathUtil.GetValidFileName(fileFullName, "_"); using (fileStream) { - multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, GXDbFile.GenerateUri(fileName, !GXDbFile.HasToken(fileName), false), tableName, fieldName, GxFileType.Public); + multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, GXDbFile.GenerateUri(fileName, !GXDbFile.HasToken(fileName), false), tableName, fieldName, GxFileType.DefaultAttribute); GXLogging.Debug(log, "Upload file (_gxi) to ExternalProvider:", multimediaUri); } } @@ -665,7 +664,7 @@ public void SetParameterMultimedia(int id, string image_gxi, string image, strin { try { - multimediaUri = ServiceFactory.GetExternalProvider().Copy(image, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.Public); + multimediaUri = ServiceFactory.GetExternalProvider().Copy(image, GXDbFile.GenerateUri(image_gxi, !GXDbFile.HasToken(image_gxi), false), tableName, fieldName, GxFileType.DefaultAttribute); GXLogging.Debug(log, "Copy external file in ExternalProvider:", multimediaUri); } catch(Exception e) @@ -726,7 +725,7 @@ private static string PushToExternalProvider(Stream fileStream, string externalF string multimediaUri; using (fileStream) { - multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, externalFileName, tableName, fieldName, GxFileType.PublicAttribute); + multimediaUri = ServiceFactory.GetExternalProvider().Save(fileStream, externalFileName, tableName, fieldName, GxFileType.DefaultAttribute); GXLogging.Debug(log, "Upload file to ExternalProvider:", multimediaUri); } diff --git a/dotnet/src/dotnetframework/GxClasses/Domain/GXFileIO.cs b/dotnet/src/dotnetframework/GxClasses/Domain/GXFileIO.cs index 8fe6a1f61..b7f0c1d79 100644 --- a/dotnet/src/dotnetframework/GxClasses/Domain/GXFileIO.cs +++ b/dotnet/src/dotnetframework/GxClasses/Domain/GXFileIO.cs @@ -209,7 +209,7 @@ public IGxFileInfo[] GetFiles(string searchPattern) searchPattern = ""; List files = _provider.GetFiles(_name, searchPattern); - GxExternalFileInfo[] externalFiles = files.Select(elem => new GxExternalFileInfo(elem, _provider, GxFileType.Public)).ToArray(); + GxExternalFileInfo[] externalFiles = files.Select(elem => new GxExternalFileInfo(elem, _provider, GxFileType.Default)).ToArray(); return externalFiles; } @@ -412,7 +412,7 @@ public class GxExternalFileInfo : IGxFileInfo private string _name; private ExternalProvider _provider; private string _url; - private GxFileType _fileTypeAtt = GxFileType.Public; + private GxFileType _fileTypeAtt = GxFileType.Private; public GxExternalFileInfo(ExternalProvider provider) { @@ -421,32 +421,32 @@ public GxExternalFileInfo(ExternalProvider provider) _url = ""; } - public GxExternalFileInfo(string storageObjectFullname, ExternalProvider provider, GxFileType fileType) + public GxExternalFileInfo(string objectPath, ExternalProvider provider, GxFileType fileType) { - storageObjectFullname = storageObjectFullname!=null ? storageObjectFullname.Replace('\\', '/') : storageObjectFullname; - _name = storageObjectFullname; + objectPath = !String.IsNullOrEmpty(objectPath) ? objectPath.Replace('\\', '/') : objectPath; _provider = provider; + _fileTypeAtt = fileType; + _name = objectPath; + Uri result; - if (Uri.TryCreate(storageObjectFullname, UriKind.Absolute, out result) && result.IsAbsoluteUri) + if (Uri.TryCreate(objectPath, UriKind.Absolute, out result) && result.IsAbsoluteUri) { - _url = storageObjectFullname; + _url = objectPath; } else { - if (fileType.HasFlag(GxFileType.Attribute)) //Attributes multimedia consider Storage Provider Folder + string folderName = ((ExternalProviderBase)provider).Folder; + if (!string.IsNullOrEmpty(folderName) && fileType.HasFlag(GxFileType.Attribute) && !_name.StartsWith(folderName)) { - _url = provider.GetBaseURL() + storageObjectFullname; - _name = _url.Replace(provider.StorageUri, string.Empty); - if (_name.StartsWith("/")) - _name = _name.Substring(1, _name.Length - 1); + _url = $"{provider.GetBaseURL()}{_name}"; + _name = $"{folderName}{StorageUtils.DELIMITER}{_name}"; } - } - _fileTypeAtt = fileType; + } } - public GxExternalFileInfo(string storageObjectFullname, string url, ExternalProvider provider, GxFileType fileType = GxFileType.Public) - { - _name = storageObjectFullname; - _provider = provider; + public GxExternalFileInfo(string objectPath, string url, ExternalProvider provider, GxFileType fileType = GxFileType.Private) + { + _name = StorageFactory.GetProviderObjectAbsoluteUriSafe(provider, objectPath); + _provider = provider; _url = url; _fileTypeAtt = fileType; } @@ -576,12 +576,8 @@ public string AbsolutePath private string URL { - get { - if (string.IsNullOrEmpty(_url)) - { - _url = _provider.Get(_name, _fileTypeAtt, 0); - } - return _url; + get { + return _provider.GetUrl(_name, _fileTypeAtt, 0); } } @@ -640,11 +636,12 @@ public Stream GetStream() [Flags] public enum GxFileType { - Public = 0, - Private = 1, - Attribute = 2, - PublicAttribute = Attribute | Public, - PrivateAttribute = Attribute | Private, + Default = 0, + PublicRead = 1, + Private = 2, + Attribute = 8, + DefaultAttribute = Attribute | Default, + PrivateAttribute = Attribute | Private } public class GxFile @@ -670,13 +667,13 @@ public GxFile(string baseDirectory) _baseDirectory = _baseDirectory.Substring(0, _baseDirectory.Length - 1); } - public GxFile(string baseDirectory, IGxFileInfo file, GxFileType fileType = GxFileType.Public) + public GxFile(string baseDirectory, IGxFileInfo file, GxFileType fileType = GxFileType.Private) : this(baseDirectory) { _file = file; } - public GxFile(string baseDirectory, string fileName, GxFileType fileType = GxFileType.Public) + public GxFile(string baseDirectory, string fileName, GxFileType fileType = GxFileType.Private) : this(baseDirectory) { if (GxUploadHelper.IsUpload(fileName)) @@ -733,22 +730,26 @@ public string Source } else { + _file = null; if (GxUploadHelper.IsUpload(value)) { _uploadFileId = value; value = GxUploadHelper.UploadPath(value); + ExternalProvider provider = ServiceFactory.GetExternalProvider(); + _file = (provider != null)? new GxExternalFileInfo(value, String.Empty, provider): _file; } - - if (IsAbsoluteUrl(value)) - { - string objName = string.Empty; - ExternalProvider p = StorageFactory.GetExternalProviderFromUrl(value, out objName); - _file = new GxExternalFileInfo(objName, value, p); - } - else + + if (_file == null) { - _file = new GxFileInfo(_baseDirectory); - _file.Source = value; + if (IsAbsoluteUrl(value)) + { + _file =new GxExternalFileInfo(value, value, ServiceFactory.GetExternalProvider()); + } + else + { + _file = new GxFileInfo(_baseDirectory); + _file.Source = value; + } } _lastError = 0; diff --git a/dotnet/src/dotnetframework/GxClasses/Helpers/GXRestUtils.cs b/dotnet/src/dotnetframework/GxClasses/Helpers/GXRestUtils.cs index 8bd57d8f9..b8ffa60af 100644 --- a/dotnet/src/dotnetframework/GxClasses/Helpers/GXRestUtils.cs +++ b/dotnet/src/dotnetframework/GxClasses/Helpers/GXRestUtils.cs @@ -29,10 +29,10 @@ internal static bool IsUploadURL(HttpContext httpContext) { return httpContext.Request.GetRawUrl().EndsWith(HttpHelper.GXOBJECT, StringComparison.OrdinalIgnoreCase); } - internal static void CacheUploadFile(string fileGuid, string savedFileName, string localfileName, string ext, GxFile file, IGxContext gxContext) + internal static void CacheUploadFile(string fileGuid, string realFileName, string realFileExtension, GxFile temporalFile, IGxContext gxContext) { - CacheAPI.FilesCache.Set(fileGuid, JSONHelper.Serialize(new UploadCachedFile() { path = savedFileName, fileExtension = ext, fileName = localfileName }), GxRestPrefix.UPLOAD_TIMEOUT); - GXFileWatcher.Instance.AddTemporaryFile(file, gxContext); + CacheAPI.FilesCache.Set(fileGuid, JSONHelper.Serialize(new UploadCachedFile() { path = temporalFile.GetAbsoluteName(), fileExtension = realFileExtension, fileName = realFileName }), GxRestPrefix.UPLOAD_TIMEOUT); + GXFileWatcher.Instance.AddTemporaryFile(temporalFile, gxContext); } internal static string GetUploadFileGuid() { diff --git a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs index cb2f23036..9a7948910 100644 --- a/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs +++ b/dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs @@ -235,7 +235,7 @@ public static bool GetHttpRequestPostedFile(IGxContext gxContext, string varName ext = ext.TrimStart('.'); filePath = FileUtil.getTempFileName(tempDir); GXLogging.Debug(log, "cgiGet(" + varName + "), fileName:" + filePath); - GxFile file = new GxFile(tempDir, filePath, GxFileType.Private); + GxFile file = new GxFile(tempDir, filePath, GxFileType.PrivateAttribute); #if NETCORE filePath = file.Create(pf.OpenReadStream()); #else diff --git a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpServices.cs b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpServices.cs index 21994ac6c..ada323861 100644 --- a/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpServices.cs +++ b/dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpServices.cs @@ -386,7 +386,7 @@ public override void webExecute() { try { - string savedFileName, ext, fName; + string ext, fName; if (context.isMultipartRequest()) { localHttpContext.Response.ContentType = MediaTypesNames.TextPlain; @@ -406,8 +406,7 @@ public override void webExecute() ext = FileUtil.GetFileType(fName); string tempDir = Preferences.getTMP_MEDIA_PATH(); - savedFileName = FileUtil.getTempFileName(tempDir); - GxFile gxFile = new GxFile(tempDir, savedFileName, GxFileType.Private); + GxFile gxFile = new GxFile(tempDir, FileUtil.getTempFileName(tempDir), GxFileType.PrivateAttribute); gxFile.Create(hpf.InputStream); string uri = gxFile.GetURI(); @@ -423,7 +422,7 @@ public override void webExecute() thumbnailUrl = url, path = fileToken }); - GxUploadHelper.CacheUploadFile(fileGuid, savedFileName, Path.GetFileName(fName), ext, gxFile, context); + GxUploadHelper.CacheUploadFile(fileGuid, Path.GetFileName(fName), ext, gxFile, context); } UploadFilesResult result = new UploadFilesResult() { files = r }; var jsonObj = JSONHelper.Serialize(result); @@ -457,11 +456,10 @@ public override void webExecute() internal void WcfExecute(Stream istream, string contentType) { - string savedFileName, ext, fName; + string ext, fName; ext = context.ExtensionForContentType(contentType); - string tempDir = Preferences.getTMP_MEDIA_PATH(); - savedFileName = FileUtil.getTempFileName(tempDir); - GxFile file = new GxFile(tempDir, savedFileName, GxFileType.Private); + string tempDir = Preferences.getTMP_MEDIA_PATH(); + GxFile file = new GxFile(tempDir, FileUtil.getTempFileName(tempDir), GxFileType.PrivateAttribute); file.Create(istream); JObject obj = new JObject(); @@ -475,7 +473,7 @@ internal void WcfExecute(Stream istream, string contentType) HttpHelper.SetResponseStatus(localHttpContext, ((int)HttpStatusCode.Created).ToString(), string.Empty); localHttpContext.Response.Write(obj.ToString()); - GxUploadHelper.CacheUploadFile(fileGuid, savedFileName, $"{Path.GetFileNameWithoutExtension(fName)}.{ext}", ext, file, context); + GxUploadHelper.CacheUploadFile(fileGuid, $"{Path.GetFileNameWithoutExtension(fName)}.{ext}", ext, file, context); } protected override bool IntegratedSecurityEnabled { diff --git a/dotnet/src/dotnetframework/GxClasses/Services/Storage/ExternalProviderBase.cs b/dotnet/src/dotnetframework/GxClasses/Services/Storage/ExternalProviderBase.cs new file mode 100644 index 000000000..6db3409fb --- /dev/null +++ b/dotnet/src/dotnetframework/GxClasses/Services/Storage/ExternalProviderBase.cs @@ -0,0 +1,173 @@ +using System; +using System.IO; +using GeneXus.Encryption; +using log4net; +#if NETCORE +using GeneXus.Mime; +#else +using System.Web; +#endif + +namespace GeneXus.Services +{ + public abstract class ExternalProviderBase + { + static readonly ILog logger = log4net.LogManager.GetLogger(typeof(ExternalProviderBase)); + + private GXService service; + + protected static String DEFAULT_ACL = "DEFAULT_ACL"; + protected static String DEFAULT_EXPIRATION = "DEFAULT_EXPIRATION"; + protected static String FOLDER = "FOLDER_NAME"; + protected static String DEFAULT_ACL_DEPRECATED = "STORAGE_PROVIDER_DEFAULT_ACL"; + protected static String DEFAULT_EXPIRATION_DEPRECATED = "STORAGE_PROVIDER_DEFAULT_EXPIRATION"; + protected TimeSpan defaultExpiration = new TimeSpan(24, 0, 0); + protected static string DEFAULT_TMP_CONTENT_TYPE = "image/jpeg"; + protected static string DEFAULT_CONTENT_TYPE = "application/octet-stream"; + + protected GxFileType defaultAcl = GxFileType.Private; + public string Folder { get; set; } + + public ExternalProviderBase() + { + Initialize(); + } + + public ExternalProviderBase(GXService s) + { + if (s == null) { + try + { + s = ServiceFactory.GetGXServices().Get(GXServices.STORAGE_SERVICE); + } + catch (Exception) { + logger.Warn("STORAGE_SERVICE is not activated in CloudServices.config"); + } + } + + this.service = s; + Initialize(); + } + + public abstract String GetName(); + + private void Initialize() + { + String aclS = GetPropertyValue(DEFAULT_ACL, DEFAULT_ACL_DEPRECATED, ""); + if (!String.IsNullOrEmpty(aclS)) + { + this.defaultAcl = aclS.Equals("Private") ? GxFileType.Private : GxFileType.PublicRead; + } + + String expirationS = GetPropertyValue(DEFAULT_EXPIRATION, DEFAULT_EXPIRATION, defaultExpiration.TotalMinutes.ToString()); + if (!String.IsNullOrEmpty(expirationS)) + { + int minutes; + if (Int32.TryParse(expirationS, out minutes) && minutes > 0) + { + defaultExpiration = new TimeSpan(0, minutes, 0); + } + } + Folder = GetPropertyValue(FOLDER, null, string.Empty); + } + + protected TimeSpan ResolveExpiration(int expirationMinutes) + { + return expirationMinutes > 0 ? new TimeSpan(0, expirationMinutes, 0) : defaultExpiration; + } + + protected String GetEncryptedPropertyValue(String propertyName, String alternativePropertyName = null) + { + String value = GetEncryptedPropertyValue(propertyName, alternativePropertyName, null); + if (value == null) + { + String errorMessage = String.Format($"Service configuration error - Property name {ResolvePropertyName(propertyName)} must be defined"); + logger.Fatal(errorMessage); + throw new Exception(errorMessage); + } + return value; + } + + protected String GetEncryptedPropertyValue(String propertyName, String alternativePropertyName, String defaultValue) + { + String value = GetPropertyValue(propertyName, alternativePropertyName, defaultValue); + if (!String.IsNullOrEmpty(value)) + { + try + { + value = CryptoImpl.Decrypt(value); + } + catch (Exception) + { + logger.Warn($"Could not decrypt property name: {ResolvePropertyName(propertyName)}"); + } + } + return value; + } + + protected String GetPropertyValue(String propertyName, String alternativePropertyName = null) + { + String value = GetPropertyValue(propertyName, alternativePropertyName, null); + if (value == null) + { + String errorMessage = String.Format($"Service configuration error - Property name {ResolvePropertyName(propertyName)} must be defined"); + logger.Fatal(errorMessage); + throw new Exception(errorMessage); + } + return value; + } + + protected String GetPropertyValue(String propertyName, String alternativePropertyName, String defaultValue) + { + String value = null; + value = string.IsNullOrEmpty(value) ? GetPropertyValueImpl(ResolvePropertyName(propertyName)) : value; + value = string.IsNullOrEmpty(value) ? GetPropertyValueImpl(propertyName) : value; + value = string.IsNullOrEmpty(value) ? GetPropertyValueImpl(alternativePropertyName) : value; + value = string.IsNullOrEmpty(value) ? defaultValue : value; + return value; + } + + private string GetPropertyValueImpl(string propertyName) + { + String value = null; + if (!string.IsNullOrEmpty(propertyName)) + { + value = Environment.GetEnvironmentVariable(propertyName); + if (this.service != null) + { + value = this.service.Properties.Get(propertyName); + } + } + return value; + } + + protected String ResolvePropertyName(String propertyName) + { + return $"STORAGE_{GetName()}_{propertyName}"; + } + + protected bool TryGetContentType(string fileName, out string mimeType, string defaultValue = null) + { + mimeType = defaultValue; + string extension = Path.GetExtension(fileName); + if (!string.IsNullOrEmpty(extension)) + { + if (fileName.EndsWith(".tmp")) + { + mimeType = DEFAULT_TMP_CONTENT_TYPE; + } + else + { + try + { + mimeType = MimeMapping.GetMimeMapping(fileName); + } + catch (Exception) + { + } + } + } + return mimeType != null; + } + } +} diff --git a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs index 399e02a56..525d95e55 100644 --- a/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs +++ b/dotnet/src/dotnetframework/GxClasses/Services/Storage/GXServices.cs @@ -231,6 +231,7 @@ public interface ExternalProvider string Upload(string localFile, string objectName, GxFileType fileType); void Download(string objectName, string localFile, GxFileType fileType); string Get(string objectName, GxFileType fileType, int urlMinutes); + string GetUrl(string objectName, GxFileType fileType, int urlMinutes); void Delete(string objectName, GxFileType fileType); bool Exists(string objectName, GxFileType fileType); string Rename(string objectName, string newName, GxFileType fileType); @@ -247,9 +248,9 @@ public interface ExternalProvider List GetFiles(string directoryName, string filter = ""); List GetSubDirectories(string directoryName); Stream GetStream(string objectName, GxFileType fileType); - bool GetMessageFromException(Exception ex, SdtMessages_Message msg); - bool GetObjectNameFromURL(string url, out string objectName); + bool GetMessageFromException(Exception ex, SdtMessages_Message msg); string GetBaseURL(); + bool TryGetObjectNameFromURL(string objectNameOrUrl, out string providerObjectName); string StorageUri { get; } } } diff --git a/dotnet/src/dotnetframework/GxClasses/Storage/StorageFactory.cs b/dotnet/src/dotnetframework/GxClasses/Storage/StorageFactory.cs index 0ef2a2d3f..f4bc8fdda 100644 --- a/dotnet/src/dotnetframework/GxClasses/Storage/StorageFactory.cs +++ b/dotnet/src/dotnetframework/GxClasses/Storage/StorageFactory.cs @@ -5,20 +5,29 @@ namespace GeneXus.Storage public class StorageFactory { const char QUESTION_MARK = '?'; - public static ExternalProvider GetExternalProviderFromUrl(string url, out string objectName) + public static bool TryGetProviderObjectName(ExternalProvider provider, string objectNameOrUrl, out string providerObjectName) { - objectName = null; - ExternalProvider provider = ServiceFactory.GetExternalProvider(); - if (provider != null) + providerObjectName = null; + if (provider != null && provider.TryGetObjectNameFromURL(objectNameOrUrl, out providerObjectName)) { - if (provider.GetObjectNameFromURL(url, out objectName)) + int idx = providerObjectName.IndexOf(QUESTION_MARK); + if (idx > 0) { - var questionMarkIndex = objectName.IndexOf(QUESTION_MARK); - objectName = questionMarkIndex >= 0 ? objectName.Substring(0, questionMarkIndex): objectName.Substring(0); - return provider; + providerObjectName = providerObjectName.Substring(0, idx); } + return true; } - return null; + return false; + } + + public static string GetProviderObjectAbsoluteUriSafe(ExternalProvider provider, string rawObjectUri) + { + string providerObjectName; + if (TryGetProviderObjectName(provider, rawObjectUri, out providerObjectName)) + { + rawObjectUri = providerObjectName; + } + return rawObjectUri; } } } diff --git a/dotnet/src/dotnetframework/GxExcel/GxExcelI.cs b/dotnet/src/dotnetframework/GxExcel/GxExcelI.cs index 8317f9bc5..27d4c25a9 100644 --- a/dotnet/src/dotnetframework/GxExcel/GxExcelI.cs +++ b/dotnet/src/dotnetframework/GxExcel/GxExcelI.cs @@ -144,7 +144,7 @@ public String Template string localTemplate = value; if (!Path.IsPathRooted(localTemplate)) localTemplate = Path.Combine(GxContext.StaticPhysicalPath(), localTemplate); - ServiceFactory.GetExternalProvider().Upload(localTemplate, template, GxFileType.Public); + ServiceFactory.GetExternalProvider().Upload(localTemplate, template, GxFileType.Private); } } else if (!Path.IsPathRooted(value)) diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXAmazonS3/ExternalProviderS3.cs b/dotnet/src/dotnetframework/Providers/Storage/GXAmazonS3/ExternalProviderS3.cs index 148ad28bb..c7a26cd14 100644 --- a/dotnet/src/dotnetframework/Providers/Storage/GXAmazonS3/ExternalProviderS3.cs +++ b/dotnet/src/dotnetframework/Providers/Storage/GXAmazonS3/ExternalProviderS3.cs @@ -10,28 +10,51 @@ using System.Collections.Generic; using System.IO; using System.Threading; -using System.Threading.Tasks; + namespace GeneXus.Storage.GXAmazonS3 { - public class ExternalProviderS3 : ExternalProvider + public class ExternalProviderS3 : ExternalProviderBase, ExternalProvider { - const int PRIVATE_URL_MINUTES_EXPIRATION = 24 * 60; // 24 hours - const string ACCESS_KEY_ID = "STORAGE_PROVIDER_ACCESSKEYID"; - const string SECRET_ACCESS_KEY = "STORAGE_PROVIDER_SECRETACCESSKEY"; - const string REGION = "STORAGE_PROVIDER_REGION"; - const string ENDPOINT = "STORAGE_ENDPOINT"; + public const string Name = "AWSS3"; + + const string ACCESS_KEY = "ACCESS_KEY"; + const string SECRET_ACCESS_KEY = "SECRET_KEY"; + const string STORAGE_CUSTOM_ENDPOINT = "CUSTOM_ENDPOINT"; + const string STORAGE_ENDPOINT = "ENDPOINT"; const string BUCKET = "BUCKET_NAME"; - const string FOLDER = "FOLDER_NAME"; + const string REGION = "REGION"; + const string STORAGE_CUSTOM_ENDPOINT_VALUE = "custom"; + + const string DEFAULT_ENDPOINT = "s3.amazonaws.com"; + const string DEFAULT_REGION = "us-east-1"; + + [Obsolete("Use Property ACCESS_KEY instead", false)] + const string ACCESS_KEY_ID_DEPRECATED = "STORAGE_PROVIDER_ACCESSKEYID"; + [Obsolete("Use Property SECRET_ACCESS_KEY instead", false)] + const string SECRET_ACCESS_KEY_DEPRECATED = "STORAGE_PROVIDER_SECRETACCESSKEY"; + [Obsolete("Use Property REGION instead", false)] + const string REGION_DEPRECATED = "STORAGE_PROVIDER_REGION"; + [Obsolete("Use Property STORAGE_ENDPOINT instead", false)] + const string ENDPOINT_DEPRECATED = "STORAGE_ENDPOINT"; + [Obsolete("Use Property STORAGE_CUSTOM_ENDPOINT instead", false)] + const string STORAGE_CUSTOM_ENDPOINT_DEPRECATED = "STORAGE_CUSTOM_ENDPOINT"; + + string _storageUri; IAmazonS3 Client { get; set; } string Bucket { get; set; } - string Folder { get; set; } string Endpoint { get; set; } + string Region { get; set; } + bool forcePathStyle = false; + bool customEndpoint = false; + public string StorageUri { - get { return $"https://{Bucket}.{Endpoint}/"; } + get { + return _storageUri; + } } public string GetBaseURL() @@ -39,30 +62,41 @@ public string GetBaseURL() return StorageUri + Folder + StorageUtils.DELIMITER; } - public ExternalProviderS3() - : this(ServiceFactory.GetGXServices().Get(GXServices.STORAGE_SERVICE)) - { + public ExternalProviderS3(): this(null) + { } - public ExternalProviderS3(GXService providerService) + public ExternalProviderS3(GXService providerService): base(providerService) { - string keyId = CryptoImpl.Decrypt(providerService.Properties.Get(ACCESS_KEY_ID)); - string keySecret = CryptoImpl.Decrypt(providerService.Properties.Get(SECRET_ACCESS_KEY)); + Initialize(); + } + + private void Initialize() { + string keyId = GetEncryptedPropertyValue(ACCESS_KEY, ACCESS_KEY_ID_DEPRECATED); + string keySecret = GetEncryptedPropertyValue(SECRET_ACCESS_KEY, SECRET_ACCESS_KEY_DEPRECATED); AWSCredentials credentials = null; if (!string.IsNullOrEmpty(keyId) && !string.IsNullOrEmpty(keySecret)) { credentials = new BasicAWSCredentials(keyId, keySecret); } - var region = Amazon.RegionEndpoint.GetBySystemName(providerService.Properties.Get(REGION)); - Endpoint = providerService.Properties.Get(ENDPOINT); + var region = Amazon.RegionEndpoint.GetBySystemName(GetPropertyValue(REGION, REGION_DEPRECATED, DEFAULT_REGION)); AmazonS3Config config = new AmazonS3Config() { - ServiceURL = Endpoint, RegionEndpoint = region }; + Endpoint = GetPropertyValue(STORAGE_ENDPOINT, ENDPOINT_DEPRECATED, DEFAULT_ENDPOINT); + if (Endpoint == STORAGE_CUSTOM_ENDPOINT_VALUE) + { + Endpoint = GetPropertyValue(STORAGE_CUSTOM_ENDPOINT, STORAGE_CUSTOM_ENDPOINT_DEPRECATED); + forcePathStyle = true; + config.ForcePathStyle = forcePathStyle; + config.ServiceURL = Endpoint; + customEndpoint = true; + } + #if NETCORE if (credentials != null) { @@ -83,13 +117,33 @@ public ExternalProviderS3(GXService providerService) } #endif - Bucket = CryptoImpl.Decrypt(providerService.Properties.Get(BUCKET)); - Folder = providerService.Properties.Get(FOLDER); + Bucket = GetEncryptedPropertyValue(BUCKET); + Region = region.SystemName; + SetURI(); CreateBucket(); CreateFolder(Folder); } + private void SetURI() + { + if (customEndpoint) + { + _storageUri = !Endpoint.EndsWith("/") ? $"{Endpoint}/{Bucket}/": $"{Endpoint}{Bucket}/"; + } + else + { + if (Region == DEFAULT_REGION) + { + _storageUri = (forcePathStyle) ? $"{Endpoint}/" : $"https://{Bucket}.{Endpoint}/"; + } + else + { + _storageUri = $"https://{Bucket}.{Endpoint.Replace("s3.amazonaws.com", $"s3.{Region.ToLower()}.amazonaws.com")}/"; + } + } + } + private void AddObjectMetadata(MetadataCollection metadata, string tableName, string fieldName, string key) { metadata.Add("Table", tableName); @@ -160,11 +214,11 @@ private void CreateBucket() PutBucketRequest request = new PutBucketRequest { BucketName = Bucket, - UseClientRegion = true, - // Every bucket is public - CannedACL = S3CannedACL.PublicRead + UseClientRegion = true }; - + if (defaultAcl == GxFileType.PublicRead) { + request.CannedACL = S3CannedACL.PublicRead; + } PutBucket(request); } } @@ -178,28 +232,38 @@ public string Upload(string localFile, string objectName, GxFileType fileType) FilePath = localFile, CannedACL = GetCannedACL(fileType) }; - PutObjectResponse result = PutObject(objectRequest); - return Get(objectName, fileType); + PutObject(objectRequest); + return GetUrlImpl(objectName, fileType); } - private static bool IsPrivateUpload(GxFileType fileType) + private bool IsPrivateUpload(GxFileType fileType) { - return fileType.HasFlag(GxFileType.Private); + return GetCannedACL(fileType) != S3CannedACL.PublicRead; } - public string Get(string objectName, GxFileType fileType, int urlMinutes = PRIVATE_URL_MINUTES_EXPIRATION) + public string Get(string objectName, GxFileType fileType, int urlMinutes = 0) { - bool isPrivate = IsPrivateUpload(fileType); if (Exists(objectName, fileType)) - if (isPrivate) - return GetPreSignedUrl(objectName, urlMinutes); - else - return StorageUri + StorageUtils.EncodeUrl(objectName); + { + return GetUrlImpl(objectName, fileType, urlMinutes); + } else return string.Empty; } - private string GetPreSignedUrl(string objectName, int urlMinutes) + public string GetUrl(string objectName, GxFileType fileType, int urlMinutes = 0) + { + return GetUrlImpl(objectName, fileType, urlMinutes); + } + + private string GetUrlImpl(string objectName, GxFileType fileType, int urlMinutes = 0) + { + bool isPrivate = IsPrivateUpload(fileType); + return (isPrivate)? GetPreSignedUrl(objectName, ResolveExpiration(urlMinutes).TotalMinutes): StorageUri + StorageUtils.EncodeUrl(objectName); + + } + + private string GetPreSignedUrl(string objectName, double urlMinutes) { GetPreSignedUrlRequest request = new GetPreSignedUrlRequest { @@ -207,6 +271,10 @@ private string GetPreSignedUrl(string objectName, int urlMinutes) Key = objectName, Expires = DateTime.Now.AddMinutes(urlMinutes) }; + if (customEndpoint && StorageUri.StartsWith("http://")) + { + request.Protocol = Protocol.HTTP; + } return Client.GetPreSignedURL(request); } @@ -234,7 +302,7 @@ public void Delete(string objectName, GxFileType fileType) //https://github.com/aws/aws-sdk-net/blob/master/sdk/src/Services/S3/Custom/_bcl/IO/S3FileInfo.cs public bool Exists(string objectName, GxFileType fileType) { - bool exists = true; + bool exists; try { exists = new S3FileInfo(Client, Bucket, objectName).Exists; @@ -267,9 +335,24 @@ public string Copy(string objectName, GxFileType sourceFileType, string newName, return StorageUri + StorageUtils.EncodeUrl(newName); } - private static S3CannedACL GetCannedACL(GxFileType destFileType) + private S3CannedACL GetCannedACL(GxFileType acl) { - return (destFileType.HasFlag(GxFileType.Private)) ? S3CannedACL.Private : S3CannedACL.PublicRead; + if (acl.HasFlag(GxFileType.Private)) + { + return S3CannedACL.Private; + } + else if (acl.HasFlag(GxFileType.PublicRead)) + { + return S3CannedACL.PublicRead; + } + else if (this.defaultAcl == GxFileType.Private) + { + return S3CannedACL.Private; + } + else + { + return S3CannedACL.PublicRead; + } } public string Upload(string fileName, Stream stream, GxFileType destFileType) @@ -281,8 +364,9 @@ public string Upload(string fileName, Stream stream, GxFileType destFileType) InputStream = stream, CannedACL = GetCannedACL(destFileType) }; - if (Path.GetExtension(fileName).Equals(".tmp")) - objectRequest.ContentType = "image/jpeg"; + if (TryGetContentType(fileName, out string mimeType)) { + objectRequest.ContentType = mimeType; + } PutObjectResponse result = PutObject(objectRequest); return Get(fileName, destFileType); } @@ -301,8 +385,15 @@ public string Copy(string url, string newName, string tableName, string fieldNam SourceKey = url, DestinationBucket = Bucket, DestinationKey = resourceKey, - CannedACL = GetCannedACL(destFileType) + CannedACL = GetCannedACL(destFileType), + MetadataDirective = S3MetadataDirective.REPLACE }; + + if (TryGetContentType(newName, out string mimeType, DEFAULT_CONTENT_TYPE)) + { + request.ContentType = mimeType; + } + AddObjectMetadata(request.Metadata, tableName, fieldName, resourceKey); CopyObject(request); @@ -322,10 +413,15 @@ public string Save(Stream fileStream, string fileName, string tableName, string InputStream = fileStream, CannedACL = GetCannedACL(destFileType) }; + if (TryGetContentType(fileName, out string mimeType)) + { + objectRequest.ContentType = mimeType; + } + AddObjectMetadata(objectRequest.Metadata, tableName, fieldName, resourceKey); PutObjectResponse result = PutObject(objectRequest); - return "https://" + Bucket + ".s3.amazonaws.com/" + resourceKey; + return StorageUri + resourceKey; } catch (Exception ex) { @@ -378,7 +474,7 @@ public void RenameDirectory(string directoryName, string newDirectoryName) if (file.Type == FileSystemType.Directory) RenameDirectory(directoryName + "\\" + file.Name, newDirectoryName + "\\" + file.Name); else - Rename(directoryName.Replace("\\", StorageUtils.DELIMITER) + StorageUtils.DELIMITER + file.Name, newDirectoryName.Replace("\\", StorageUtils.DELIMITER) + StorageUtils.DELIMITER + file.Name, GxFileType.Public); + Rename(directoryName.Replace("\\", StorageUtils.DELIMITER) + StorageUtils.DELIMITER + file.Name, newDirectoryName.Replace("\\", StorageUtils.DELIMITER) + StorageUtils.DELIMITER + file.Name, GxFileType.PublicRead); } s3DirectoryInfo.Delete(); } @@ -446,7 +542,7 @@ public bool GetMessageFromException(Exception ex, SdtMessages_Message msg) } } - public bool GetObjectNameFromURL(string url, out string objectName) + public bool TryGetObjectNameFromURL(string url, out string objectName) { if (url.StartsWith(StorageUri)) { @@ -456,5 +552,11 @@ public bool GetObjectNameFromURL(string url, out string objectName) objectName = null; return false; } + + public override string GetName() + { + return Name; + } + } } diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXAzureStorage/AzureStorageExternalProvider.cs b/dotnet/src/dotnetframework/Providers/Storage/GXAzureStorage/AzureStorageExternalProvider.cs index 811b079db..036ec89e0 100644 --- a/dotnet/src/dotnetframework/Providers/Storage/GXAzureStorage/AzureStorageExternalProvider.cs +++ b/dotnet/src/dotnetframework/Providers/Storage/GXAzureStorage/AzureStorageExternalProvider.cs @@ -14,8 +14,10 @@ namespace GeneXus.Storage.GXAzureStorage { - public class AzureStorageExternalProvider : ExternalProvider + public class AzureStorageExternalProvider : ExternalProviderBase, ExternalProvider { + public static String Name = "AZUREBS"; //Azure Blob Storage + const string ACCOUNT_NAME = "ACCOUNT_NAME"; const string ACCESS_KEY = "ACCESS_KEY"; const string PUBLIC_CONTAINER = "PUBLIC_CONTAINER_NAME"; @@ -26,22 +28,35 @@ public class AzureStorageExternalProvider : ExternalProvider CloudBlobContainer PublicContainer { get; set; } CloudBlobContainer PrivateContainer { get; set; } CloudBlobClient Client { get; set; } + public string StorageUri { get { return $"https://{Account}.blob.core.windows.net"; } } - public AzureStorageExternalProvider() - : this(ServiceFactory.GetGXServices().Get(GXServices.STORAGE_SERVICE)) { } + public override string GetName() + { + return Name; + } - public AzureStorageExternalProvider(GXService providerService) + public AzureStorageExternalProvider() : this(null) { - Account = CryptoImpl.Decrypt(providerService.Properties.Get(ACCOUNT_NAME)); - Key = CryptoImpl.Decrypt(providerService.Properties.Get(ACCESS_KEY)); + } - string publicContainer = CryptoImpl.Decrypt(providerService.Properties.Get(PUBLIC_CONTAINER)); - string privateContainer = CryptoImpl.Decrypt(providerService.Properties.Get(PRIVATE_CONTAINER)); + public AzureStorageExternalProvider(GXService providerService) : base(providerService) + { + Initialize(); + } + private void Initialize() + { + Account = GetEncryptedPropertyValue(ACCOUNT_NAME); + Key = GetEncryptedPropertyValue(ACCESS_KEY); + + string publicContainer = GetEncryptedPropertyValue(PUBLIC_CONTAINER); + string privateContainer = GetEncryptedPropertyValue(PRIVATE_CONTAINER); + + StorageCredentials credentials = new StorageCredentials(Account, Key); CloudStorageAccount storageAccount = new CloudStorageAccount(credentials, true); @@ -76,7 +91,8 @@ public string Upload(string localFile, string externalFileName, GxFileType fileT private CloudBlockBlob GetCloudBlockBlob(string externalFileName, GxFileType fileType) { CloudBlockBlob blob; - if (fileType.HasFlag(GxFileType.Private)) + + if (IsPrivateFile(fileType)) { blob = PrivateContainer.GetBlockBlobReference(externalFileName); } @@ -87,10 +103,25 @@ private CloudBlockBlob GetCloudBlockBlob(string externalFileName, GxFileType fil return blob; } - - private bool IsPrivateFile(GxFileType fileType) + + private bool IsPrivateFile(GxFileType acl) { - return fileType.HasFlag(GxFileType.Private); + if (acl.HasFlag(GxFileType.Private)) + { + return true; + } + else if (acl.HasFlag(GxFileType.PublicRead)) + { + return false; + } + else if (this.defaultAcl == GxFileType.Private) + { + return true; + } + else + { + return false; + } } public string Get(string objectName, GxFileType fileType, int urlMinutes) @@ -103,6 +134,12 @@ public string Get(string objectName, GxFileType fileType, int urlMinutes) return string.Empty; } + public string GetUrl(string objectName, GxFileType fileType, int urlMinutes = 0) + { + CloudBlockBlob blob = GetCloudBlockBlob(objectName, fileType); + return GetURL(blob, fileType, urlMinutes); + } + private string GetURL(CloudBlockBlob blob, GxFileType fileType, int urlMinutes = 0) { string url = StorageUri + StorageUtils.DELIMITER + blob.Container.Name + StorageUtils.DELIMITER + StorageUtils.EncodeUrl(blob.Name); @@ -110,7 +147,7 @@ private string GetURL(CloudBlockBlob blob, GxFileType fileType, int urlMinutes = { SharedAccessBlobPolicy sasConstraints = new SharedAccessBlobPolicy(); sasConstraints.SharedAccessStartTime = DateTime.UtcNow; - sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes((urlMinutes > 0) ? urlMinutes : 60 * 24 * 7); + sasConstraints.SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(ResolveExpiration(urlMinutes).TotalMinutes); sasConstraints.Permissions = SharedAccessBlobPermissions.Read; url += blob.GetSharedAccessSignature(sasConstraints); } @@ -146,10 +183,11 @@ public string Copy(string objectName, GxFileType sourceFileType, string newName, public string Upload(string fileName, Stream stream, GxFileType fileType) { CloudBlockBlob blob = GetCloudBlockBlob(fileName, fileType); - if (Path.GetExtension(fileName).Equals(".tmp")) - blob.Properties.ContentType = "image/jpeg"; - else - blob.Properties.ContentType = MimeMapping.GetMimeMapping(fileName); + + if (TryGetContentType(fileName, out string mimeType)) + { + blob.Properties.ContentType = mimeType; + } blob.UploadFromStreamAsync(stream).GetAwaiter().GetResult(); return GetURL(blob, fileType); @@ -165,8 +203,14 @@ public string Copy(string sourceUrl, string newName, string tableName, string fi targetBlob.Metadata["Table"] = tableName; targetBlob.Metadata["Field"] = fieldName; targetBlob.Metadata["KeyValue"] = StorageUtils.EncodeUrl(newName); + + if (TryGetContentType(newName, out string mimeType, DEFAULT_CONTENT_TYPE)) + { + targetBlob.Properties.ContentType = mimeType; + } targetBlob.StartCopyAsync(sourceBlob).GetAwaiter().GetResult(); + targetBlob.SetPropertiesAsync().GetAwaiter().GetResult(); //Required to apply new object metadata return GetURL(targetBlob, fileType); } return string.Empty; @@ -254,15 +298,15 @@ public void RenameDirectory(string directoryName, string newDirectoryName) { CloudBlockBlob blob = (CloudBlockBlob)item; fileName = Path.GetFileName(blob.Name); - Copy(blob.Name, GxFileType.Public, newDirectoryName + fileName, GxFileType.Public); - Delete(blob.Name, GxFileType.Public); + Copy(blob.Name, GxFileType.PublicRead, newDirectoryName + fileName, GxFileType.PublicRead); + Delete(blob.Name, GxFileType.PublicRead); } else if (item.GetType() == typeof(CloudPageBlob)) { CloudPageBlob pageBlob = (CloudPageBlob)item; fileName = Path.GetFileName(pageBlob.Name); - Copy(directoryName + fileName, GxFileType.Public, newDirectoryName + fileName, GxFileType.Public); + Copy(directoryName + fileName, GxFileType.PublicRead, newDirectoryName + fileName, GxFileType.PublicRead); } else if (item.GetType() == typeof(CloudBlobDirectory)) @@ -337,7 +381,7 @@ public string Save(Stream fileStream, string fileName, string tableName, string return Upload(newName, fileStream, fileType); } - public bool GetObjectNameFromURL(string url, out string objectName) + public bool TryGetObjectNameFromURL(string url, out string objectName) { string baseUrl = StorageUri + StorageUtils.DELIMITER + PublicContainer.Name + StorageUtils.DELIMITER; if (url.StartsWith(baseUrl)) @@ -345,13 +389,19 @@ public bool GetObjectNameFromURL(string url, out string objectName) objectName = url.Replace(baseUrl, string.Empty); return true; } + baseUrl = StorageUri + StorageUtils.DELIMITER + PrivateContainer.Name + StorageUtils.DELIMITER; + if (url.StartsWith(baseUrl)) + { + objectName = url.Replace(baseUrl, string.Empty); + return true; + } objectName = null; return false; } public string GetBaseURL() { - return StorageUri + StorageUtils.DELIMITER + PublicContainer.Name + StorageUtils.DELIMITER; + return StorageUri + StorageUtils.DELIMITER + PrivateContainer.Name + StorageUtils.DELIMITER; } } diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/ExternalProviderBluemix.cs b/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/ExternalProviderBluemix.cs deleted file mode 100644 index 058182ba4..000000000 --- a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/ExternalProviderBluemix.cs +++ /dev/null @@ -1,1434 +0,0 @@ -using GeneXus.Configuration; -using GeneXus.Services; -using JSIStudios.SimpleRESTServices.Client; -using log4net; -using net.openstack.Core; -using net.openstack.Core.Caching; -using net.openstack.Core.Domain; -using net.openstack.Core.Domain.Mapping; -using net.openstack.Core.Exceptions; -using net.openstack.Core.Exceptions.Response; -using net.openstack.Core.Providers; -using net.openstack.Core.Validators; -using net.openstack.Providers.Rackspace; -using net.openstack.Providers.Rackspace.Exceptions; -using net.openstack.Providers.Rackspace.Objects; -using net.openstack.Providers.Rackspace.Validators; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenStack.Authentication; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using Thread = System.Threading.Thread; -using GeneXus.Utils; -using GeneXus.Encryption; - -namespace GeneXus.Storage.GXBluemix -{ - public class ExternalProviderBluemix : ExternalProvider - { - const string USER = "STORAGE_PROVIDER_USER"; - const string PASSWORD = "STORAGE_PROVIDER_PASSWORD"; - const string SERVER_URL = "SERVER_URL"; - const string PROJECT_ID = "PROJECT_ID"; - const string REGION = "STORAGE_PROVIDER_REGION"; - const string PRIVATE_BUCKET = "PRIVATE_BUCKET_NAME"; - const string PUBLIC_BUCKET = "PUBLIC_BUCKET_NAME"; - const string FOLDER = "FOLDER_NAME"; - - BluemixFilesProvider Client { get; set; } - string PublicBucket { get; set; } - string PrivateBucket { get; set; } - string Folder { get; set; } - string AuthToken { get; set; } - public string StorageUri { get; set; } - string PrivateTempKeyUrl { get; set; } - - public ExternalProviderBluemix() - : this(ServiceFactory.GetGXServices().Get(GXServices.STORAGE_SERVICE)) - { - } - - public ExternalProviderBluemix(GXService providerService) - { - var identityEndpoint = new Uri(providerService.Properties.Get(SERVER_URL)); - CloudIdentityWithProject identity = new CloudIdentityWithProject - { - Username = CryptoImpl.Decrypt(providerService.Properties.Get(USER)), - Password = CryptoImpl.Decrypt(providerService.Properties.Get(PASSWORD)), - ProjectName = providerService.Properties.Get(PROJECT_ID), - }; - - BlueMixIdentityProvider identityProvider = new BlueMixIdentityProvider(identityEndpoint, identity); - - GetStorageEndpoint(identityProvider, identity); - - Client = new BluemixFilesProvider(null, providerService.Properties.Get(REGION), identityProvider, null); - PublicBucket = CryptoImpl.Decrypt(providerService.Properties.Get(PUBLIC_BUCKET)); - PrivateBucket = CryptoImpl.Decrypt(providerService.Properties.Get(PRIVATE_BUCKET)); - Folder = providerService.Properties.Get(FOLDER); - AuthToken = identityProvider.AuthToken; - - CreateBuckets(); - CreateFolder(Folder); - } - - public ExternalProviderBluemix(string proj, string url, string user, string pass, string bucket, string region) - { - var identityEndpoint = new Uri(url); - CloudIdentityWithProject identity = new CloudIdentityWithProject - { - Username = user, - Password = pass, - ProjectName = proj, - }; - - BlueMixIdentityProvider identityProvider = new BlueMixIdentityProvider(identityEndpoint, identity); - - GetStorageEndpoint(identityProvider, identity); - - Client = new BluemixFilesProvider(null, region, identityProvider, null); - PublicBucket = bucket; - AuthToken = identityProvider.AuthToken; - - CreateBuckets(); - } - - private void GetStorageEndpoint(BlueMixIdentityProvider identityProvider, CloudIdentityWithProject identity) - { - UserAccessV3 user = identityProvider.GetUserAccess(identity); - var catalog = user.Catalog; - Endpoint objectStorageEndpoint = null; - foreach (Catalog service in catalog) - if (service.Type == "object-store") - if (service.Endpoints.Where(e => e.Region_id == "dallas").Any()) - objectStorageEndpoint = service.Endpoints.Where(e => e.Region_id == "dallas").First(); - - StorageUri = objectStorageEndpoint.Url; - - if (String.IsNullOrEmpty(StorageUri)) - throw new Exception("Can't find the object storage endpoint, please check the credentials in the storage configuration."); - } - - private Dictionary CreateObjectMetadata(string tableName, string fieldName, string key) - { - Dictionary metadata = new Dictionary(); - metadata.Add("Table", tableName); - metadata.Add("Field", fieldName); - metadata.Add("KeyValue", key); - return metadata; - } - - private void CreateBuckets() - { - Dictionary headers = new Dictionary(); - headers.Add("X-Auth-Token", AuthToken); - PrivateTempKeyUrl = Guid.NewGuid().ToString(); - headers.Add("X-Container-Meta-Temp-URL-Key", PrivateTempKeyUrl); - Client.CreateContainer(PrivateBucket, headers); - - headers = new Dictionary(); - headers.Add("X-Auth-Token", AuthToken); - headers.Add("X-Container-Read", ".r:*"); - Client.CreateContainer(PublicBucket, headers); - } - - private void CreateFolder(string folder, string table = null, string field = null) - { - string name = StorageUtils.NormalizeDirectoryName(folder); - if (table != null) - name += table + StorageUtils.DELIMITER; - if (field != null) - name += field + StorageUtils.DELIMITER; - using (var stream = new MemoryStream()) - { - Client.CreateObject(PublicBucket, stream, name, "application/directory"); - } - } - - public void Download(string externalFileName, string localFile, GxFileType fileType) - { - string bucket = GetBucket(fileType); - string localDirectory = Path.GetDirectoryName(localFile); - string localFileName = Path.GetFileName(localFile); - Client.GetObjectSaveToFile(bucket, localDirectory, externalFileName, localFileName); - } - - public string Upload(string localFile, string externalFileName, GxFileType fileType) - { - string bucket = GetBucket(fileType); - Client.CreateObjectFromFile(bucket, localFile, externalFileName); - return StorageUri + StorageUtils.DELIMITER + bucket + StorageUtils.DELIMITER + StorageUtils.EncodeUrl(externalFileName); - } - - private string GetBucket(GxFileType fileType) - { - return (fileType.HasFlag(GxFileType.Private)) ? PrivateBucket : PublicBucket; - } - - public string Get(string externalFileName, GxFileType fileType, int urlMinutes) - { - string bucket = GetBucket(fileType); - if (Exists(externalFileName, fileType)) - { - if (fileType.HasFlag(GxFileType.Private)) - return Client.CreateTemporaryPublicUri(HttpMethod.GET, bucket, externalFileName, PrivateTempKeyUrl, DateTimeOffset.Now.AddMinutes(urlMinutes)).ToString(); - return StorageUri + StorageUtils.DELIMITER + bucket + StorageUtils.DELIMITER + StorageUtils.EncodeUrl(externalFileName); - } - return string.Empty; - } - - public void Delete(string objectName, GxFileType fileType) - { - Client.DeleteObject(GetBucket(fileType), objectName); - } - - public bool Exists(string objectName, GxFileType fileType) - { - try - { - Client.GetObjectMetaData(GetBucket(fileType), objectName); - return true; - } - catch (ItemNotFoundException) - { - return false; - } - } - - public string Rename(string objectName, string newName, GxFileType fileType) - { - string bucket = GetBucket(fileType); - Copy(objectName, fileType, newName, fileType); - Delete(objectName, fileType); - return StorageUri + StorageUtils.DELIMITER + bucket + StorageUtils.DELIMITER + StorageUtils.EncodeUrl(newName); - } - - public string Copy(string objectName, GxFileType sourceFileType, string newName, GxFileType destFileType) - { - string sourceBucket = GetBucket(sourceFileType); - string destBucket = GetBucket(destFileType); - Client.CopyObject(sourceBucket, objectName, destBucket, newName); - return Get(objectName, destFileType, 0); - } - - public string Upload(string fileName, Stream stream, GxFileType fileType) - { - string bucket = GetBucket(fileType); - if (Path.GetExtension(fileName).Equals(".tmp")) - Client.CreateObject(bucket, stream, fileName, "image / jpeg"); - else - Client.CreateObject(bucket, stream, fileName); - - return Get(fileName, fileType, 0); - } - - public string Copy(string url, string newName, string tableName, string fieldName, GxFileType fileType) - { - string bucket = GetBucket(fileType); - string resourceKey = Folder + StorageUtils.DELIMITER + tableName + StorageUtils.DELIMITER + fieldName + StorageUtils.DELIMITER + newName; - CreateFolder(Folder, tableName, fieldName); - url = StorageUtils.DecodeUrl(url.Replace(StorageUri + StorageUtils.DELIMITER + bucket + StorageUtils.DELIMITER, "")); - - Copy(url, fileType, resourceKey, fileType); - Client.UpdateObjectMetadata(bucket, resourceKey, CreateObjectMetadata(tableName, fieldName, resourceKey)); - - return Get(resourceKey, fileType, 0); - } - - public Stream GetStream(string objectName, GxFileType fileType) - { - MemoryStream stream = new MemoryStream(); - Client.GetObject(GetBucket(fileType), objectName, stream); - return stream; - } - - public string GetDirectory(string directoryName) - { - directoryName = StorageUtils.NormalizeDirectoryName(directoryName); - if (ExistsDirectory(directoryName)) - return PublicBucket + StorageUtils.DELIMITER + directoryName; - else - return ""; - } - - public long GetLength(string objectName, GxFileType fileType) - { - foreach (ContainerObject obj in Client.ListObjects(GetBucket(fileType))) - if (obj.Name.Equals(objectName)) - return obj.Bytes; - return 0; - } - - public DateTime GetLastModified(string objectName, GxFileType fileType) - { - foreach (ContainerObject obj in Client.ListObjects(GetBucket(fileType))) - if (obj.Name.Equals(objectName)) - return obj.LastModified.UtcDateTime; - return new DateTime(); - } - - public void CreateDirectory(string directoryName) - { - CreateFolder(directoryName); - } - - public void DeleteDirectory(string directoryName) - { - List objs = new List(); - foreach (ContainerObject obj in Client.ListObjects(PublicBucket, prefix: StorageUtils.NormalizeDirectoryName(directoryName))) - { - objs.Add(obj.Name); - } - objs.Add(directoryName); - Client.DeleteObjects(PublicBucket, objs); - } - - public bool ExistsDirectory(string directoryName) - { - List directories = GetDirectories(); - return directories.Contains(directoryName) || directories.Contains(StorageUtils.NormalizeDirectoryName(directoryName)); - } - - public void RenameDirectory(string directoryName, string newDirectoryName) - { - directoryName = StorageUtils.NormalizeDirectoryName(directoryName); - newDirectoryName = StorageUtils.NormalizeDirectoryName(newDirectoryName); - - foreach (ContainerObject obj in Client.ListObjects(PublicBucket, prefix: directoryName)) - { - Client.CopyObject(PublicBucket, obj.Name, PublicBucket, obj.Name.Replace(directoryName, newDirectoryName)); - } - DeleteDirectory(directoryName); - } - - public List GetSubDirectories(string directoryName) - { - return GetDirectories(StorageUtils.NormalizeDirectoryName(directoryName)); - } - - private List GetDirectories(string directoryName = null) - { - List subdir = new List(); - foreach (ContainerObject obj in Client.ListObjects(PublicBucket, prefix: directoryName)) - { - if (directoryName == null) - { - string dir = ""; - string[] parts = obj.Name.Split(Convert.ToChar(StorageUtils.DELIMITER)); - for (int i = 0; i < parts.Length - 1; i++) - { - dir += parts[i] + StorageUtils.DELIMITER; - if (!subdir.Contains(dir)) - subdir.Add(dir); - } - } - else - { - string name = obj.Name.Replace(directoryName, ""); - int i = name.IndexOf(StorageUtils.DELIMITER); - if (i != -1) - { - name = name.Substring(0, i); - string dir = StorageUtils.NormalizeDirectoryName(directoryName + name); - if (!subdir.Contains(dir)) - subdir.Add(dir); - } - } - - } - if (directoryName != null) - { - subdir.Remove(directoryName); - } - return subdir; - } - - public List GetFiles(string directoryName, string filter = "") - { - directoryName = StorageUtils.NormalizeDirectoryName(directoryName); - List files = new List(); - foreach (ContainerObject obj in Client.ListObjects(PublicBucket, prefix: directoryName)) - { - if (IsFile(obj, directoryName) && (String.IsNullOrEmpty(filter) || obj.Name.Contains(filter))) - files.Add(obj.Name); - } - return files; - } - - private bool IsFile(ContainerObject obj, string directory = null) - { - char delimiter = Convert.ToChar(StorageUtils.DELIMITER); - if (directory == null) - return obj.Name.Split(delimiter).Length == 1; - else - return obj.Name.Replace(StorageUtils.NormalizeDirectoryName(directory), "").Split(delimiter).Length == 1 && !String.IsNullOrEmpty(obj.Name.Replace(StorageUtils.NormalizeDirectoryName(directory), "").Split(delimiter)[0]); - } - - public bool GetMessageFromException(Exception ex, SdtMessages_Message msg) - { - try - { - ResponseException rex = (ResponseException)ex; - msg.gxTpr_Id = rex.Response.Status; - return true; - } - catch (Exception) - { - return false; - } - } - - public string Save(Stream fileStream, string fileName, string tableName, string fieldName, GxFileType fileType) - { - CreateFolder(Folder, tableName, fieldName); - string resourceKey = Folder + StorageUtils.DELIMITER + tableName + StorageUtils.DELIMITER + fieldName + StorageUtils.DELIMITER + fileName; - return Upload(resourceKey, fileStream, fileType); - } - - public bool GetObjectNameFromURL(string url, out string objectName) - { - string baseUrl = StorageUri + StorageUtils.DELIMITER + PublicBucket + StorageUtils.DELIMITER; - if (url.StartsWith(baseUrl)) - { - objectName = url.Replace(baseUrl, string.Empty); - return true; - } - objectName = null; - return false; - } - public string GetBaseURL() - { - return StorageUri + StorageUtils.DELIMITER + PublicBucket + StorageUtils.DELIMITER + Folder + StorageUtils.DELIMITER; - } - - #region Extension of openstack.net (issue: https://github.com/openstacknetsdk/openstack.net/issues/503) to enable openstack Identity API v3: http://developer.openstack.org/api-ref-identity-v3.html - - public class IdentityTokenV3 : IdentityToken - { - public new string Id { get; set; } - } - - public class BlueMixIdentityProvider : CloudIdentityProvider - { - public string AuthToken { get; set; } - new ICache TokenCache { get; } - - public BlueMixIdentityProvider(Uri urlBase, CloudIdentity defaultIdentity) - : this(urlBase, defaultIdentity, null, null) - { - AuthToken = null; - } - - public BlueMixIdentityProvider(Uri urlBase, CloudIdentity defaultIdentity, IRestService restService, ICache tokenCache) - : base(defaultIdentity, restService, tokenCache, urlBase) - { - AuthToken = null; - if (urlBase == null) - throw new ArgumentNullException(nameof(urlBase)); - } - - public new IdentityToken GetToken(CloudIdentity identity, bool forceCacheRefresh = false) - { - CheckIdentity(identity); - - GetUserAccess(identity, forceCacheRefresh); - - IdentityTokenV3 token = new IdentityTokenV3(); - token.Id = AuthToken; - return token; - } - public new UserAccessV3 GetUserAccess(CloudIdentity identity, bool forceCacheRefresh = false) - { - identity = identity ?? DefaultIdentity; - - CloudIdentityWithProject identityWithProject = identity as CloudIdentityWithProject; - - if (string.IsNullOrEmpty(identityWithProject.Password)) - throw new NotSupportedException(string.Format("The {0} identity must specify a password.", typeof(CloudIdentityWithProject))); - if (!string.IsNullOrEmpty(identityWithProject.APIKey)) - throw new NotSupportedException(string.Format("The {0} identity does not support API key authentication.", typeof(CloudIdentityWithProject))); - - var auth = Authorization.CreateCredentials(identityWithProject); - var projectId = identityWithProject.ProjectId != null ? JToken.FromObject(identityWithProject.ProjectId) : string.Empty; - - var response = ExecuteRESTRequest(identity, new Uri(UrlBase, "/v3/auth/tokens"), HttpMethod.POST, auth, isTokenRequest: true); - if (response == null || response.Data == null) - return null; - - AuthToken = response.Headers[0].Value; - JToken userAccessObject = response.Data["token"]; - if (userAccessObject == null) - return null; - - UserAccessV3 access = userAccessObject.ToObject(); - if (access == null) - return null; - - string key = string.Format("{0}:{1}/{2}", UrlBase, identityWithProject.ProjectId, identityWithProject.Username); - - return access; - } - - protected override string LookupServiceTypeKey(IServiceType serviceType) - { - return serviceType.Type; - } - - } - - public class Authorization - { - //Generates the JSON with the credentials to authenticate to the Identity API v3 - public static JObject CreateCredentials(CloudIdentityWithProject identityWithProject) - { - return new JObject( - new JProperty("auth", new JObject( - new JProperty("identity", new JObject( - new JProperty("methods", new JArray("password")), - new JProperty("password", new JObject( - new JProperty("user", new JObject( - new JProperty("id", JValue.CreateString(identityWithProject.Username)), - new JProperty("password", JValue.CreateString(identityWithProject.Password)))))))), - new JProperty("scope", new JObject( - new JProperty("project", new JObject( - new JProperty("id", JValue.CreateString(identityWithProject.ProjectName))))))))); - } - } - - internal class EncodeDecodeProviderAux : IEncodeDecodeProvider - { - - private static readonly EncodeDecodeProviderAux _default = new EncodeDecodeProviderAux(); - - public static EncodeDecodeProviderAux Default - { - get - { - return _default; - } - } - - public string UrlEncode(string stringToEncode) - { - if (stringToEncode == null) - return null; - - return UriUtility.UriEncode(stringToEncode, UriPart.AnyUrl); - } - - public string UrlDecode(string stringToDecode) - { - if (stringToDecode == null) - return null; - - return UriUtility.UriDecode(stringToDecode); - } - } - - public class BluemixFilesProvider : CloudFilesProvider - { - private readonly IObjectStorageValidator _cloudFilesValidator; - private readonly IEncodeDecodeProvider _encodeDecodeProvider; - private readonly IObjectStorageMetadataProcessor _cloudFilesMetadataProcessor; - private readonly IStatusParser _statusParser; - private readonly IObjectMapper _bulkDeletionResultMapper; - public BluemixFilesProvider(CloudIdentity defaultIdentity, string defaultRegion, IIdentityProvider identityProvider, IRestService restService) - : base(defaultIdentity, defaultRegion, identityProvider, restService) - { - _cloudFilesValidator = CloudFilesValidator.Default; - _encodeDecodeProvider = EncodeDecodeProviderAux.Default; - _cloudFilesMetadataProcessor = CloudFilesMetadataProcessor.Default; - _statusParser = HttpStatusCodeParser.Default; - _bulkDeletionResultMapper = new BulkDeletionResultMapper(_statusParser); - } - - public new string GetServiceEndpointCloudFiles(CloudIdentity identity, string region = null, bool useInternalUrl = false) - { - string serviceType = "object-store"; - - if (serviceType == null) - throw new ArgumentNullException("serviceType"); - if (string.IsNullOrEmpty(serviceType)) - throw new ArgumentException("serviceType cannot be empty"); - CheckIdentity(identity); - - identity = GetDefaultIdentity(identity); - - var userAccess = ((BlueMixIdentityProvider)IdentityProvider).GetUserAccess(identity); - - if (userAccess == null || userAccess.Catalog == null) - throw new UserAuthenticationException("Unable to authenticate user and retrieve authorized service endpoints."); - - IEnumerable services = userAccess.Catalog.Where(sc => string.Equals(sc.Type, serviceType, StringComparison.OrdinalIgnoreCase)); - - IEnumerable> endpoints = - services.SelectMany(service => service.Endpoints.Select(endpoint => Tuple.Create(service, endpoint))); - - string effectiveRegion = region; - if (string.IsNullOrEmpty(effectiveRegion)) - { - if (!string.IsNullOrEmpty(DefaultRegion)) - effectiveRegion = DefaultRegion; - else if (!string.IsNullOrEmpty(userAccess.User.DefaultRegion)) - effectiveRegion = userAccess.User.DefaultRegion; - } - - IEnumerable> regionEndpoints = - endpoints.Where(i => string.Equals(i.Item2.Region ?? string.Empty, effectiveRegion ?? string.Empty, StringComparison.OrdinalIgnoreCase)); - - if (regionEndpoints.Any()) - endpoints = regionEndpoints; - else - endpoints = endpoints.Where(i => string.IsNullOrEmpty(i.Item2.Region)); - - if (effectiveRegion == null && !endpoints.Any()) - throw new NoDefaultRegionSetException("No region was provided, the service does not provide a region-independent endpoint, and there is no default region set for the user's account."); - - Tuple serviceEndpoint = endpoints.Where(e => e.Item2.Url.Contains("softlayer")).FirstOrDefault(); - if (serviceEndpoint == null) - throw new UserAuthorizationException("The user does not have access to the requested service or region."); - - return serviceEndpoint.Item2.Url; - } - public new ObjectStore CreateContainer(string container, Dictionary headers = null, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - var urlPath = new Uri(string.Format("{0}/{1}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container))); - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.PUT, headers: headers); - - switch (response.StatusCode) - { - case HttpStatusCode.Created: - return ObjectStore.ContainerCreated; - - case HttpStatusCode.Accepted: - return ObjectStore.ContainerExists; - - default: - throw new ResponseException(string.Format("Unexpected status {0} returned by Create Container.", response.StatusCode), response); - } - } - - public new void CreateObject(string container, Stream stream, string objectName, string contentType = null, int chunkSize = 4096, Dictionary headers = null, string region = null, Action progressUpdated = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - if (chunkSize < 0) - throw new ArgumentOutOfRangeException(nameof(chunkSize)); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - - if (stream.Length > LargeFileBatchThreshold) - { - throw new ArgumentException("objectName is too big"); - } - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - RequestSettings settings = BuildDefaultRequestSettings(); - settings.ChunkRequest = true; - settings.ContentType = contentType; - - StreamRESTRequest(identity, urlPath, HttpMethod.PUT, stream, chunkSize, headers: headers, progressUpdated: progressUpdated, requestSettings: settings); - } - - public new void CreateObjectFromFile(string container, string filePath, string objectName = null, string contentType = null, int chunkSize = 4096, Dictionary headers = null, string region = null, Action progressUpdated = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (filePath == null) - throw new ArgumentNullException(nameof(filePath)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(filePath)) - throw new ArgumentException("filePath cannot be empty"); - if (chunkSize < 0) - throw new ArgumentOutOfRangeException(nameof(chunkSize)); - CheckIdentity(identity); - - if (string.IsNullOrEmpty(objectName)) - objectName = Path.GetFileName(filePath); - - using (var stream = File.OpenRead(filePath)) - { - CreateObject(container, stream, objectName, contentType, chunkSize, headers, region, progressUpdated, useInternalUrl, identity); - } - } - public new void UpdateObjectMetadata(string container, string objectName, Dictionary metadata, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (metadata == null) - throw new ArgumentNullException(nameof(metadata)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - - var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair m in metadata) - { - if (string.IsNullOrEmpty(m.Key)) - throw new ArgumentException("metadata cannot contain any empty keys"); - - headers.Add(ObjectMetaDataPrefix + m.Key, EncodeUnicodeValue(m.Value)); - } - - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - RequestSettings settings = BuildDefaultRequestSettings(); - // make sure the content type is not changed by the metadata operation - settings.ContentType = null; - - ExecuteRESTRequest(identity, urlPath, HttpMethod.POST, headers: headers, settings: settings); - } - - public new Dictionary GetObjectMetaData(string container, string objectName, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.HEAD); - - var processedHeaders = _cloudFilesMetadataProcessor.ProcessMetadata(response.Headers); - - return processedHeaders[ProcessedHeadersMetadataKey]; - } - - public new void GetObject(string container, string objectName, Stream outputStream, int chunkSize = 4096, Dictionary headers = null, string region = null, bool verifyEtag = false, Action progressUpdated = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (outputStream == null) - throw new ArgumentNullException(nameof(outputStream)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - if (chunkSize < 0) - throw new ArgumentOutOfRangeException(nameof(chunkSize)); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - long? initialPosition; - try - { - initialPosition = outputStream.Position; - } - catch (NotSupportedException) - { - if (verifyEtag) - throw; - - initialPosition = null; - } - - // This flag indicates whether the outputStream needs to be set prior to copying data. - // See: https://github.com/openstacknetsdk/openstack.net/issues/297 - bool requiresPositionReset = false; - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.GET, (resp, isError) => - { - if (resp == null) - return new Response(0, null, null); - - string body; - - if (!isError) - { - using (var respStream = resp.GetResponseStream()) - { - // The second condition will throw a proper NotSupportedException if the position - // cannot be checked. - if (requiresPositionReset && outputStream.Position != initialPosition) - outputStream.Position = initialPosition.Value; - - requiresPositionReset = true; - CopyStream(respStream, outputStream, chunkSize, progressUpdated); - } - - body = "[Binary]"; - } - else - { - using (StreamReader reader = new StreamReader(resp.GetResponseStream())) - { - body = reader.ReadToEnd(); - } - } - - var respHeaders = resp.Headers.AllKeys.Select(key => new HttpHeader(key, resp.GetResponseHeader(key))).ToList(); - - return new Response(resp.StatusCode, respHeaders, body); - }, headers: headers); - - } - - public new void GetObjectSaveToFile(string container, string saveDirectory, string objectName, string fileName = null, int chunkSize = 65536, Dictionary headers = null, string region = null, bool verifyEtag = false, Action progressUpdated = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (saveDirectory == null) - throw new ArgumentNullException(nameof(saveDirectory)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(saveDirectory)) - throw new ArgumentException("saveDirectory cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - if (chunkSize < 0) - throw new ArgumentOutOfRangeException(nameof(chunkSize)); - CheckIdentity(identity); - - if (string.IsNullOrEmpty(fileName)) - fileName = objectName; - - var filePath = Path.Combine(saveDirectory, string.IsNullOrEmpty(fileName) ? objectName : fileName); - - try - { - using (var fileStream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite)) - { - GetObject(container, objectName, fileStream, chunkSize, headers, region, verifyEtag, progressUpdated, useInternalUrl, identity); - } - } - catch (InvalidETagException) - { - File.Delete(filePath); - throw; - } - } - - private static string EncodeUnicodeValue(string value) - { - if (value == null) - return null; - - return Encoding.GetEncoding("ISO-8859-1").GetString(Encoding.UTF8.GetBytes(value)); - } - internal void CheckResponse(Response response) - { - if (response == null) - throw new ArgumentNullException(nameof(response)); - - ResponseCodeValidator.Validate(response); - } - protected new Response ExecuteRESTRequest(CloudIdentity identity, Uri absoluteUri, HttpMethod method, object body = null, Dictionary queryStringParameter = null, Dictionary headers = null, bool isRetry = false, bool isTokenRequest = false, RequestSettings settings = null) - { - if (absoluteUri == null) - throw new ArgumentNullException(nameof(absoluteUri)); - CheckIdentity(identity); - - return ExecuteRESTRequest>(identity, absoluteUri, method, body, queryStringParameter, headers, isRetry, isTokenRequest, settings, RestService.Execute); - } - protected new Response ExecuteRESTRequest(CloudIdentity identity, Uri absoluteUri, HttpMethod method, object body = null, Dictionary queryStringParameter = null, Dictionary headers = null, bool isRetry = false, bool isTokenRequest = false, RequestSettings settings = null) - { - if (absoluteUri == null) - throw new ArgumentNullException(nameof(absoluteUri)); - CheckIdentity(identity); - - return ExecuteRESTRequest(identity, absoluteUri, method, body, queryStringParameter, headers, isRetry, isTokenRequest, settings, RestService.Execute); - } - - protected new Response ExecuteRESTRequest(CloudIdentity identity, Uri absoluteUri, HttpMethod method, Func buildResponseCallback, object body = null, Dictionary queryStringParameter = null, Dictionary headers = null, bool isRetry = false, bool isTokenRequest = false, RequestSettings settings = null) - { - if (absoluteUri == null) - throw new ArgumentNullException(nameof(absoluteUri)); - CheckIdentity(identity); - - return ExecuteRESTRequest(identity, absoluteUri, method, body, queryStringParameter, headers, isRetry, isTokenRequest, settings, - (uri, requestMethod, requestBody, requestHeaders, requestQueryParams, requestSettings) => RestService.Execute(uri, requestMethod, buildResponseCallback, requestBody, requestHeaders, requestQueryParams, requestSettings)); - } - - private T ExecuteRESTRequest(CloudIdentity identity, Uri absoluteUri, HttpMethod method, object body, Dictionary queryStringParameter, Dictionary headers, bool isRetry, bool isTokenRequest, RequestSettings requestSettings, - Func, Dictionary, RequestSettings, T> callback) where T : Response - { - if (absoluteUri == null) - throw new ArgumentNullException(nameof(absoluteUri)); - CheckIdentity(identity); - - identity = GetDefaultIdentity(identity); - - if (requestSettings == null) - requestSettings = BuildDefaultRequestSettings(); - - if (headers == null) - headers = new Dictionary(); - - if (!isTokenRequest) - headers["X-Auth-Token"] = ((IdentityTokenV3)((BlueMixIdentityProvider)IdentityProvider).GetToken(identity, isRetry)).Id; - - string bodyStr = null; - if (body != null) - { - if (body is JObject) - bodyStr = body.ToString(); - else if (body is string) - bodyStr = body as string; - else - bodyStr = JsonConvert.SerializeObject(body, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); - } - - if (string.IsNullOrEmpty(requestSettings.UserAgent)) - requestSettings.UserAgent = DefaultUserAgent; - - var response = callback(absoluteUri, method, bodyStr, headers, queryStringParameter, requestSettings); - - // on errors try again 1 time. - if (response.StatusCode == HttpStatusCode.Unauthorized && !isRetry && !isTokenRequest) - { - return ExecuteRESTRequest(identity, absoluteUri, method, body, queryStringParameter, headers, true, isTokenRequest, requestSettings, callback); - } - - CheckResponse(response); - - return response; - } - - protected new Response StreamRESTRequest(CloudIdentity identity, Uri absoluteUri, HttpMethod method, Stream stream, int chunkSize, long maxReadLength = 0, Dictionary queryStringParameter = null, Dictionary headers = null, bool isRetry = false, RequestSettings requestSettings = null, Action progressUpdated = null) - { - if (absoluteUri == null) - throw new ArgumentNullException(nameof(absoluteUri)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - if (chunkSize <= 0) - throw new ArgumentOutOfRangeException(nameof(chunkSize)); - if (maxReadLength < 0) - throw new ArgumentOutOfRangeException(nameof(maxReadLength)); - CheckIdentity(identity); - - identity = GetDefaultIdentity(identity); - - if (requestSettings == null) - requestSettings = BuildDefaultRequestSettings(); - - requestSettings.Timeout = TimeSpan.FromMilliseconds(14400000); // Need to pass this in. - - if (headers == null) - headers = new Dictionary(); - - headers["X-Auth-Token"] = ((IdentityTokenV3)((BlueMixIdentityProvider)IdentityProvider).GetToken(identity, isRetry)).Id; ; - - if (string.IsNullOrEmpty(requestSettings.UserAgent)) - requestSettings.UserAgent = DefaultUserAgent; - - long? initialPosition; - try - { - initialPosition = stream.Position; - } - catch (NotSupportedException) - { - initialPosition = null; - } - - Response response; - try - { - response = RestService.Stream(absoluteUri, method, stream, chunkSize, maxReadLength, headers, queryStringParameter, requestSettings, progressUpdated); - } - catch (ProtocolViolationException) - { - ServicePoint servicePoint = ServicePointManager.FindServicePoint(absoluteUri); - if (servicePoint.ProtocolVersion < HttpVersion.Version11) - { - // this is a workaround for issue #333 - // https://github.com/openstacknetsdk/openstack.net/issues/333 - // http://stackoverflow.com/a/22976809/138304 - int maxIdleTime = servicePoint.MaxIdleTime; - servicePoint.MaxIdleTime = 0; - Thread.Sleep(1); - servicePoint.MaxIdleTime = maxIdleTime; - } - - response = RestService.Stream(absoluteUri, method, stream, chunkSize, maxReadLength, headers, queryStringParameter, requestSettings, progressUpdated); - } - - // on errors try again 1 time. - if (response.StatusCode == HttpStatusCode.Unauthorized && !isRetry && initialPosition != null) - { - bool canRetry; - - try - { - if (stream.Position != initialPosition.Value) - stream.Position = initialPosition.Value; - - canRetry = true; - } - catch (NotSupportedException) - { - // unable to retry the operation - canRetry = false; - } - - if (canRetry) - { - return StreamRESTRequest(identity, absoluteUri, method, stream, chunkSize, maxReadLength, queryStringParameter, headers, true, requestSettings, progressUpdated); - } - } - - CheckResponse(response); - - return response; - } - - public new void DeleteObject(string container, string objectName, Dictionary headers = null, bool deleteSegments = true, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - - Dictionary objectHeader = null; - if (deleteSegments) - objectHeader = GetObjectHeaders(container, objectName, region, useInternalUrl, identity); - - if (deleteSegments && objectHeader != null && objectHeader.Any(h => h.Key.Equals(ObjectManifest, StringComparison.OrdinalIgnoreCase))) - { - var objects = ListObjects(container, region: region, useInternalUrl: useInternalUrl, - identity: identity); - - if (objects != null && objects.Any()) - { - var segments = objects.Where(f => f.Name.StartsWith(string.Format("{0}.seg", objectName))); - var delObjects = new List { objectName }; - if (segments.Any()) - delObjects.AddRange(segments.Select(s => s.Name)); - - DeleteObjects(container, delObjects, headers, region, useInternalUrl, identity); - } - else - throw new Exception(string.Format("Object \"{0}\" in container \"{1}\" does not exist.", objectName, container)); - } - else - { - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - ExecuteRESTRequest(identity, urlPath, HttpMethod.DELETE, headers: headers); - } - } - - public new Dictionary GetObjectHeaders(string container, string objectName, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container), _encodeDecodeProvider.UrlEncode(objectName))); - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.HEAD); - - var processedHeaders = _cloudFilesMetadataProcessor.ProcessMetadata(response.Headers); - - return processedHeaders[ProcessedHeadersHeaderKey]; - } - - public new IEnumerable ListObjects(string container, int? limit = null, string marker = null, string markerEnd = null, string prefix = null, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (limit < 0) - throw new ArgumentOutOfRangeException(nameof(limit)); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - var urlPath = new Uri(string.Format("{0}/{1}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(container))); - - var queryStringParameter = new Dictionary(); - - if (limit != null) - queryStringParameter.Add("limit", limit.ToString()); - - if (!string.IsNullOrEmpty(marker)) - queryStringParameter.Add("marker", marker); - - if (!string.IsNullOrEmpty(markerEnd)) - queryStringParameter.Add("end_marker", markerEnd); - - if (!string.IsNullOrEmpty(prefix)) - queryStringParameter.Add("prefix", prefix); - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.GET, null, queryStringParameter); - - if (response == null || response.Data == null) - return null; - - return response.Data; - } - - public new void CopyObject(string sourceContainer, string sourceObjectName, string destinationContainer, string destinationObjectName, string destinationContentType = null, Dictionary headers = null, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (sourceContainer == null) - throw new ArgumentNullException(nameof(sourceContainer)); - if (sourceObjectName == null) - throw new ArgumentNullException(nameof(sourceObjectName)); - if (destinationContainer == null) - throw new ArgumentNullException(nameof(destinationContainer)); - if (destinationObjectName == null) - throw new ArgumentNullException(nameof(destinationObjectName)); - if (string.IsNullOrEmpty(sourceContainer)) - throw new ArgumentException("sourceContainer cannot be empty"); - if (string.IsNullOrEmpty(sourceObjectName)) - throw new ArgumentException("sourceObjectName cannot be empty"); - if (string.IsNullOrEmpty(destinationContainer)) - throw new ArgumentException("destinationContainer cannot be empty"); - if (string.IsNullOrEmpty(destinationObjectName)) - throw new ArgumentException("destinationObjectName cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(sourceContainer); - _cloudFilesValidator.ValidateObjectName(sourceObjectName); - - _cloudFilesValidator.ValidateContainerName(destinationContainer); - _cloudFilesValidator.ValidateObjectName(destinationObjectName); - - var urlPath = new Uri(string.Format("{0}/{1}/{2}", GetServiceEndpointCloudFiles(identity, region, useInternalUrl), _encodeDecodeProvider.UrlEncode(sourceContainer), _encodeDecodeProvider.UrlEncode(sourceObjectName))); - - if (headers == null) - headers = new Dictionary(StringComparer.OrdinalIgnoreCase); - - headers.Add(Destination, string.Format("{0}/{1}", UriUtility.UriEncode(destinationContainer, UriPart.AnyUrl, Encoding.UTF8), UriUtility.UriEncode(destinationObjectName, UriPart.AnyUrl, Encoding.UTF8))); - - RequestSettings settings = BuildDefaultRequestSettings(); - if (destinationContentType != null) - { - settings.ContentType = destinationContentType; - } - else - { - // make sure to preserve the content type during the copy operation - settings.ContentType = null; - } - - ExecuteRESTRequest(identity, urlPath, HttpMethod.COPY, headers: headers, settings: settings); - } - - public new void DeleteObjects(string container, IEnumerable objects, Dictionary headers = null, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objects == null) - throw new ArgumentNullException(nameof(objects)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - - BulkDelete(objects.Select(o => new KeyValuePair(container, o)), headers, region, useInternalUrl, identity); - } - - public new void BulkDelete(IEnumerable> items, Dictionary headers = null, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - var urlPath = new Uri(string.Format("{0}/?bulk-delete", GetServiceEndpointCloudFiles(identity, region, useInternalUrl))); - - var encoded = items.Select( - pair => - { - if (string.IsNullOrEmpty(pair.Key)) - throw new ArgumentException("items", "items cannot contain any entries with a null or empty key (container name)"); - if (string.IsNullOrEmpty(pair.Value)) - throw new ArgumentException("items", "items cannot contain any entries with a null or empty value (object name)"); - _cloudFilesValidator.ValidateContainerName(pair.Key); - _cloudFilesValidator.ValidateObjectName(pair.Value); - - return string.Format("/{0}/{1}", _encodeDecodeProvider.UrlEncode(pair.Key), _encodeDecodeProvider.UrlEncode(pair.Value)); - }); - var body = string.Join("\n", encoded.ToArray()); - - var response = ExecuteRESTRequest(identity, urlPath, HttpMethod.POST, body: body, headers: headers, settings: new JSIStudios.SimpleRESTServices.Client.Json.JsonRequestSettings { ContentType = "text/plain" }); - - Status status; - if (_statusParser.TryParse(response.Data.Status, out status)) - { - if (status.Code != 200 && !response.Data.Errors.Any()) - { - response.Data.AllItems = encoded; - throw new BulkDeletionException(response.Data.Status, _bulkDeletionResultMapper.Map(response.Data)); - } - } - } - - public new Uri CreateTemporaryPublicUri(HttpMethod method, string container, string objectName, string key, DateTimeOffset expiration, string region = null, bool useInternalUrl = false, CloudIdentity identity = null) - { - if (container == null) - throw new ArgumentNullException(nameof(container)); - if (objectName == null) - throw new ArgumentNullException(nameof(objectName)); - if (key == null) - throw new ArgumentNullException(nameof(key)); - if (string.IsNullOrEmpty(container)) - throw new ArgumentException("container cannot be empty"); - if (string.IsNullOrEmpty(objectName)) - throw new ArgumentException("objectName cannot be empty"); - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("key cannot be empty"); - CheckIdentity(identity); - - _cloudFilesValidator.ValidateContainerName(container); - _cloudFilesValidator.ValidateObjectName(objectName); - - Uri baseAddress = new Uri(GetServiceEndpointCloudFiles(identity, region, useInternalUrl), UriKind.Absolute); - - StringBuilder body = new StringBuilder(); - body.Append(method.ToString().ToUpperInvariant()).Append('\n'); - body.Append(ToTimestamp(expiration) / 1000).Append('\n'); - body.Append(baseAddress.PathAndQuery).Append('/').Append(container).Append('/').Append(objectName); - - using (HMAC hmac = HMAC.Create()) - { - hmac.HashName = "SHA1"; - hmac.Key = Encoding.UTF8.GetBytes(key); - byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(body.ToString())); - string sig = string.Join(string.Empty, Array.ConvertAll(hash, i => i.ToString("x2"))); - string expires = (ToTimestamp(expiration) / 1000).ToString(); - - return new Uri(String.Format("{0}/{1}/{2}?temp_url_sig={3}&temp_url_expires={4}", baseAddress, container, objectName, sig, expires)); - } - } - private long ToTimestamp(DateTimeOffset dateTimeOffset) - { - DateTimeOffset Epoch = new DateTimeOffset(new DateTime(1970, 1, 1), TimeSpan.Zero); - return (long)(dateTimeOffset - Epoch).TotalMilliseconds; - } - - } - internal class BulkDeletionResultMapper : IObjectMapper - { - private readonly IStatusParser _statusParser; - - public BulkDeletionResultMapper(IStatusParser statusParser) - { - _statusParser = statusParser; - } - - public BulkDeletionResults Map(BulkDeleteResponse from) - { - var successfulObjects = from.AllItems.Where(i => !from.IsItemError(i)); - var failedObjects = from.Errors.Select(e => - { - var eParts = e.ToArray(); - Status errorStatus; - string errorItem; - - if (eParts.Length != 2) - { - errorStatus = new Status(0, "Unknown"); - errorItem = string.Format("The error array has an unexpected length. Array: {0}", string.Join("||", eParts)); - } - else - { - errorItem = eParts[1]; - if (!_statusParser.TryParse(eParts[0], out errorStatus)) - { - errorItem = eParts[0]; - if (!_statusParser.TryParse(eParts[1], out errorStatus)) - { - errorStatus = new Status(0, "Unknown"); - errorItem = string.Format("The error array is in an unknown format. Array: {0}", string.Join("||", eParts)); - } - } - } - - return new BulkDeletionFailedObject(errorItem, errorStatus); - }); - - return new BulkDeletionResults(successfulObjects, failedObjects); - } - - public BulkDeleteResponse Map(BulkDeletionResults to) - { - throw new NotImplementedException(); - } - } - internal class CloudFilesMetadataProcessor : IObjectStorageMetadataProcessor - { - - private static readonly CloudFilesMetadataProcessor _default = new CloudFilesMetadataProcessor(); - - public static CloudFilesMetadataProcessor Default - { - get - { - return _default; - } - } - - public virtual Dictionary> ProcessMetadata(IList httpHeaders) - { - if (httpHeaders == null) - throw new ArgumentNullException(nameof(httpHeaders)); - - var pheaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - var metadata = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var header in httpHeaders) - { - if (header == null) - throw new ArgumentException("httpHeaders cannot contain any null values"); - if (string.IsNullOrEmpty(header.Key)) - throw new ArgumentException("httpHeaders cannot contain any values with a null or empty key"); - - if (header.Key.StartsWith(CloudFilesProvider.AccountMetaDataPrefix, StringComparison.OrdinalIgnoreCase)) - { - metadata.Add(header.Key.Substring(CloudFilesProvider.AccountMetaDataPrefix.Length), DecodeUnicodeValue(header.Value)); - } - else if (header.Key.StartsWith(CloudFilesProvider.ContainerMetaDataPrefix, StringComparison.OrdinalIgnoreCase)) - { - metadata.Add(header.Key.Substring(CloudFilesProvider.ContainerMetaDataPrefix.Length), DecodeUnicodeValue(header.Value)); - } - else if (header.Key.StartsWith(CloudFilesProvider.ObjectMetaDataPrefix, StringComparison.OrdinalIgnoreCase)) - { - metadata.Add(header.Key.Substring(CloudFilesProvider.ObjectMetaDataPrefix.Length), DecodeUnicodeValue(header.Value)); - } - else - { - pheaders.Add(header.Key, header.Value); - } - } - - var processedHeaders = new Dictionary>(StringComparer.OrdinalIgnoreCase) - { - {CloudFilesProvider.ProcessedHeadersHeaderKey, pheaders}, - {CloudFilesProvider.ProcessedHeadersMetadataKey, metadata} - }; - - return processedHeaders; - } - - private string DecodeUnicodeValue(string value) - { - return Encoding.UTF8.GetString(Encoding.GetEncoding("ISO-8859-1").GetBytes(value)); - } - } - - [JsonObject(MemberSerialization.OptIn)] - internal class BulkDeleteResponse - { - [JsonProperty("Number Not Found")] - public int NumberNotFound { get; set; } - - [JsonProperty("Response Status")] - public string Status { get; set; } - - [JsonProperty("Errors")] - public IEnumerable> Errors { get; set; } - - [JsonProperty("Number Deleted")] - public int NumberDeleted { get; set; } - - [JsonProperty("Response Body")] - public string ResponseBody { get; set; } - - public IEnumerable AllItems { get; set; } - - public bool IsItemError(string s) - { - return Errors.Any(e => e.Any(e2 => e2.Equals(s))); - } - } - #region Json Classes - [JsonObject(MemberSerialization.OptIn)] - public class Endpoint - { - [JsonProperty("region_id")] - public string Region_id { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("region")] - public string Region { get; set; } - [JsonProperty("interface")] - public string @Interface { get; set; } - [JsonProperty("id")] - public string Id { get; set; } - } - [JsonObject(MemberSerialization.OptIn)] - public class Catalog - { - [JsonProperty("endpoints")] - public Endpoint[] Endpoints { get; set; } - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } - - [JsonObject(MemberSerialization.OptIn)] - public class UserAccessV3 : UserAccess - { - [JsonProperty("catalog")] - public Catalog[] Catalog { get; set; } - - [JsonExtensionData] - public IDictionary _additionalData; - } - #endregion - #endregion - - } -} - diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GXBluemix.csproj b/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GXBluemix.csproj deleted file mode 100644 index 37f002ed7..000000000 --- a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GXBluemix.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - net462 - GXBluemix - GXBluemix - CA1501;CA1812 - Bluemix - GeneXus.Bluemix - - - - - - - - - - 0.2.5 - - - - - - - - - - \ No newline at end of file diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GlobalSuppressions.cs b/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GlobalSuppressions.cs deleted file mode 100644 index 6ca697c8d..000000000 --- a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/GlobalSuppressions.cs +++ /dev/null @@ -1,18 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.Authorization")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.BluemixFilesProvider")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.BlueMixIdentityProvider")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.Catalog")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.Endpoint")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.IdentityTokenV3")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "", Scope = "type", Target = "~T:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.UserAccessV3")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "", Scope = "member", Target = "~P:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.Endpoint.Region_id")] - -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.Catalog.Endpoints")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "", Scope = "member", Target = "~P:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.UserAccessV3.Catalog")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "", Scope = "member", Target = "~M:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.BluemixFilesProvider.BulkDelete(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{System.String,System.String}},System.Collections.Generic.Dictionary{System.String,System.String},System.String,System.Boolean,net.openstack.Core.Domain.CloudIdentity)")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "", Scope = "member", Target = "~M:GeneXus.Storage.GXBluemix.ExternalProviderBluemix.BluemixFilesProvider.GetServiceEndpointCloudFiles(net.openstack.Core.Domain.CloudIdentity,System.String,System.Boolean)~System.String")] \ No newline at end of file diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/Properties/AssemblyInfo.cs b/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/Properties/AssemblyInfo.cs deleted file mode 100644 index bb7c4dab9..000000000 --- a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("GXBluemix")] - - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -// The following GUID is for the ID of the typelib if this project is exposed to COM -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: NeutralResourcesLanguageAttribute("en")] diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/app.config b/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/app.config deleted file mode 100644 index 1cd55066a..000000000 --- a/dotnet/src/dotnetframework/Providers/Storage/GXBluemix/app.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXGoogleCloud/ExternalProviderGoogle.cs b/dotnet/src/dotnetframework/Providers/Storage/GXGoogleCloud/ExternalProviderGoogle.cs index 7e72b04b7..e0ab68018 100644 --- a/dotnet/src/dotnetframework/Providers/Storage/GXGoogleCloud/ExternalProviderGoogle.cs +++ b/dotnet/src/dotnetframework/Providers/Storage/GXGoogleCloud/ExternalProviderGoogle.cs @@ -15,56 +15,70 @@ namespace GeneXus.Storage.GXGoogleCloud { - public class ExternalProviderGoogle : ExternalProvider + public class ExternalProviderGoogle : ExternalProviderBase, ExternalProvider { - const int BUCKET_EXISTS = 409; + public static String Name = "GOOGLECS"; //Google Cloud Storage + const int BUCKET_EXISTS = 409; const int OBJECT_NOT_FOUND = 404; const string APPLICATION_NAME = "APPLICATION_NAME"; const string PROJECT_ID = "PROJECT_ID"; const string KEY = "KEY"; const string BUCKET = "BUCKET_NAME"; - const string FOLDER = "FOLDER_NAME"; + StorageClient Client { get; set; } StorageService Service { get; set; } String Project { get; set; } String Bucket { get; set; } - String Folder { get; set; } UrlSigner Signer { get; set; } + + public string StorageUri { get { return $"https://{Bucket}.storage.googleapis.com/"; } } - public ExternalProviderGoogle() - : this(ServiceFactory.GetGXServices().Get(GXServices.STORAGE_SERVICE)) - { - } + public override string GetName() + { + return Name; - public ExternalProviderGoogle(GXService providerService) - { + } + + public ExternalProviderGoogle() : this(null) + { + } + + public ExternalProviderGoogle(GXService providerService) : base(providerService) + { + Initialize(); + } + + + private void Initialize() + { GoogleCredential credentials; - using (Stream stream = KeyStream(CryptoImpl.Decrypt(providerService.Properties.Get(KEY)))) - { - credentials = GoogleCredential.FromStream(stream).CreateScoped(StorageService.Scope.CloudPlatform); - } + string key = GetEncryptedPropertyValue(KEY); - using (Stream stream = KeyStream(CryptoImpl.Decrypt(providerService.Properties.Get(KEY)))) + using (Stream stream = KeyStream(key)) { - Signer = UrlSigner.FromServiceAccountData(stream); - } + credentials = GoogleCredential.FromStream(stream).CreateScoped(StorageService.Scope.CloudPlatform); + } - Client = StorageClient.Create(credentials); + using (Stream stream = KeyStream(key)) + { + Signer = UrlSigner.FromServiceAccountData(stream); + } + + Client = StorageClient.Create(credentials); Service = new StorageService(new BaseClientService.Initializer() { HttpClientInitializer = credentials, - ApplicationName = providerService.Properties.Get(APPLICATION_NAME) + ApplicationName = GetPropertyValue(APPLICATION_NAME) }); - Project = providerService.Properties.Get(PROJECT_ID); - Bucket = CryptoImpl.Decrypt(providerService.Properties.Get(BUCKET)); - Folder = providerService.Properties.Get(FOLDER); + Bucket = GetEncryptedPropertyValue(BUCKET); + Project = GetPropertyValue(PROJECT_ID); CreateBucket(); CreateFolder(Folder); @@ -82,25 +96,33 @@ private Stream KeyStream(string key) private void CreateBucket() { - //objects in bucket are public by default - ObjectAccessControl defaultAccess = new ObjectAccessControl(); - defaultAccess.Entity = "allUsers"; - defaultAccess.Role = "READER"; - - Bucket bucket = new Bucket(); - bucket.Name = Bucket; - bucket.DefaultObjectAcl = new List { defaultAccess }; - - try - { - Client.CreateBucket(Project, bucket); - } - catch (GoogleApiException ex) - { - if (ex.Error.Code != BUCKET_EXISTS) - throw ex; - } - + try + { + //objects in bucket are public by default + ObjectAccessControl defaultAccess = new ObjectAccessControl(); + defaultAccess.Entity = "allUsers"; + defaultAccess.Role = "READER"; + + Bucket bucket = new Bucket(); + bucket.Name = Bucket; + bucket.DefaultObjectAcl = new List { defaultAccess }; + + if (Client.GetBucket(Bucket) == null) + { + try + { + Client.CreateBucket(Project, bucket); + } + catch (GoogleApiException ex) + { + if (ex.Error.Code != BUCKET_EXISTS) + throw ex; + } + } + } + catch (Exception) { + //We should not fail when Bucket cannot be created. Bucket should be created beforehand as Credentials may not allow Bucket creation. + } } private void CreateFolder(String name, string table = null, string field = null) @@ -129,13 +151,13 @@ public string Upload(string fileName, Stream stream, GxFileType fileType) obj.Name = fileName; obj.Bucket = Bucket; - if (Path.GetExtension(fileName).Equals(".tmp")) - obj.ContentType = "image/jpeg"; - else - obj.ContentType = MimeMapping.GetMimeMapping(fileName); - + if (TryGetContentType(fileName, out string mimeType, DEFAULT_CONTENT_TYPE)) + { + obj.ContentType = mimeType; + } + Client.UploadObject(obj, stream, GetUploadOptions(fileType)); - return StorageUri + StorageUtils.EncodeUrl(fileName); + return GetURL(fileName, fileType); } private Dictionary CreateObjectMetadata(string tableName, string fieldName, string name) @@ -150,45 +172,55 @@ private Dictionary CreateObjectMetadata(string tableName, string public string Copy(string objectName, GxFileType sourceFileType, string newName, GxFileType targetFileType) { Client.CopyObject(Bucket, objectName, Bucket, newName, GetCopyOptions(targetFileType)); - return GetURL(objectName, targetFileType, 0); + return GetURL(objectName, targetFileType); } - private static CopyObjectOptions GetCopyOptions(GxFileType fileType) + + + private PredefinedObjectAcl GetPredefinedAcl(GxFileType acl) { - CopyObjectOptions options = new CopyObjectOptions(); - if (fileType.HasFlag(GxFileType.Private)) - options.DestinationPredefinedAcl = PredefinedObjectAcl.ProjectPrivate; - else - options.DestinationPredefinedAcl = PredefinedObjectAcl.PublicRead; - return options; + PredefinedObjectAcl objectPredefinedObjectAcl = PredefinedObjectAcl.PublicRead; + if (acl.HasFlag(GxFileType.Private)) + { + objectPredefinedObjectAcl = PredefinedObjectAcl.ProjectPrivate; + } + else if (acl.HasFlag(GxFileType.PublicRead)) + { + objectPredefinedObjectAcl = PredefinedObjectAcl.PublicRead; + } + else if (this.defaultAcl == GxFileType.Private) + { + objectPredefinedObjectAcl = PredefinedObjectAcl.Private; + } + return objectPredefinedObjectAcl; + } + + private CopyObjectOptions GetCopyOptions(GxFileType acl) + { + return new CopyObjectOptions() { DestinationPredefinedAcl = GetPredefinedAcl(acl) }; + } + + private UploadObjectOptions GetUploadOptions(GxFileType acl) + { + return new UploadObjectOptions() { PredefinedAcl = GetPredefinedAcl(acl) }; } public void Delete(string objectName, GxFileType fileType) { - Google.Apis.Storage.v1.Data.Object obj = new Google.Apis.Storage.v1.Data.Object(); - obj.Name = objectName; - obj.Bucket = Bucket; - Client.DeleteObject(obj); + Client.DeleteObject(Bucket, objectName); } public string Upload(string localFile, string objectName, GxFileType fileType) { using (FileStream stream = new FileStream(localFile, FileMode.Open)) { - Google.Apis.Storage.v1.Data.Object obj = Client.UploadObject(Bucket, objectName, "application/octet-stream", stream, GetUploadOptions(fileType)); - return obj.MediaLink; + TryGetContentType(objectName, out string mimeType, DEFAULT_CONTENT_TYPE); + Google.Apis.Storage.v1.Data.Object obj = Client.UploadObject(Bucket, objectName, mimeType, stream, GetUploadOptions(fileType)); + return GetURL(objectName, fileType); } } - private static UploadObjectOptions GetUploadOptions(GxFileType fileType) - { - UploadObjectOptions options = new UploadObjectOptions(); - if (fileType.HasFlag(GxFileType.Private)) - options.PredefinedAcl = PredefinedObjectAcl.ProjectPrivate; - else - options.PredefinedAcl = PredefinedObjectAcl.PublicRead; - return options; - } + public void Download(string objectName, string localFile, GxFileType fileType) { @@ -222,6 +254,7 @@ public bool Exists(string objectName, GxFileType fileType) public string Get(string objectName, GxFileType fileType, int urlMinutes) { + objectName = objectName.Replace("%2F", "/"); //Google Cloud Storage Bug. https://github.com/googleapis/google-cloud-dotnet/pull/3677 if (Exists(objectName, fileType)) { return GetURL(objectName, fileType, urlMinutes); @@ -229,10 +262,28 @@ public string Get(string objectName, GxFileType fileType, int urlMinutes) return string.Empty; } - private string GetURL(string objectName, GxFileType fileType, int urlMinutes = 60 * 24 * 7) + public string GetUrl(string objectName, GxFileType fileType, int urlMinutes) + { + return GetURL(objectName, fileType, urlMinutes); + } + + private bool IsPrivateResource(GxFileType fileType) { if (fileType.HasFlag(GxFileType.Private)) - return Signer.Sign(Bucket, StorageUtils.EncodeUrl(objectName), TimeSpan.FromMinutes(urlMinutes), HttpMethod.Get); + { + return true; + } + if (fileType.HasFlag(GxFileType.PublicRead)) + { + return false; + } + return (this.defaultAcl == GxFileType.PublicRead) ? false : true; + } + + private string GetURL(string objectName, GxFileType fileType, int urlMinutes = 0) + { + if (IsPrivateResource(fileType)) + return Signer.Sign(Bucket, StorageUtils.EncodeUrl(objectName), ResolveExpiration(urlMinutes), HttpMethod.Get); else { return StorageUri + StorageUtils.EncodeUrl(objectName); @@ -245,8 +296,13 @@ public string Copy(string url, string newName, string tableName, string fieldNam url = StorageUtils.DecodeUrl(url.Replace(StorageUri, string.Empty)); Google.Apis.Storage.v1.Data.Object obj = Client.CopyObject(Bucket, url, Bucket, newName, GetCopyOptions(fileType)); + obj.Metadata = CreateObjectMetadata(tableName, fieldName, newName); - Client.UpdateObject(obj); + if (TryGetContentType(newName, out string mimeType, DEFAULT_CONTENT_TYPE)) + { + obj.ContentType = mimeType; + } + Client.UpdateObject(obj); return GetURL(newName, fileType, 0); } @@ -264,7 +320,8 @@ public DateTime GetLastModified(string objectName, GxFileType fileType) public long GetLength(string objectName, GxFileType fileType) { - return (long)Client.GetObject(Bucket, objectName).Size.GetValueOrDefault(); + var obj = Client.GetObject(Bucket, objectName); + return (long)obj.Size.GetValueOrDefault(); } public void CreateDirectory(string directoryName) @@ -279,12 +336,12 @@ public void DeleteDirectory(string directoryName) foreach (Google.Apis.Storage.v1.Data.Object item in Client.ListObjects(Bucket, directoryName)) { if (!item.Name.EndsWith(StorageUtils.DELIMITER)) - Delete(item.Name, GxFileType.Public); + Delete(item.Name, GxFileType.PublicRead); } foreach (string subdir in GetSubDirectories(directoryName)) DeleteDirectory(subdir); - if (Exists(directoryName, GxFileType.Public)) - Delete(directoryName, GxFileType.Public); + if (Exists(directoryName, GxFileType.PublicRead)) + Delete(directoryName, GxFileType.PublicRead); } public bool ExistsDirectory(string directoryName) @@ -377,13 +434,13 @@ public void RenameDirectory(string directoryName, string newDirectoryName) { if (IsFile(item.Name)) { - Copy(item.Name, GxFileType.Public, item.Name.Replace(directoryName, newDirectoryName), GxFileType.Public); - Delete(item.Name, GxFileType.Public); + Copy(item.Name, GxFileType.PublicRead, item.Name.Replace(directoryName, newDirectoryName), GxFileType.PublicRead); + Delete(item.Name, GxFileType.PublicRead); } } CreateDirectory(newDirectoryName); - if (Exists(directoryName, GxFileType.Public)) - Delete(directoryName, GxFileType.Public); + if (Exists(directoryName, GxFileType.PublicRead)) + Delete(directoryName, GxFileType.PublicRead); request.PageToken = response.NextPageToken; } while (response.NextPageToken != null); foreach (string subdir in GetSubDirectories(directoryName)) @@ -417,12 +474,20 @@ public string Save(Stream fileStream, string fileName, string tableName, string fileName = Folder + StorageUtils.DELIMITER + tableName + StorageUtils.DELIMITER + fieldName + StorageUtils.DELIMITER + fileName; return Upload(fileName, fileStream, fileType); } - public bool GetObjectNameFromURL(string url, out string objectName) + public bool TryGetObjectNameFromURL(string url, out string objectName) { string baseUrl = StorageUri; if (url.StartsWith(baseUrl)) { objectName = url.Replace(baseUrl, string.Empty); + objectName = objectName.Replace("%2F", "/"); //Google Cloud Storage Bug. https://github.com/googleapis/google-cloud-dotnet/pull/3677 + return true; + } + baseUrl = $"https://storage.googleapis.com/{Bucket}/"; + if (url.StartsWith(baseUrl)) + { + objectName = url.Replace(baseUrl, string.Empty); + objectName = objectName.Replace("%2F", "/"); //Google Cloud Storage Bug. https://github.com/googleapis/google-cloud-dotnet/pull/3677 return true; } objectName = null; diff --git a/dotnet/src/dotnetframework/Providers/Storage/GXOpenStack/ExternalProviderOpenStack.cs b/dotnet/src/dotnetframework/Providers/Storage/GXOpenStack/ExternalProviderOpenStack.cs index a8aaddff1..d85e22127 100644 --- a/dotnet/src/dotnetframework/Providers/Storage/GXOpenStack/ExternalProviderOpenStack.cs +++ b/dotnet/src/dotnetframework/Providers/Storage/GXOpenStack/ExternalProviderOpenStack.cs @@ -151,6 +151,11 @@ public string Get(string externalFileName, GxFileType fileType, int urlMinutes) return GetURL(externalFileName, fileType); } + public string GetUrl(string externalFileName, GxFileType fileType, int urlMinutes) + { + return GetURL(externalFileName, fileType); + } + public void Delete(string objectName, GxFileType fileType) { openstackFilesProvider.DeleteObject(GetBucket(fileType), objectName); @@ -362,7 +367,7 @@ public string GetBaseURL() return storageUrl + StorageUtils.DELIMITER + publicBucketName + StorageUtils.DELIMITER; } - public bool GetObjectNameFromURL(string url, out string objectName) + public bool TryGetObjectNameFromURL(string url, out string objectName) { string baseUrl = storageUrl + StorageUtils.DELIMITER + publicBucketName + StorageUtils.DELIMITER; if (url.StartsWith(baseUrl)) diff --git a/dotnet/test/DotNetUnitTest/DotNetUnitTest.csproj b/dotnet/test/DotNetUnitTest/DotNetUnitTest.csproj index c883b01b2..e1e18d01d 100644 --- a/dotnet/test/DotNetUnitTest/DotNetUnitTest.csproj +++ b/dotnet/test/DotNetUnitTest/DotNetUnitTest.csproj @@ -30,18 +30,30 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + Always + + PreserveNewest + + Always + diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzurePrivateTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzurePrivateTest.cs new file mode 100644 index 000000000..7738d2b3a --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzurePrivateTest.cs @@ -0,0 +1,14 @@ +using GeneXus.Storage.GXAmazonS3; +using GeneXus.Storage.GXAzureStorage; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderAzurePrivateTest : ExternalProviderTest + { + public ExternalProviderAzurePrivateTest(): base(AzureStorageExternalProvider.Name, typeof(AzureStorageExternalProvider), true) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzureTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzureTest.cs new file mode 100644 index 000000000..c78b22364 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderAzureTest.cs @@ -0,0 +1,14 @@ +using GeneXus.Storage.GXAmazonS3; +using GeneXus.Storage.GXAzureStorage; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderAzureTest : ExternalProviderTest + { + public ExternalProviderAzureTest(): base(AzureStorageExternalProvider.Name, typeof(AzureStorageExternalProvider)) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGooglePrivateTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGooglePrivateTest.cs new file mode 100644 index 000000000..263869594 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGooglePrivateTest.cs @@ -0,0 +1,15 @@ +using GeneXus.Storage.GXAmazonS3; +using GeneXus.Storage.GXAzureStorage; +using GeneXus.Storage.GXGoogleCloud; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderGooglePrivateTest : ExternalProviderTest + { + public ExternalProviderGooglePrivateTest(): base(ExternalProviderGoogle.Name, typeof(ExternalProviderGoogle), true) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGoogleTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGoogleTest.cs new file mode 100644 index 000000000..c58dbb0a1 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderGoogleTest.cs @@ -0,0 +1,15 @@ +using GeneXus.Storage.GXAmazonS3; +using GeneXus.Storage.GXAzureStorage; +using GeneXus.Storage.GXGoogleCloud; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderGoogleTest : ExternalProviderTest + { + public ExternalProviderGoogleTest(): base(ExternalProviderGoogle.Name, typeof(ExternalProviderGoogle)) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderMinioTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderMinioTest.cs new file mode 100644 index 000000000..2695bd232 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderMinioTest.cs @@ -0,0 +1,13 @@ +using GeneXus.Storage.GXAmazonS3; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderMinioTest : ExternalProviderTest + { + public ExternalProviderMinioTest(): base(ExternalProviderS3.Name, typeof(ExternalProviderS3)) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderOracleTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderOracleTest.cs new file mode 100644 index 000000000..41e6ee3f0 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderOracleTest.cs @@ -0,0 +1,13 @@ +using GeneXus.Storage.GXAmazonS3; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderOracleTest : ExternalProviderTest + { + public ExternalProviderOracleTest(): base("ORACLE", typeof(ExternalProviderS3)) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3PrivateTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3PrivateTest.cs new file mode 100644 index 000000000..048cf4add --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3PrivateTest.cs @@ -0,0 +1,13 @@ +using GeneXus.Storage.GXAmazonS3; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderS3PrivateTest : ExternalProviderTest + { + public ExternalProviderS3PrivateTest(): base(ExternalProviderS3.Name, typeof(ExternalProviderS3), true) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3Test.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3Test.cs new file mode 100644 index 000000000..e53655069 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderS3Test.cs @@ -0,0 +1,13 @@ +using GeneXus.Storage.GXAmazonS3; +using UnitTesting; + +namespace DotNetUnitTest +{ + public class ExternalProviderS3Test : ExternalProviderTest + { + public ExternalProviderS3Test(): base(ExternalProviderS3.Name, typeof(ExternalProviderS3)) + { + } + + } +} diff --git a/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderTest.cs b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderTest.cs new file mode 100644 index 000000000..1dd2b991b --- /dev/null +++ b/dotnet/test/DotNetUnitTest/ExternalProvider/ExternalProviderTest.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using DotNetUnitTest; +using GeneXus.Services; +using GeneXus.Storage; +using Xunit; + + +#pragma warning disable CA1031 // Do not catch general exception types +namespace UnitTesting +{ + [Collection("Sequential")] + public abstract class ExternalProviderTest + { + + private GeneXus.Services.ExternalProvider provider; + private String TEST_SAMPLE_FILE_NAME = $"text{new Random().Next(1, 10000)}.txt"; + private String TEST_SAMPLE_FILE_PATH; + private bool defaultAclPrivate; + + public ExternalProviderTest(string providerName, Type externalProviderType, bool isPrivate) + { + defaultAclPrivate = isPrivate; + Environment.SetEnvironmentVariable($"STORAGE_{providerName}_DEFAULT_ACL", defaultAclPrivate ? GxFileType.Private.ToString() : GxFileType.PublicRead.ToString()); + bool testEnabled = Environment.GetEnvironmentVariable(providerName + "_TEST_ENABLED") == "true"; + + + Skip.IfNot(testEnabled, "Environment variables not set"); + provider = (GeneXus.Services.ExternalProvider)Activator.CreateInstance(externalProviderType); + + Assert.NotNull(provider); + + TEST_SAMPLE_FILE_PATH = Path.Combine("resources", TEST_SAMPLE_FILE_NAME).ToString(CultureInfo.InvariantCulture); + File.WriteAllText(TEST_SAMPLE_FILE_PATH, "This is a Sample Test from External Storage GeneXus .NET Generator Unit Tests"); + } + + public ExternalProviderTest(string providerName, Type externalProviderType) : this(providerName, externalProviderType, false) + { + + } + + [SkippableFact] + public void TestUploadPublicMethod() + { + String upload = provider.Upload(TEST_SAMPLE_FILE_PATH, TEST_SAMPLE_FILE_NAME, GxFileType.PublicRead); + EnsureUrl(upload, GxFileType.PublicRead); + } + + [SkippableFact] + public void TestUploadPrivateSubfolderMethod() + { + String upload = provider.Upload(TEST_SAMPLE_FILE_PATH, $"folder/folder2/folder3/{TEST_SAMPLE_FILE_NAME}", GxFileType.Private); + EnsureUrl(upload, GxFileType.Private); + } + + [SkippableFact] + public void TestUploadDefaultMethod() + { + String upload = provider.Upload(TEST_SAMPLE_FILE_PATH, TEST_SAMPLE_FILE_NAME, GxFileType.Default); + EnsureUrl(upload, GxFileType.Default); + } + + [SkippableFact] + public void TestUploadDefaultAttributeMethod() + { + String upload = provider.Upload(TEST_SAMPLE_FILE_PATH, TEST_SAMPLE_FILE_NAME, GxFileType.DefaultAttribute); + EnsureUrl(upload, GxFileType.DefaultAttribute); + } + + [SkippableFact] + public void TestUploadAndCopyDefault() + { + TestUploadAndCopyByAcl(GxFileType.Default, GxFileType.Default); + } + + [SkippableFact] + public void TestUploadAndCopyPrivate() + { + TestUploadAndCopyByAcl(GxFileType.Private, GxFileType.Private); + } + + [SkippableFact] + public void TestUploadAndCopyPublic() + { + TestUploadAndCopyByAcl(GxFileType.PublicRead, GxFileType.PublicRead); + } + + [SkippableFact] + public void TestUploadAndCopyMixed() + { + TestUploadAndCopyByAcl(GxFileType.Default, GxFileType.Private); + } + + [SkippableFact] + public void TestUploadAndCopyPrivateToPublic() + { + TestUploadAndCopyByAcl(GxFileType.Private, GxFileType.PublicRead); + } + + [SkippableFact] + public void TestUploadAndCopyPublicToPrivate() + { + TestUploadAndCopyByAcl(GxFileType.PublicRead, GxFileType.Private); + } + + public void TestUploadAndCopyByAcl(GxFileType aclUpload, GxFileType aclCopy) + { + string copySourceName = $"test-source-upload-and-copy_{new Random().Next()}.txt"; + String copyTargetName = $"test-upload-and-copy_{new Random().Next()}.txt"; + DeleteSafe(TEST_SAMPLE_FILE_PATH); + DeleteSafe(copyTargetName); + String upload = provider.Upload(TEST_SAMPLE_FILE_PATH, copySourceName, aclUpload); + EnsureUrl(upload, aclUpload); + + String copyUrl = TryGet(copyTargetName, aclCopy); + Assert.False(UrlExists(copyUrl), "URL cannot exist: " + copyUrl); + + provider.Copy(copySourceName, aclUpload, copyTargetName, aclCopy); + upload = provider.Get(copyTargetName, aclCopy, 100); + EnsureUrl(upload, aclCopy); + } + + [SkippableFact] + public void TestCopyMethod() + { + String copyFileName = "copy-text.txt"; + Copy(copyFileName, GxFileType.PublicRead); + } + + [SkippableFact] + public void TestCopyPrivateMethod() + { + String copyFileName = "copy-text-private.txt"; + Copy(copyFileName, GxFileType.Private); + } + + [SkippableFact] + public void TestMultimediaUpload() + { + string sourceFile = $"folder1/folder2/folder3{TEST_SAMPLE_FILE_NAME}"; + String copyFileName = "copy-text-private.txt"; + GxFileType acl = GxFileType.Private; + + provider.Upload(TEST_SAMPLE_FILE_PATH, sourceFile, acl); + String upload = provider.Get(sourceFile, acl, 100); + EnsureUrl(upload, acl); + + DeleteSafe(copyFileName); + upload = provider.Copy(sourceFile, copyFileName, "Table", "Field", acl); + + copyFileName = StorageFactory.GetProviderObjectAbsoluteUriSafe(provider, upload); + if (!copyFileName.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + upload = TryGet(copyFileName, acl); + } + EnsureUrl(upload, acl); + } + + [SkippableFact] + public void TestGetMethod() + { + TestUploadPublicMethod(); + String url = provider.Get(TEST_SAMPLE_FILE_NAME, GxFileType.PublicRead, 10); + EnsureUrl(url, GxFileType.PublicRead); + } + + [SkippableFact] + public void TestGetObjectName() + { + TestUploadPublicMethod(); + string url = provider.Get(TEST_SAMPLE_FILE_NAME, GxFileType.PublicRead, 10); + Assert.True(UrlExists(url)); + string objectName; + provider.TryGetObjectNameFromURL(url, out objectName); + Assert.Equal(TEST_SAMPLE_FILE_NAME, objectName); + } + + [SkippableFact] + public void TestDownloadMethod() + { + TestUploadPublicMethod(); + + String downloadPath = Path.Combine("resources", "test", TEST_SAMPLE_FILE_NAME); + try + { + File.Delete(downloadPath); + } + catch (Exception) { } + try + { + Directory.CreateDirectory(Path.Combine("resources", "test")); + } + catch (Exception) { } + provider.Download(TEST_SAMPLE_FILE_NAME, downloadPath, GxFileType.PublicRead); + Assert.True(File.Exists(downloadPath)); + } + + [SkippableFact] + public void TestDeleteFile() + { + GxFileType acl = GxFileType.PublicRead; + TestUploadPublicMethod(); + String url = TryGet(TEST_SAMPLE_FILE_NAME, acl); + EnsureUrl(url, acl); + provider.Delete(TEST_SAMPLE_FILE_NAME, acl); + + url = TryGet(TEST_SAMPLE_FILE_NAME, acl); + Assert.False(UrlExists(url)); + } + + + [SkippableFact] + public void TestDeleteFilePrivate() + { + GxFileType acl = GxFileType.Private; + provider.Upload(TEST_SAMPLE_FILE_PATH, TEST_SAMPLE_FILE_NAME, acl); + provider.Delete(TEST_SAMPLE_FILE_NAME, acl); + String url = TryGet(TEST_SAMPLE_FILE_NAME, acl); + Assert.False(UrlExists(url)); + } + + [SkippableFact] + public void TestUploadPrivateMethod() + { + GxFileType acl = GxFileType.Private; + String externalFileName = "text-private-2.txt"; + + DeleteSafe(externalFileName); + String signedUrl = provider.Upload(TEST_SAMPLE_FILE_PATH, externalFileName, acl); + EnsureUrl(signedUrl, acl); + signedUrl = provider.Get(externalFileName, acl, 10); + EnsureUrl(signedUrl, acl); + + } + + + [SkippableFact] + public void TestEmptyFolder() + { + GxFileType acl = GxFileType.PublicRead; + string folderName = $"folderTemp{new Random().Next(1, 100)}"; + + provider.DeleteDirectory(folderName); + + List urls = new List(); + + urls.Add(provider.Upload(TEST_SAMPLE_FILE_PATH, $"{folderName}/test1.png", acl)); + urls.Add(provider.Upload(TEST_SAMPLE_FILE_PATH, $"{folderName}/text2.txt", acl)); + urls.Add(provider.Upload(TEST_SAMPLE_FILE_PATH, $"{folderName}/text3.txt", acl)); + urls.Add(provider.Upload(TEST_SAMPLE_FILE_PATH, $"{folderName}/text4.txt", acl)); + urls.Add(provider.Upload(TEST_SAMPLE_FILE_PATH, $"{folderName}/test1.png", acl)); + + + var files = provider.GetFiles(folderName); + Assert.Equal(4, files.Count); + + provider.DeleteDirectory(folderName); + + files = provider.GetFiles(folderName); + Assert.Empty(files); + + } + + private void Copy(String copyFileName, GxFileType acl) + { + provider.Upload(TEST_SAMPLE_FILE_PATH, TEST_SAMPLE_FILE_NAME, acl); + String upload = provider.Get(TEST_SAMPLE_FILE_NAME, acl, 100); + EnsureUrl(upload, acl); + + DeleteSafe(copyFileName); + Wait(500); //Google CDN replication seems to be delayed. + + String urlCopy = TryGet(copyFileName, GxFileType.PublicRead); + Assert.False(UrlExists(urlCopy), "URL cannot exist: " + urlCopy); + + provider.Copy(TEST_SAMPLE_FILE_NAME, acl, copyFileName, acl); + upload = provider.Get(copyFileName, acl, 100); + EnsureUrl(upload, acl); + } + + + + private String TryGet(String objectName, GxFileType acl) + { + String getValue = ""; + try + { + if (((ExternalProviderBase)provider).GetName() == GeneXus.Storage.GXGoogleCloud.ExternalProviderGoogle.Name) + { + objectName = objectName.Replace("%2F", "/"); //Google Cloud Storage Bug. https://github.com/googleapis/google-cloud-dotnet/pull/3677 + } + getValue = provider.Get(objectName, acl, 5); + } + catch (Exception) + { + + } + return getValue; + } + + private String GetSafe(String objectName, GxFileType acl) + { + try + { + return provider.Get(objectName, acl, 100); + } + catch (Exception) + { + + } + return String.Empty; + } + + private bool DeleteSafe(String objectName) + { + DeleteSafeImpl(objectName, GxFileType.Private); + DeleteSafeImpl(objectName, GxFileType.PublicRead); + return true; + } + + private bool DeleteSafeImpl(String objectName, GxFileType acl) + { + try + { + provider.Delete(objectName, acl); + } + catch (Exception) + { + + } + return true; + } + + private static void Wait(int milliseconds) + { + System.Threading.Thread.Sleep(milliseconds); + } + + private void EnsureUrl(String signedOrUnsignedUrl, GxFileType acl) + { + Assert.True(UrlExists(signedOrUnsignedUrl), "URL not found: " + signedOrUnsignedUrl); + if (IsPrivateFile(acl)) + { + if (!(this is ExternalProviderMinioTest)) //Minio local installation not supported + { + Skip.If(this is ExternalProviderMinioTest); + String noSignedUrl = signedOrUnsignedUrl.Substring(0, signedOrUnsignedUrl.IndexOf('?') + 1); + Assert.False(UrlExists(noSignedUrl), "URL must be private: " + noSignedUrl); + } + } + else + { + Assert.False(signedOrUnsignedUrl.Contains("?"), "URL cannot be signed"); + } + } + private bool IsPrivateFile(GxFileType acl) + { + if (acl.HasFlag(GxFileType.Private)) + { + return true; + } + else if (acl.HasFlag(GxFileType.PublicRead)) + { + return false; + } + return defaultAclPrivate; + } + +#pragma warning disable CA1054 // Uri parameters should not be strings + protected static bool UrlExists(string url) + { + if (string.IsNullOrEmpty(url)) + { + return false; + } + bool exists = false; + HttpWebRequest request = (HttpWebRequest)WebRequest.Create(new Uri(url)); + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + + try + { + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + exists = response.StatusCode == HttpStatusCode.OK; + } + } + catch (WebException) + { + + } + return exists; + } +#pragma warning restore CA1054 // Uri parameters should not be strings + } +} +#pragma warning restore CA1031 // Do not catch general exception types \ No newline at end of file diff --git a/dotnet/test/DotNetUnitTest/resources/text.txt b/dotnet/test/DotNetUnitTest/resources/text.txt new file mode 100644 index 000000000..e04a19003 --- /dev/null +++ b/dotnet/test/DotNetUnitTest/resources/text.txt @@ -0,0 +1 @@ +My uploaded file test \ No newline at end of file