Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1935 as a developper i want to use amazon s3 to store and expose devices images #2032

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/AzureIoTHub.Portal.Domain/ConfigHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,6 @@ public abstract class ConfigHandler
public abstract string AWSAccessSecret { get; }
public abstract string AWSRegion { get; }
public abstract string AWSS3StorageConnectionString { get; }
public abstract string AWSBucketName { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
<ItemGroup>
<PackageReference Include="AWSSDK.IoT" Version="3.7.105.22" />
<PackageReference Include="AWSSDK.IotData" Version="3.7.102.5" />
<PackageReference Include="AWSSDK.S3" Version="3.7.103.50" />
<PackageReference Include="AWSSDK.SecretsManager" Version="3.7.102.21" />
<PackageReference Include="Azure.Data.Tables" Version="12.8.0" />
<PackageReference Include="Azure.Messaging.EventHubs" Version="5.8.1" />
Expand All @@ -126,6 +127,7 @@
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4">
<PrivateAssets>all</PrivateAssets>
Expand Down
2 changes: 2 additions & 0 deletions src/AzureIoTHub.Portal.Infrastructure/ConfigHandlerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ internal abstract class ConfigHandlerBase : ConfigHandler
internal const string AWSAccessSecretKey = "AWS:AccessSecret";
internal const string AWSRegionKey = "AWS:Region";
internal const string AWSS3StorageConnectionStringKey = "AWS:S3Storage:ConnectionString";
internal const string AWSBucketNameKey = "AWS:BucketName";

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ internal DevelopmentConfigHandler(IConfiguration config)
public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!;
public override string AWSRegion => this.config[AWSRegionKey]!;
public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!;
public override string AWSBucketName => this.config[AWSBucketNameKey]!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright (c) CGI France. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace AzureIoTHub.Portal.Infrastructure.Managers
{
using System;
using System.Threading.Tasks;
using Amazon;
using Amazon.S3;
using Amazon.S3.Model;
using Azure;
using AzureIoTHub.Portal.Application.Managers;
using AzureIoTHub.Portal.Domain;
using AzureIoTHub.Portal.Domain.Exceptions;
using AzureIoTHub.Portal.Domain.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

public class AwsDeviceModelImageManager : IDeviceModelImageManager
{
private readonly ILogger<AwsDeviceModelImageManager> logger;
private readonly ConfigHandler configHandler;
private readonly IOptions<DeviceModelImageOptions> imageOptions;
private readonly IAmazonS3 s3Client;

public AwsDeviceModelImageManager(
ILogger<AwsDeviceModelImageManager> logger,
ConfigHandler configHandler,
IOptions<DeviceModelImageOptions> options,
IAmazonS3 s3Client)
{
this.logger = logger;
this.configHandler = configHandler;
this.imageOptions = options;
this.s3Client = s3Client;
}


public async Task<string> ChangeDeviceModelImageAsync(string deviceModelId, Stream stream)
{
this.logger.LogInformation($"Uploading Image to AWS S3 storage");

//Portal must be able to upload images to Amazon S3
var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
InputStream = stream,
ContentType = "image/*",
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }
};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK
? ComputeImageUrl(deviceModelId)
: throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public Uri ComputeImageUri(string deviceModelId)
{
throw new NotImplementedException();
}

private string ComputeImageUrl(string deviceModelId)
{
return $"https://{this.configHandler.AWSBucketName}.s3.{RegionEndpoint.GetBySystemName(this.configHandler.AWSRegion)}.amazonaws.com/{deviceModelId}";
}
public async Task DeleteDeviceModelImageAsync(string deviceModelId)
{

this.logger.LogInformation($"Deleting image from AWS S3 storage");

var deleteImageObject = new DeleteObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId
};
try
{
_ = await this.s3Client.DeleteObjectAsync(deleteImageObject);

}
catch (RequestFailedException e)
{
throw new InternalServerErrorException("Unable to delete the image from S3 storage.", e);
}
}

public async Task<string> SetDefaultImageToModel(string deviceModelId)
{
this.logger.LogInformation($"Uploading Default Image to AWS S3 storage");

//Portal must be able to upload images to Amazon S3
var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}",
ContentType = "image/*", // image content type
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }

};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = deviceModelId,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

