diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..ef6bc9c --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,33 @@ +name: ci + +on: + push: + branches-ignore: + - "main" + # paths: + # - "MediaFileMetadataCheckerAPI/**" + +env: + TEST_TAG: user/app:test + LATEST_TAG: user/app:latest + METADATA_API_CONFIG_CONNECTION_STRING: ${{ secrets.APP_CONFIG_CONNECTION_STRING }} + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Build and export to local images + uses: docker/build-push-action@v5 + with: + context: "{{defaultContext}}:MediaFileMetadataCheckerAPI" + load: true + tags: ${{ env.TEST_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Test running image + run: | + docker run --rm ${{ env.TEST_TAG }} -e METADATA_API_CONFIG_CONNECTION_STRING="${{ env.METADATA_API_CONFIG_CONNECTION_STRING }}" -e METADATA_API_CONFIG_CACHE_EXPIRATION="1" --stop-timeout 30 \ No newline at end of file diff --git a/MediaFileMetadataCheckerAPI/Controllers/FileUploadController.cs b/MediaFileMetadataCheckerAPI/Controllers/FileUploadController.cs index 23842a6..b94a3a2 100644 --- a/MediaFileMetadataCheckerAPI/Controllers/FileUploadController.cs +++ b/MediaFileMetadataCheckerAPI/Controllers/FileUploadController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using System.IO; using System.Threading.Tasks; @@ -21,11 +22,13 @@ namespace MediaFileMetadataCheckerAPI.Controllers [Route("[controller]")] public class FileUploadController : ControllerBase { + private readonly Settings _settings; private readonly ILogger _logger; - public FileUploadController(ILogger logger) + public FileUploadController(ILogger logger, IOptionsSnapshot settings) { _logger = logger; + _settings = settings.Value; } /// @@ -39,15 +42,14 @@ public FileUploadController(ILogger logger) [HttpPost] [Route(nameof(UploadLargeFileForMetadata))] [ProducesResponseType(typeof(FileStreamResult), 200, "application/json")] - // [Produces("application/json", "application/json")] [DisableFormValueModelBinding] public async Task UploadLargeFileForMetadata() { var request = HttpContext.Request; - // validation of Content-Type - // 1. first, it must be a form-data request - // 2. a boundary should be found in the Content-Type + // Validation of Content-Type + // 1. First, it must be a form-data request + // 2. A boundary should be found in the Content-Type if (!request.HasFormContentType || !MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader) || string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value)) @@ -59,8 +61,6 @@ public async Task UploadLargeFileForMetadata() var reader = new MultipartReader(boundary, request.Body); var section = await reader.ReadNextSectionAsync(); - // This sample try to get the first file from request and save it - // Make changes according to your needs in actual use while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, @@ -70,11 +70,6 @@ public async Task UploadLargeFileForMetadata() if (hasContentDispositionHeader && contentDisposition.DispositionType.Equals("form-data") && !string.IsNullOrEmpty(contentDisposition.FileName.Value)) { - // Don't trust any file name, file extension, and file data from the request unless you trust them completely - // Otherwise, it is very likely to cause problems such as virus uploading, disk filling, etc - // In short, it is necessary to restrict and verify the upload - // Here, we just use the temporary folder and a random file name - // Get the temporary folder, and combine a random file name with it var fileName = Path.GetRandomFileName(); var saveToPath = Path.Combine(Path.GetTempPath(), fileName); @@ -88,7 +83,8 @@ public async Task UploadLargeFileForMetadata() if (mediaInfo is not null) { - HashSet returnProperties = Settings.ReturnProperties.Split(";").ToHashSet(); + // Get return properties from App Config and only return configured properties + HashSet returnProperties = _settings.ReturnProperties.Split(";").ToHashSet(); var File = new FileUploadItem(); File.Duration = returnProperties.Contains("Duration") ? mediaInfo.Duration : null; @@ -105,12 +101,9 @@ public async Task UploadLargeFileForMetadata() } } -#pragma warning restore CS8602 // Dereference of a possibly null reference. - section = await reader.ReadNextSectionAsync(); } - // If the code runs to this location, it means that no files have been saved return BadRequest("No files data in the request."); } } diff --git a/MediaFileMetadataCheckerAPI/Filters/FileFilter.cs b/MediaFileMetadataCheckerAPI/Filters/FileFilter.cs index bbe7396..25ba8f1 100644 --- a/MediaFileMetadataCheckerAPI/Filters/FileFilter.cs +++ b/MediaFileMetadataCheckerAPI/Filters/FileFilter.cs @@ -15,12 +15,10 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) if (formParameters.Any()) { - // already taken care by swashbuckle. no need to add explicitly. return; } if (operation.RequestBody != null) { - // NOT required for form type return; } if (context.ApiDescription.HttpMethod == HttpMethod.Post.Method) @@ -64,29 +62,3 @@ internal static bool IsFromForm(this ApiParameterDescription apiParameter) return (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType)); } } - -public class FileDownloadFilter : IOperationFilter -{ - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - if (context.ApiDescription.SupportedResponseTypes.Any(x => - x.Type == typeof(FileStreamResult) || - x.Type == typeof(FileResult))) - - if (operation.RequestBody == null) - { - return; - } - - // See https://github.com/Azure/autorest.csharp/issues/197 - foreach (var content in operation.RequestBody.Content) - foreach (var contentProperty in content.Value.Schema.Properties) - { - var schema = contentProperty.Value; - if (schema.Format == "binary") - { - schema.Type = "file"; - } - } - } -} \ No newline at end of file diff --git a/MediaFileMetadataCheckerAPI/MediaFileMetadataCheckerAPI.csproj b/MediaFileMetadataCheckerAPI/MediaFileMetadataCheckerAPI.csproj index 9f219d5..965cfc3 100644 --- a/MediaFileMetadataCheckerAPI/MediaFileMetadataCheckerAPI.csproj +++ b/MediaFileMetadataCheckerAPI/MediaFileMetadataCheckerAPI.csproj @@ -20,6 +20,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + diff --git a/MediaFileMetadataCheckerAPI/Program.cs b/MediaFileMetadataCheckerAPI/Program.cs index 1b1946f..e54ccec 100644 --- a/MediaFileMetadataCheckerAPI/Program.cs +++ b/MediaFileMetadataCheckerAPI/Program.cs @@ -3,6 +3,7 @@ using MediaFileMetadataCheckerAPI.Filters; using Microsoft.Extensions.DependencyInjection; using Microsoft.Azure.AppConfiguration.AspNetCore; +using Microsoft.Extensions.Configuration; var builder = WebApplication.CreateBuilder(args); @@ -13,7 +14,7 @@ opt.UseInMemoryDatabase("FileUploadList")); // Get connection string from Environment variables string? connectionString = Environment.GetEnvironmentVariable("METADATA_API_CONFIG_CONNECTION_STRING"); -double? configCacheExpiration = Convert.ToDouble(Environment.GetEnvironmentVariable("METADATA_API_CONFIG_CACHE_EXPIRATION")); +double configCacheExpiration = Convert.ToDouble(Environment.GetEnvironmentVariable("METADATA_API_CONFIG_CACHE_EXPIRATION")); // Load configuration from Azure App Configuration builder.Configuration.AddAzureAppConfiguration(options => @@ -24,8 +25,10 @@ // Configure to reload configuration if the registered sentinel key is modified .ConfigureRefresh(refreshOptions => refreshOptions.Register("MetadataApp:Settings:Sentinel", refreshAll: true) - .SetCacheExpiration(TimeSpan.FromHours(configCacheExpiration ?? 1))); + .SetCacheExpiration(TimeSpan.FromHours(configCacheExpiration))); }); +// Add Azure App Configuration middleware to the container of services. +builder.Services.AddAzureAppConfiguration(); // Bind configuration "MetadataApp:Settings" section to the Settings object builder.Services.Configure(builder.Configuration.GetSection("MetadataApp:Settings")); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle diff --git a/MediaFileMetadataCheckerAPI/Settings.cs b/MediaFileMetadataCheckerAPI/Settings.cs index a63b802..74ea97c 100644 --- a/MediaFileMetadataCheckerAPI/Settings.cs +++ b/MediaFileMetadataCheckerAPI/Settings.cs @@ -2,6 +2,6 @@ namespace MetadataAppConfig { public class Settings { - public static string? ReturnProperties { get; set; } + public string? ReturnProperties { get; set; } } } \ No newline at end of file diff --git a/scripts/docker-run.zsh b/scripts/docker-run.zsh index 40f768a..b4a9387 100644 --- a/scripts/docker-run.zsh +++ b/scripts/docker-run.zsh @@ -1,3 +1,11 @@ #!/usr/bin/env zsh # doc: https://github.com/dotnet/dotnet-docker/blob/main/samples/run-aspnetcore-https-development.md -docker run --rm -it -p 8000:80 -p 8001:443 -e ASPNETCORE_URLS="https://+;http://+" -e ASPNETCORE_HTTPS_PORTS=8001 -e ASPNETCORE_Kestrel__Certificates__Default__Password=$pass -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx -e ASPNETCORE_ENVIRONMENT=Development -v ${HOME}/.aspnet/https:/https/ mediametadata \ No newline at end of file +docker run --rm -it -p 8000:80 -p 8001:443 / + -e ASPNETCORE_URLS="https://+;http://+" / + -e ASPNETCORE_HTTPS_PORTS=8001 / + -e ASPNETCORE_Kestrel__Certificates__Default__Password=$pass / + -e ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx / + -e ASPNETCORE_ENVIRONMENT=Development / + -e METADATA_API_CONFIG_CACHE_EXPIRATION=1 / + -e METADATA_API_CONFIG_CONNECTION_STRING=$connectionString / + -v ${HOME}/.aspnet/https:/https/ mediametadata \ No newline at end of file