diff --git a/.gitignore b/.gitignore index b69f001..f9793f8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ __azurite*.json private*.data azurestorageexplorer.sln +credentials.json +.vscode/ + diff --git a/README.md b/README.md index 12a13ad..a27f8f7 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ These variables are `AZURE_STORAGE_CONNECTIONSTRING`, `AZURE_STORAGE_ACCOUNT`, ` If you want to test it against [Azurite](https://github.com/Azure/Azurite) (either locally, via Docker, Docker Compose, or Kubernetes) you'll have to add the `AZURITE` variable set to `true`. +#### AWS & GCP + +You can now also manager your AWS and GCP buckets. Just make sure you set the environment variable `CLOUD_PROVIDER` to either `AWS` or `GCP` and you also need to set up a few other environments depending the cloud provider. +If you want to connect to your AWS S3 account, you need to provider three environment variables `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, and `AWS_REGION`. +If in the other hand you want to connect to GCP, you need to donload the credentials file for your service account and set the `GCP_CREDENTIALS_FILE` environment variable to the full path to that file. + +![](./res/AWSExplorer.png) + +**This feature is in beta so feel free to provide feedback** + ## Exploring **Blobs**: Create public or private Containers and Blobs (only BlockBlobs for now). Download or delete your blobs. diff --git a/res/AWSExplorer.png b/res/AWSExplorer.png new file mode 100644 index 0000000..d22faec Binary files /dev/null and b/res/AWSExplorer.png differ diff --git a/res/AzureExplorerLogo.pdn b/res/AzureExplorerLogo.pdn index 2b23b5f..8746418 100644 Binary files a/res/AzureExplorerLogo.pdn and b/res/AzureExplorerLogo.pdn differ diff --git a/src/StorageLibrary/AWS/AWSBucket.cs b/src/StorageLibrary/AWS/AWSBucket.cs new file mode 100644 index 0000000..eaf8df6 --- /dev/null +++ b/src/StorageLibrary/AWS/AWSBucket.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; + +using StorageLibrary.Common; +using StorageLibrary.Interfaces; + +namespace StorageLibrary.AWS +{ + internal class AWSBucket : StorageObject, IContainer + { + protected AmazonS3Client _s3Client; + public AWSBucket(StorageFactoryConfig config): base(config) + { + var credentials = new BasicAWSCredentials(config.AwsKey, config.AwsSecret); + _s3Client = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(config.AwsRegion)); + } + + public async Task CreateAsync(string bucket, bool publicAccess) + { + var putBucketRequest = new PutBucketRequest + { + BucketName = bucket, + }; + + await _s3Client.PutBucketAsync(putBucketRequest); + + if (publicAccess) + { + await SetBucketPolicyAsync(bucket); + } + } + + public async Task SetBucketPolicyAsync(string bucket) + { + var bucketPolicy = new + { + Version = "2012-10-17", + Statement = new[] + { + new + { + Sid = "AddPerm", + Effect = "Allow", + Principal = "*", + Action = "s3:GetObject", + Resource = $"arn:aws:s3:::{bucket}/*" + } + } + }; + + var policyJson = JsonSerializer.Serialize(bucketPolicy); + + var putBucketPolicyRequest = new PutBucketPolicyRequest + { + BucketName = bucket, + Policy = policyJson + }; + + await _s3Client.PutBucketPolicyAsync(putBucketPolicyRequest); + } + + public async Task CreateBlobAsync(string bucket, string key, Stream fileContent) + { + var putRequest = new PutObjectRequest + { + BucketName = bucket, + Key = key, + InputStream = fileContent + }; + + await _s3Client.PutObjectAsync(putRequest); + } + + public async Task DeleteAsync(string bucket) + { + var deleteBucketRequest = new DeleteBucketRequest + { + BucketName = bucket + }; + + await _s3Client.DeleteBucketAsync(deleteBucketRequest); + } + + public async Task DeleteBlobAsync(string bucket, string key) + { + var deleteObjectRequest = new DeleteObjectRequest + { + BucketName = bucket, + Key = key + }; + + await _s3Client.DeleteObjectAsync(deleteObjectRequest); + } + + public async Task GetBlobAsync(string bucket, string key) + { + string tmpPath = Util.File.GetTempFileName(); + + var getRequest = new GetObjectRequest + { + BucketName = bucket, + Key = key + }; + + using (GetObjectResponse response = await _s3Client.GetObjectAsync(getRequest)) + using (Stream responseStream = response.ResponseStream) + using (FileStream fileStream = File.Create(tmpPath)) + { + await responseStream.CopyToAsync(fileStream); + } + + return tmpPath; + } + + public async Task> ListBlobsAsync(string bucket, string path) + { + var request = new ListObjectsV2Request + { + BucketName = bucket, + Prefix = path, + Delimiter = Path.AltDirectorySeparatorChar.ToString() + }; + + + var blobs = new List(); + var response = await _s3Client.ListObjectsV2Async(request); + + var uriTemplate = $"https://{bucket}.s3.{RegionEndpoint.USEast1.SystemName}.amazonaws.com/"; + + foreach (S3Object entry in response.S3Objects) + { + if (entry.Key == path) + continue; + + blobs.Add(new BlobItemWrapper($"{uriTemplate}{entry.Key}", entry.Size, CloudProvider.AWS)); + } + + foreach (string commonPrefix in response.CommonPrefixes) + blobs.Add(new BlobItemWrapper($"{uriTemplate}{commonPrefix}", 0, CloudProvider.AWS)); + + return blobs; + } + + public async Task> ListContainersAsync() + { + var buckets = new List(); + + ListBucketsResponse response = await _s3Client.ListBucketsAsync(); + foreach (S3Bucket bucket in response.Buckets) + { + buckets.Add(new CloudBlobContainerWrapper() { Name = bucket.BucketName }); + } + + return buckets; + } + } +} \ No newline at end of file diff --git a/src/StorageLibrary/AzureContainer.cs b/src/StorageLibrary/Azure/AzureContainer.cs similarity index 96% rename from src/StorageLibrary/AzureContainer.cs rename to src/StorageLibrary/Azure/AzureContainer.cs index 54db206..118f5cf 100644 --- a/src/StorageLibrary/AzureContainer.cs +++ b/src/StorageLibrary/Azure/AzureContainer.cs @@ -9,7 +9,7 @@ using StorageLibrary.Common; using StorageLibrary.Interfaces; -namespace StorageLibrary +namespace StorageLibrary.Azure { internal class AzureContainer : StorageObject, IContainer { @@ -45,11 +45,11 @@ public async Task> ListBlobsAsync(string containerName, st { BlobClient blobClient = container.GetBlobClient(blobItem.Blob.Name); - wrapper = new BlobItemWrapper(blobClient.Uri.AbsoluteUri, blobItem.Blob.Properties.ContentLength.HasValue ? blobItem.Blob.Properties.ContentLength.Value : 0, IsAzurite); + wrapper = new BlobItemWrapper(blobClient.Uri.AbsoluteUri, blobItem.Blob.Properties.ContentLength.HasValue ? blobItem.Blob.Properties.ContentLength.Value : 0, CloudProvider.Azure, IsAzurite); } else if (blobItem.IsPrefix) { - wrapper = new BlobItemWrapper($"{container.Uri}/{blobItem.Prefix}", 0, IsAzurite); + wrapper = new BlobItemWrapper($"{container.Uri}/{blobItem.Prefix}", 0, CloudProvider.Azure, IsAzurite); } if (wrapper != null && !results.Contains(wrapper)) diff --git a/src/StorageLibrary/AzureFile.cs b/src/StorageLibrary/Azure/AzureFile.cs similarity index 99% rename from src/StorageLibrary/AzureFile.cs rename to src/StorageLibrary/Azure/AzureFile.cs index b2bd224..2a263da 100644 --- a/src/StorageLibrary/AzureFile.cs +++ b/src/StorageLibrary/Azure/AzureFile.cs @@ -10,7 +10,7 @@ using StorageLibrary.Common; using StorageLibrary.Interfaces; -namespace StorageLibrary +namespace StorageLibrary.Azure { internal class AzureFile : StorageObject, IFile { diff --git a/src/StorageLibrary/AzureQueue.cs b/src/StorageLibrary/Azure/AzureQueue.cs similarity index 98% rename from src/StorageLibrary/AzureQueue.cs rename to src/StorageLibrary/Azure/AzureQueue.cs index f243baa..996dded 100644 --- a/src/StorageLibrary/AzureQueue.cs +++ b/src/StorageLibrary/Azure/AzureQueue.cs @@ -7,7 +7,7 @@ using StorageLibrary.Common; using StorageLibrary.Interfaces; -namespace StorageLibrary +namespace StorageLibrary.Azure { internal class AzureQueue : StorageObject, IQueue { diff --git a/src/StorageLibrary/AzureTable.cs b/src/StorageLibrary/Azure/AzureTable.cs similarity index 98% rename from src/StorageLibrary/AzureTable.cs rename to src/StorageLibrary/Azure/AzureTable.cs index 78eeda6..122f1ba 100644 --- a/src/StorageLibrary/AzureTable.cs +++ b/src/StorageLibrary/Azure/AzureTable.cs @@ -6,7 +6,7 @@ using StorageLibrary.Common; using StorageLibrary.Interfaces; -namespace StorageLibrary +namespace StorageLibrary.Azure { internal class AzureTable : StorageObject, ITable { diff --git a/src/StorageLibrary/Common/BlobItemWrapper.cs b/src/StorageLibrary/Common/BlobItemWrapper.cs index b7c972c..fc630b3 100644 --- a/src/StorageLibrary/Common/BlobItemWrapper.cs +++ b/src/StorageLibrary/Common/BlobItemWrapper.cs @@ -20,22 +20,43 @@ public string Url public long Size { get; private set; } + public CloudProvider Provider { get; private set; } + public decimal SizeInKBs { get => (decimal)Size / 1024; } public decimal SizeInMBs { get => (decimal)Size / 1024 / 1024; } - public BlobItemWrapper(string url) : this(url, 0) { } + public BlobItemWrapper(string url) : this(url, 0, CloudProvider.Azure) { } - public BlobItemWrapper(string url, long size, bool fromAzurite = false) + public BlobItemWrapper(string url, long size, CloudProvider provider, bool fromAzurite = false) { Url = url; Size = size; + Provider = provider; m_isAzurite = fromAzurite; IsFile = !m_internalUri.Segments[m_internalUri.Segments.Length - 1].EndsWith(System.IO.Path.AltDirectorySeparatorChar); - Container = m_isAzurite ? m_internalUri.Segments[2] : m_internalUri.Segments[1]; Name = HttpUtility.UrlDecode(m_internalUri.Segments[m_internalUri.Segments.Length - 1]); - int containerPos = m_internalUri.LocalPath.IndexOf(Container) + Container.Length; - Path = m_internalUri.LocalPath.Substring(containerPos, (m_internalUri.LocalPath.Length) - (containerPos) - Name.Length); + + switch (provider) + { + case CloudProvider.Azure: + Container = m_isAzurite ? m_internalUri.Segments[2] : m_internalUri.Segments[1]; + int containerPos = m_internalUri.LocalPath.IndexOf(Container) + Container.Length; + Path = m_internalUri.LocalPath.Substring(containerPos, (m_internalUri.LocalPath.Length) - (containerPos) - Name.Length); + break; + case CloudProvider.AWS: + Container = m_internalUri.Host.Split('.')[0]; + Path = m_internalUri.LocalPath.Substring(1, m_internalUri.LocalPath.Length - 1 - Name.Length); + break; + case CloudProvider.GCP: + Container = m_internalUri.Segments[1]; + int bucketPos = m_internalUri.LocalPath.IndexOf(Container) + Container.Length; + Path = m_internalUri.LocalPath.Substring(bucketPos, (m_internalUri.LocalPath.Length) - (bucketPos) - Name.Length); + break; + default: + throw new ApplicationException($"Invalid provider: {provider}"); + } + } public int CompareTo(BlobItemWrapper other) diff --git a/src/StorageLibrary/GCP/GCPBucket.cs b/src/StorageLibrary/GCP/GCPBucket.cs new file mode 100644 index 0000000..9dbf30e --- /dev/null +++ b/src/StorageLibrary/GCP/GCPBucket.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2; +using Google.Apis.Services; +using Google.Apis.Storage.v1; +using Google.Apis.Storage.v1.Data; +using GoogleStorageObject = Google.Apis.Storage.v1.Data.Object; + +using StorageLibrary.Common; +using StorageLibrary.Interfaces; + +namespace StorageLibrary.Google +{ + internal class GCPBucket : StorageObject, IContainer + { + const string AppName = "Sebagomez Cloud Storage Explorer"; + protected StorageService _storageService; + protected string _projectId; + + public GCPBucket(StorageFactoryConfig config) : base(config) + { + string serviceAccountPath = config.GcpCredentialsFile; + GoogleCredential credential; + using (var stream = new FileStream(serviceAccountPath, FileMode.Open, FileAccess.Read)) + { + credential = GoogleCredential.FromStream(stream).CreateScoped(StorageService.Scope.DevstorageFullControl); + } + // Create the Storage service. + _storageService = new StorageService(new BaseClientService.Initializer() + { + HttpClientInitializer = credential, + ApplicationName = AppName, + }); + + var underlyingCredential = credential.UnderlyingCredential as ServiceAccountCredential; + if (underlyingCredential != null) + _projectId = underlyingCredential.ProjectId; + } + + public async Task CreateAsync(string bucket, bool publicAccess) + { + var newBucket = new Bucket + { + Name = bucket + }; + + var insertRequest = _storageService.Buckets.Insert(newBucket, _projectId); + await insertRequest.ExecuteAsync(); + + if (publicAccess) + { + await SetBucketPolicyAsync(bucket); + } + } + + private async Task SetBucketPolicyAsync(string bucket) + { + var bucketIamPolicy = new Policy + { + Bindings = new List + { + new Policy.BindingsData + { + Role = "roles/storage.objectViewer", + Members = new List { "allUsers" } + } + } + }; + + var setIamPolicyRequest = new BucketsResource.SetIamPolicyRequest(_storageService, bucketIamPolicy, bucket); + await setIamPolicyRequest.ExecuteAsync(); + } + + public async Task CreateBlobAsync(string bucket, string objectName, Stream fileContent) + { + var uploadRequest = new GoogleStorageObject() + { + Bucket = bucket, + Name = objectName + }; + + var mediaUpload = new ObjectsResource.InsertMediaUpload(_storageService, uploadRequest, bucket, fileContent, "application/octet-stream"); + + await mediaUpload.UploadAsync(); + } + + public async Task DeleteAsync(string bucket) + { + var deleteRequest = _storageService.Buckets.Delete(bucket); + await deleteRequest.ExecuteAsync(); + } + + public async Task DeleteBlobAsync(string bucket, string objectName) + { + var deleteRequest = _storageService.Objects.Delete(bucket, objectName); + await deleteRequest.ExecuteAsync(); + } + + public async Task GetBlobAsync(string bucket, string objectName) + { + string tmpPath = Util.File.GetTempFileName(); + using (var outputFile = new FileStream(tmpPath, FileMode.Create, FileAccess.Write)) + { + var getRequest = _storageService.Objects.Get(bucket, objectName); + await getRequest.DownloadAsync(outputFile); + } + + return tmpPath; + } + + public async Task> ListBlobsAsync(string bucket, string path) + { + var listRequest = _storageService.Objects.List(bucket); + listRequest.Prefix = path; + listRequest.Delimiter = "/"; + + var listObjects = await listRequest.ExecuteAsync(); + + List blobs = new List(); + string uriTemplate = $"https://storage.cloud.google.com/sebagomeztestbucket/"; + if (listObjects.Items != null) + { + foreach (var obj in listObjects.Items) + { + if (obj.Name == path) + continue; + + blobs.Add(new BlobItemWrapper($"{uriTemplate}{obj.Name}", (long)obj.Size, CloudProvider.GCP)); + } + } + + if (listObjects.Prefixes != null) + foreach (string commonPrefix in listObjects.Prefixes) + blobs.Add(new BlobItemWrapper($"{uriTemplate}{commonPrefix}", 0, CloudProvider.GCP)); + + return blobs; + } + + public async Task> ListContainersAsync() + { + var buckets = await _storageService.Buckets.List(_projectId).ExecuteAsync(); + + List containers = new List(); + foreach (var bucket in buckets.Items) + { + containers.Add(new CloudBlobContainerWrapper() { Name = bucket.Name }); + } + + return containers; + } + } +} \ No newline at end of file diff --git a/src/StorageLibrary/Mocks/MockContainer.cs b/src/StorageLibrary/Mocks/MockContainer.cs index 7605429..46b5c0a 100644 --- a/src/StorageLibrary/Mocks/MockContainer.cs +++ b/src/StorageLibrary/Mocks/MockContainer.cs @@ -31,7 +31,7 @@ public async Task> ListBlobsAsync(string containerName, st List results = new List(); foreach(string url in MockUtils.GetItems(containerName, path)) - results.Add(new BlobItemWrapper(url, MockUtils.NewRandomSize)); + results.Add(new BlobItemWrapper(url, MockUtils.NewRandomSize, CloudProvider.Azure)); return results; }); diff --git a/src/StorageLibrary/StorageFactory.cs b/src/StorageLibrary/StorageFactory.cs index 385b8e4..35e3402 100644 --- a/src/StorageLibrary/StorageFactory.cs +++ b/src/StorageLibrary/StorageFactory.cs @@ -1,7 +1,12 @@ +using System; + using StorageLibrary.Common; using StorageLibrary.Interfaces; using StorageLibrary.Mocks; +using StorageLibrary.Azure; +using StorageLibrary.AWS; +using StorageLibrary.Google; namespace StorageLibrary { @@ -23,27 +28,62 @@ public StorageFactory() public StorageFactory(StorageFactoryConfig config) { m_currentConfig = config; - Queues = config.Mock ? new MockQueue() : new AzureQueue(config); - Containers = config.Mock ? new MockContainer() : new AzureContainer(config); - Tables = config.Mock ? new MockTable() : new AzureTable(config); - Files = config.Mock ? new MockFile() : new AzureFile(config); + if (config.Mock) + { + Queues = new MockQueue(); + Containers = new MockContainer(); + Tables = new MockTable(); + Files = new MockFile(); + } + else + { + switch (config.Provider) + { + case CloudProvider.Azure: + Queues = new AzureQueue(config); + Containers = new AzureContainer(config); + Tables = new AzureTable(config); + Files = new AzureFile(config); + break; + case CloudProvider.AWS: + Containers = new AWSBucket(config); + break; + case CloudProvider.GCP: + Containers = new GCPBucket(config); + break; + default: + throw new ApplicationException($"Invalid provider: {config.Provider}"); + } + } Instance = this; } public static BlobItemWrapper GetBlobItemWrapper(string url, long size = 0) { - return new BlobItemWrapper(url, size, Instance.m_currentConfig.IsAzurite); + return new BlobItemWrapper(url, size, Instance.m_currentConfig.Provider, Instance.m_currentConfig.IsAzurite); } } public class StorageFactoryConfig { - public string Account { get; set; } - public string Key { get; set; } - public string Endpoint { get; set; } = "core.windows.net"; - public string ConnectionString { get; set; } + public string AzureAccount { get; set; } + public string AzureKey { get; set; } + public string AzureEndpoint { get; set; } = "core.windows.net"; + public string AzureConnectionString { get; set; } public bool IsAzurite { get; set; } public bool Mock { get; set; } + public string AwsKey { get; set; } + public string AwsSecret { get; set; } + public string AwsRegion { get; set; } + public string GcpCredentialsFile { get; set; } + public CloudProvider Provider { get; set; } = CloudProvider.Azure; + } + + public enum CloudProvider + { + Azure, + AWS, + GCP } } \ No newline at end of file diff --git a/src/StorageLibrary/StorageLibrary.csproj b/src/StorageLibrary/StorageLibrary.csproj index e87c0d3..e158794 100644 --- a/src/StorageLibrary/StorageLibrary.csproj +++ b/src/StorageLibrary/StorageLibrary.csproj @@ -15,6 +15,10 @@ + + + + diff --git a/src/StorageLibrary/StorageObject.cs b/src/StorageLibrary/StorageObject.cs index a4ab245..2db3d3d 100644 --- a/src/StorageLibrary/StorageObject.cs +++ b/src/StorageLibrary/StorageObject.cs @@ -15,25 +15,25 @@ internal class StorageObject const string CONNSTRING_TEMPLATE = "DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}"; public StorageObject(StorageFactoryConfig config) - : this(config.Account, config.Key, config.Endpoint, config.ConnectionString, config.IsAzurite){ } - - public StorageObject(string account, string key, string endpoint, string connectionString, bool azurite) { - IsAzurite = azurite; - if (!string.IsNullOrEmpty(connectionString)) + if (config.Provider != CloudProvider.Azure) + return; + + IsAzurite = config.IsAzurite; + if (!string.IsNullOrEmpty(config.AzureConnectionString)) { - ConnectionString = connectionString; + ConnectionString = config.AzureConnectionString; } - else if (key.Contains("SharedAccessSignature=")) + else if (config.AzureKey.Contains("SharedAccessSignature=")) { - ConnectionString = key; + ConnectionString = config.AzureKey; } else { - Account = account; - Key = key; - if (!string.IsNullOrEmpty(endpoint)) - Endpoint = endpoint; + Account = config.AzureAccount; + Key = config.AzureKey; + if (!string.IsNullOrEmpty(config.AzureEndpoint)) + Endpoint = config.AzureEndpoint; ConnectionString = string.Format(CONNSTRING_TEMPLATE, Account, Key, Endpoint); } diff --git a/src/web/Directory.Build.props b/src/web/Directory.Build.props index 4afcd53..a98dd4d 100644 --- a/src/web/Directory.Build.props +++ b/src/web/Directory.Build.props @@ -1,5 +1,5 @@ - 2.17.2 + 3.0.0 \ No newline at end of file diff --git a/src/web/Pages/BaseComponent.razor b/src/web/Pages/BaseComponent.razor index 0821b9a..63662e2 100644 --- a/src/web/Pages/BaseComponent.razor +++ b/src/web/Pages/BaseComponent.razor @@ -1,4 +1,4 @@ - + diff --git a/src/web/Pages/Home.razor b/src/web/Pages/Home.razor index 0c685cb..17b5171 100644 --- a/src/web/Pages/Home.razor +++ b/src/web/Pages/Home.razor @@ -3,16 +3,32 @@ +