return putACLResponse.HttpStatusCode == System.Net.HttpStatusCode.OK
? ComputeImageUrl(deviceModelId)
: throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public async Task InitializeDefaultImageBlob()
hocinehacherouf marked this conversation as resolved.
Show resolved Hide resolved
{

this.logger.LogInformation($"Initializing default Image to AWS S3 storage");

var putObjectRequest = new PutObjectRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = this.imageOptions.Value.DefaultImageName,
FilePath = $"../Resources/{this.imageOptions.Value.DefaultImageName}",
ContentType = "image/*", // image content type
Headers = {CacheControl = $"max-age={this.configHandler.StorageAccountDeviceModelImageMaxAge}, must-revalidate" }

};

var putObjectResponse = await this.s3Client.PutObjectAsync(putObjectRequest);

if (putObjectResponse.HttpStatusCode == System.Net.HttpStatusCode.OK)
{
//Images on S3 are publicly accessible and read-only
var putAclRequest = new PutACLRequest
{
BucketName = this.configHandler.AWSBucketName,
Key = this.imageOptions.Value.DefaultImageName,
CannedACL = S3CannedACL.PublicRead // Set the object's ACL to public read
};
var putACLResponse = await this.s3Client.PutACLAsync(putAclRequest);

if (putACLResponse.HttpStatusCode != System.Net.HttpStatusCode.OK)
{
throw new InternalServerErrorException("Error by setting the image access to public and read-only");
}
}
else
{
throw new InternalServerErrorException("Error by uploading the image in S3 Storage");
}

}

public Task SyncImagesCacheControl()
{
/* We don't need an implementation of
this mehod for AWS because new images will processed by the method SetDefaultImageToModel
*/
throw new NotImplementedException();

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,6 @@ internal ProductionAWSConfigHandler(IConfiguration config)
public override string AWSAccessSecret => this.config[AWSAccessSecretKey]!;
public override string AWSRegion => this.config[AWSRegionKey]!;
public override string AWSS3StorageConnectionString => this.config[AWSS3StorageConnectionStringKey]!;
public override string AWSBucketName => this.config[AWSBucketNameKey]!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,7 @@ internal ProductionAzureConfigHandler(IConfiguration config)

public override string AWSRegion => throw new NotImplementedException();
public override string AWSS3StorageConnectionString => throw new NotImplementedException();

public override string AWSBucketName => throw new NotImplementedException();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ namespace AzureIoTHub.Portal.Infrastructure.Startup
using Amazon;
using Amazon.IoT;
using Amazon.IotData;
using Amazon.S3;
using Amazon.SecretsManager;
using AzureIoTHub.Portal.Application.Managers;
using AzureIoTHub.Portal.Domain;
using AzureIoTHub.Portal.Infrastructure.Managers;
using Microsoft.Extensions.DependencyInjection;

public static class AWSServiceCollectionExtension
Expand All @@ -34,6 +37,10 @@ private static IServiceCollection ConfigureAWSClient(this IServiceCollection ser

_ = services.AddSingleton(() => new AmazonSecretsManagerClient(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion)));

_ = services.AddSingleton(() => new AmazonS3Client(configuration.AWSAccess, configuration.AWSAccessSecret, RegionEndpoint.GetBySystemName(configuration.AWSRegion)));

_ = services.AddTransient<IDeviceModelImageManager, AwsDeviceModelImageManager>();

return services;
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/AzureIoTHub.Portal.Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ public async void Configure(IApplicationBuilder app, IWebHostEnvironment env)
await ConfigureAzureAsync(app);
break;
case CloudProviders.AWS:
await ConfigureAwsAsync(app);
break;
default:
break;
Expand All @@ -463,6 +464,12 @@ private static async Task ConfigureAzureAsync(IApplicationBuilder app)

await EnsureDatabaseCreatedAndUpToDate(app)!;
}
private static async Task ConfigureAwsAsync(IApplicationBuilder app)
{
var deviceModelImageManager = app.ApplicationServices.GetService<IDeviceModelImageManager>();

await deviceModelImageManager?.InitializeDefaultImageBlob()!;
}

private static void UseApiExceptionMiddleware(IApplicationBuilder app)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private DevelopmentConfigHandler CreateDevelopmentConfigHandler()
[TestCase(ConfigHandlerBase.AWSRegionKey, nameof(ConfigHandlerBase.AWSRegion))]
[TestCase(ConfigHandlerBase.AWSS3StorageConnectionStringKey, nameof(ConfigHandlerBase.AWSS3StorageConnectionString))]
[TestCase(ConfigHandlerBase.CloudProviderKey, nameof(ConfigHandlerBase.CloudProvider))]
[TestCase(ConfigHandlerBase.AWSBucketNameKey, nameof(ConfigHandlerBase.AWSBucketName))]
public void SettingsShouldGetValueFromAppSettings(string configKey, string configPropertyName)
{
// Arrange
Expand Down
Loading