Welcome to the revamped version of Azure Storage web explorer

+@if (Utils.Util.Provider != "Azure") +{ +

Congratulations! 🥳 you just found a new hidden feature that allows you to manage your @Utils.Util.Provider objects

+}

This is a side project that I work on whenever I find the time and will. Want to contribute in any way? head over to the repo at https://github.com/sebagomez/azurestorageexplorer

With this site you can manage

    -
  • Blobs: Create public or private Containers and Blobs (only BlockBlobs for now). Upload, download and delete blobs.
  • -
  • Queues: Create Queues and messages.
  • -
  • Tables: Create table and Entities. To create an Entity you'll have to add one property per line in the form of <PropertyName>=<PropertyValue>. You can also set the data type for a specific property adding the odata.type value. To retrieve data, you must provide a query (or leave blank) in the form of <PropertyName> <operator> '<PropertyValue>'(mind the quotes '), being <operator> one of the supported comparison operators
  • -
  • Files Shares: Navigate thrugh your file shares and directories. Upload, download and delete files.
  • + @switch (Utils.Util.Provider) + { + case "Azure": +
  • Blobs: Create public or private Containers and Blobs (only BlockBlobs for now). Upload, download and delete blobs.
  • +
  • Queues: Create Queues and messages.
  • +
  • Tables: Create table and Entities. To create an Entity you'll have to add one property per line in the form of <PropertyName>=<PropertyValue>. You can also set the data type for a specific property adding the odata.type value. To retrieve data, you must provide a query (or leave blank) in the form of <PropertyName> <operator> '<PropertyValue>'(mind the quotes '), being <operator> one of the supported comparison operators
  • +
  • Files Shares: Navigate thrugh your file shares and directories. Upload, download and delete files.
  • + break; + case "AWS": +
  • Buckets: Create public or private S3 Buckets and manage (upload, download, and delete) your S3 objects.
  • + break; + case "GCP": +
  • Buckets: Create public or private Buckets and manage (upload, download, and delete) your GCP objects.
  • + break; + }

More info in the project's README file. diff --git a/src/web/Pages/Login.razor.cs b/src/web/Pages/Login.razor.cs index ba349d6..a6a30d2 100644 --- a/src/web/Pages/Login.razor.cs +++ b/src/web/Pages/Login.razor.cs @@ -75,6 +75,42 @@ protected override async Task OnInitializedAsync() } } } + else //check if it's AWS or GCP + { + string? cloudProvider = Environment.GetEnvironmentVariable("CLOUD_PROVIDER"); + if (!string.IsNullOrEmpty(cloudProvider)) + { + if (cloudProvider.Equals("AWS", StringComparison.OrdinalIgnoreCase)) + { + string? awsAccessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY"); + string? awsSecretKey = Environment.GetEnvironmentVariable("AWS_SECRET_KEY"); + string? awsRegion = Environment.GetEnvironmentVariable("AWS_REGION"); + if (!string.IsNullOrEmpty(awsAccessKey) && !string.IsNullOrEmpty(awsSecretKey) && !string.IsNullOrEmpty(awsRegion)) + { + Credentials awsCredentials = new Credentials + { + Provider = "AWS", + AwsKey = awsAccessKey, + AwsSecret = awsSecretKey, + AwsRegion = awsRegion + }; + if (await awsCredentials.IsAuthenticated(SessionStorage!)) + NavManager!.NavigateTo("home"); + } + } + else if (cloudProvider.Equals("GCP", StringComparison.OrdinalIgnoreCase)) + { + string? gcpCredentials = Environment.GetEnvironmentVariable("GCP_CREDENTIALS_FILE"); + if (!string.IsNullOrEmpty(gcpCredentials)) + { + Credentials gcpCred = new Credentials { Provider = "GCP", GcpCredFile = gcpCredentials }; + if (await gcpCred.IsAuthenticated(SessionStorage!)) + NavManager!.NavigateTo("home"); + } + } + } + + } } } diff --git a/src/web/Shared/NavMenu.razor b/src/web/Shared/NavMenu.razor index 8511b82..77bdbfd 100644 --- a/src/web/Shared/NavMenu.razor +++ b/src/web/Shared/NavMenu.razor @@ -4,29 +4,53 @@

@code { + + [Parameter] + public string? Provider { get; set; } + + [Parameter] + public bool IsAzurite { get; set; } = false; + [Parameter] public string? Account { get; set; } @@ -49,7 +73,7 @@ private Dictionary QueueAtts = new Dictionary(); private Dictionary TableAtts = new Dictionary(); private Dictionary FileAtts = new Dictionary(); - + protected override void OnInitialized() { if (BlobAtts.ContainsKey("class")) @@ -64,16 +88,16 @@ switch (Selected) { case "Containers": - BlobAtts.Add("class", "selectedMenu"); + BlobAtts.Add("class", "selectedMenu " + Provider!.ToLower()); break; case "Tables": - TableAtts.Add("class", "selectedMenu"); + TableAtts.Add("class", "selectedMenu " + Provider!.ToLower()); break; case "Queues": - QueueAtts.Add("class", "selectedMenu"); + QueueAtts.Add("class", "selectedMenu " + Provider!.ToLower()); break; case "FileShares": - FileAtts.Add("class", "selectedMenu"); + FileAtts.Add("class", "selectedMenu " + Provider!.ToLower()); break; } diff --git a/src/web/Utils/Credentials.cs b/src/web/Utils/Credentials.cs index 44058f8..9022342 100644 --- a/src/web/Utils/Credentials.cs +++ b/src/web/Utils/Credentials.cs @@ -7,10 +7,18 @@ namespace web.Utils { public class Credentials { + const string PROVIDER = "wasm_provider"; const string ACCOUNT = "wasm_account"; const string KEY = "wasm_key"; const string ENDPOINT = "wasm_endpoint"; const string CONNECTION_STRING = "wasm_connectionString"; + const string AWS_KEY = "wasm_aws_key"; + const string AWS_SECRET = "wasm_aws_secret"; + const string AWS_REGION = "wasm_aws_region"; + const string GCP_CREDENTIALS_FILE = "wasm_gcp_credfile"; + + [DataMember] + public string? Provider { get; set; } = "Azure"; [DataMember] public string? Account { get; set; } @@ -20,31 +28,42 @@ public class Credentials public string? Endpoint { get; set; } [DataMember] public string? ConnectionString { get; set; } + [DataMember] + public string? AwsKey { get; set; } + [DataMember] + public string? AwsSecret { get; set; } + [DataMember] + public string? AwsRegion { get; set; } + [DataMember] + public string? GcpCredFile { get; set; } public Credentials() { } - public Credentials(string azure_account, string azure_key, string azure_endpoint, string azure_connectionString) - { - Account = azure_account; - Key = azure_key; - Endpoint = azure_endpoint; - ConnectionString = azure_connectionString; - } - public async Task SaveAsync(ProtectedBrowserStorage sessionStorage) { if (string.IsNullOrEmpty(Account) && !string.IsNullOrEmpty(ConnectionString)) Account = GetAccountName(); + if (string.IsNullOrEmpty(Account) && Provider != "Azure") + Account = Provider; + + await sessionStorage.SetAsync(PROVIDER, Provider!); await sessionStorage.SetAsync(ACCOUNT, Account!); await sessionStorage.SetAsync(KEY, Key!); await sessionStorage.SetAsync(ENDPOINT, Endpoint!); await sessionStorage.SetAsync(CONNECTION_STRING, ConnectionString!); + await sessionStorage.SetAsync(AWS_KEY, AwsKey!); + await sessionStorage.SetAsync(AWS_SECRET, AwsSecret!); + await sessionStorage.SetAsync(AWS_REGION, AwsRegion!); + await sessionStorage.SetAsync(GCP_CREDENTIALS_FILE, GcpCredFile!); } private string GetAccountName() { + if (Provider != "Azure") + return Provider!; + var connStringArray = ConnectionString!.Split(';', StringSplitOptions.RemoveEmptyEntries); var dictionary = new Dictionary(); foreach (var item in connStringArray) @@ -76,19 +95,29 @@ public static async Task LoadCredentialsAsync(ProtectedBrowserStora { return new Credentials { + Provider = (await sessionStorage.GetAsync(PROVIDER)).Value, Account = (await sessionStorage.GetAsync(ACCOUNT)).Value, Key = (await sessionStorage.GetAsync(KEY)).Value, Endpoint = (await sessionStorage.GetAsync(ENDPOINT)).Value, - ConnectionString = (await sessionStorage.GetAsync(CONNECTION_STRING)).Value + ConnectionString = (await sessionStorage.GetAsync(CONNECTION_STRING)).Value, + AwsKey = (await sessionStorage.GetAsync(AWS_KEY)).Value, + AwsSecret = (await sessionStorage.GetAsync(AWS_SECRET)).Value, + AwsRegion = (await sessionStorage.GetAsync(AWS_REGION)).Value, + GcpCredFile = (await sessionStorage.GetAsync(GCP_CREDENTIALS_FILE)).Value }; } public static async Task ClearAsync(ProtectedBrowserStorage sessionStorage) { + await sessionStorage.DeleteAsync(PROVIDER); await sessionStorage.DeleteAsync(ACCOUNT); await sessionStorage.DeleteAsync(KEY); await sessionStorage.DeleteAsync(ENDPOINT); await sessionStorage.DeleteAsync(CONNECTION_STRING); + await sessionStorage.DeleteAsync(AWS_KEY); + await sessionStorage.DeleteAsync(AWS_SECRET); + await sessionStorage.DeleteAsync(AWS_REGION); + await sessionStorage.DeleteAsync(GCP_CREDENTIALS_FILE); } public async Task IsAuthenticated(ProtectedBrowserStorage sessionStorage) diff --git a/src/web/Utils/Util.cs b/src/web/Utils/Util.cs index 99cf108..36d4fb3 100644 --- a/src/web/Utils/Util.cs +++ b/src/web/Utils/Util.cs @@ -13,15 +13,33 @@ public class Util public const string MOCK = "MOCK"; public const string AZURITE = "AZURITE"; + public static string Provider { get; set; } = "Azure"; + public static bool Azurite { get; set; } = false; + public static StorageFactory GetStorageFactory(Credentials cred) { + Provider = cred.Provider!; + string? mock = Environment.GetEnvironmentVariable(MOCK); bool mockEnabled = mock is not null && mock.ToLower() == bool.TrueString.ToLower(); string? azurite = Environment.GetEnvironmentVariable(AZURITE); - bool azuriteEnabled = azurite is not null && azurite.ToLower() == bool.TrueString.ToLower(); + Azurite = azurite is not null && azurite.ToLower() == bool.TrueString.ToLower(); - return new StorageFactory(new StorageFactoryConfig { Account = cred.Account, Key = cred.Key, Endpoint = cred.Endpoint, ConnectionString = cred.ConnectionString, IsAzurite = azuriteEnabled, Mock = mockEnabled }); + return new StorageFactory(new StorageFactoryConfig + { + Provider = Enum.Parse(cred.Provider!), + AzureAccount = cred.Account, + AzureKey = cred.Key, + AzureEndpoint = cred.Endpoint, + AzureConnectionString = cred.ConnectionString, + AwsKey = cred.AwsKey, + AwsSecret = cred.AwsSecret, + AwsRegion = cred.AwsRegion, + GcpCredentialsFile = cred.GcpCredFile, + IsAzurite = Azurite, + Mock = mockEnabled + }); } } } diff --git a/src/web/wwwroot/css/azurewebexplorer.css b/src/web/wwwroot/css/azurewebexplorer.css index 0ea507e..890501e 100644 --- a/src/web/wwwroot/css/azurewebexplorer.css +++ b/src/web/wwwroot/css/azurewebexplorer.css @@ -12,6 +12,16 @@ font-family: Segoe UI,SegoeUI,Segoe WP,Tahoma,Arial,sans-serif; } +.aws { + color: #FF9900 !important; + font-family: Segoe UI,SegoeUI,Segoe WP,Tahoma,Arial,sans-serif; +} + +.gcp { + color: #FF3E30 !important; + font-family: Segoe UI,SegoeUI,Segoe WP,Tahoma,Arial,sans-serif; +} + .errorMessage { position: fixed !important; width: 60%; @@ -84,7 +94,6 @@ .selectedMenu { font-weight: bold; - color: #00abec; } .contextMenu{ diff --git a/src/web/wwwroot/res/AWSExplorerLogo.png b/src/web/wwwroot/res/AWSExplorerLogo.png new file mode 100644 index 0000000..f03ff7f Binary files /dev/null and b/src/web/wwwroot/res/AWSExplorerLogo.png differ diff --git a/src/web/wwwroot/res/GCPExplorerLogo.png b/src/web/wwwroot/res/GCPExplorerLogo.png new file mode 100644 index 0000000..b1cfee1 Binary files /dev/null and b/src/web/wwwroot/res/GCPExplorerLogo.png differ diff --git a/utils/azurite.sh b/utils/azurite.sh index 13c81d6..a51c1f5 100755 --- a/utils/azurite.sh +++ b/utils/azurite.sh @@ -3,7 +3,7 @@ CONTAINER_NAME="azurite" IMAGE_NAME="mcr.microsoft.com/azure-storage/azurite" -Check if the container is already running +# Check if the container is already running if [ "$(docker ps -q -f name=${CONTAINER_NAME})" ]; then echo "Container ${CONTAINER_NAME} is already running." else