diff --git a/SampleApplications/2023/LambdaTriggersSample/.editorconfig b/SampleApplications/2023/LambdaTriggersSample/.editorconfig new file mode 100644 index 0000000..e23bfb8 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/.editorconfig @@ -0,0 +1,128 @@ +# Suppress: EC112 +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = false +indent_style = space +indent_size = 4 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_style = tab +indent_size = 4 + +# Code files +[*.sln] +indent_size = 4 + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# XML files +[*.xml] +indent_size = 2 + +[*.cs] + +# Organize usings +dotnet_sort_system_directives_first = true + +# IDE0160: Use file scoped namespace +csharp_style_namespace_declarations = file_scoped:error + +# CS4014: Because this call is not awaited, execution of the current method continues before the call is completed +dotnet_diagnostic.CS4014.severity = error + +# Remove explicit default access modifiers +dotnet_style_require_accessibility_modifiers = omit_if_default:error + +# CA1063: Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = error + +# CA1001: Type owns disposable field(s) but is not disposable +dotnet_diagnostic.CA1001.severity = error + +# Pattern matching +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method=true:suggestion + +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestions + +# Naming rules + +dotnet_diagnostic.IDE1006.severity = error + +## Public Fields are kept Pascal Case +dotnet_naming_symbols.public_symbols.applicable_kinds = field +dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal + +dotnet_naming_style.first_word_upper_case_style.capitalization = first_word_upper + +dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols +dotnet_naming_rule.public_members_must_be_capitalized.style = first_word_upper_case_style +dotnet_naming_rule.public_members_must_be_capitalized.severity = suggestion + +## Instance fields are camelCase +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = error +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +## Static fields are camelCase +dotnet_naming_rule.static_fields_should_be_camel_case.severity = error +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = _ + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error + +# CA1822: Member does not access instance data and can be marked as static +dotnet_diagnostic.CA1822.severity = suggestion + +# CA1050: Declare types in namespaces +dotnet_diagnostic.CA1050.severity = error + +# CA2016: Forward the 'cancellationToken' parameter methods that take one +dotnet_diagnostic.CA2016.severity = error + +# CA2208: Method passes parameter as the paramName argument to a ArgumentNullException constructor. Replace this argument with one of the method's parameter names. Note that the provided parameter name should have the exact casing as declared on the method. +dotnet_diagnostic.CA2208.severity = error + +# CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string +dotnet_diagnostic.CA1834.severity = error + +# IDE0220: Add explicit cast +dotnet_diagnostic.IDE0220.severity = error \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/.gitattributes b/SampleApplications/2023/LambdaTriggersSample/.gitattributes new file mode 100644 index 0000000..3a4406b --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/.gitattributes @@ -0,0 +1,70 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain + +# Force bash scripts to always use lf line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.sh text eol=lf + +# Force the docs to always use lf line endings +docs/**/*.xml text eol=lf \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/.gitignore b/SampleApplications/2023/LambdaTriggersSample/.gitignore new file mode 100644 index 0000000..4740b3f --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/.gitignore @@ -0,0 +1,265 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# ignore Xamarin.Android Resource.Designer.cs files +**/*.Droid/**/[Rr]esource.[Dd]esigner.cs +**/*.Android/**/[Rr]esource.[Dd]esigner.cs +**/Android/**/[Rr]esource.[Dd]esigner.cs +**/Droid/**/[Rr]esource.[Dd]esigner.cs + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +**/.DS_Store + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Visual Studio Code +.vscode diff --git a/SampleApplications/2023/LambdaTriggersSample/Directory.Build.props b/SampleApplications/2023/LambdaTriggersSample/Directory.Build.props new file mode 100644 index 0000000..fb8cf89 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/Directory.Build.props @@ -0,0 +1,37 @@ + + + + + false + + latest + enable + enable + NETSDK1023 + True + false + true + + + + + + + + + + + + + + + nullable,CS1570,CS1571,CS1572,CS1573,CS1574,CS1580,CS1581,CS1584,CS1589,CS1590,CS1592,CS1598,CS1658,CS1734 + + + + + + + true + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LICENSE b/SampleApplications/2023/LambdaTriggersSample/LICENSE new file mode 100644 index 0000000..8d780b5 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Brandon Minnick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/AssemblyInfo.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/AssemblyInfo.cs new file mode 100644 index 0000000..981ae29 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: Amazon.Lambda.Core.LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/LambdaTriggers.Backend.Common.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/LambdaTriggers.Backend.Common.csproj new file mode 100644 index 0000000..22c1872 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/LambdaTriggers.Backend.Common.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/S3Service.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/S3Service.cs new file mode 100644 index 0000000..326eb70 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Backend.Common/S3Service.cs @@ -0,0 +1,80 @@ +using System.Net; +using System.Text.Json; +using Amazon.Lambda.Core; +using Amazon.S3; +using Amazon.S3.Model; +using LambdaTriggers.Common; + +namespace LambdaTriggers.Backend.Common; + +public static class S3Service +{ + public const string BucketName = "lambdatriggersbucket"; + + public static async Task UploadContentToS3(IAmazonS3 s3Client, string bucket, string key, T content, ILambdaLogger logger) + { + var request = content switch + { + Stream stream => new PutObjectRequest + { + InputStream = stream, + BucketName = bucket, + Key = key + }, + _ => new PutObjectRequest + { + ContentType = "application/json", + ContentBody = JsonSerializer.Serialize(content), + BucketName = bucket, + Key = key + } + }; + + logger.LogInformation($"Uploading object to S3..."); + + var putObjectResponse = await s3Client.PutObjectAsync(request).ConfigureAwait(false); + var fileUrl = s3Client.GeneratePreSignedURL(bucket, key, DateTime.UtcNow.AddYears(1), null); + + if (putObjectResponse.HttpStatusCode is not HttpStatusCode.OK) + throw new HttpRequestException($"{nameof(IAmazonS3.PutObjectAsync)} Failed: {putObjectResponse.HttpStatusCode}"); + + logger.LogInformation($"Upload suceeded"); + logger.LogInformation($"{nameof(putObjectResponse.ChecksumSHA256)}: {putObjectResponse.ChecksumSHA256}"); + + return new Uri(fileUrl); + } + + public static string GenerateThumbnailFilename(in string fileName) => Path.GetFileNameWithoutExtension(fileName) + Constants.ThumbnailSuffix; + + public static async Task GetFileUri(IAmazonS3 s3Client, string bucket, string key, ILambdaLogger lambdaLogger, DateTime? expirationDate = default) + { + expirationDate ??= DateTime.UtcNow.AddYears(1); + + lambdaLogger.LogInformation("Creating Presigned URL..."); + + + var doesFileExist = await DoesFileExist(bucket, key, s3Client, lambdaLogger).ConfigureAwait(false); + if (!doesFileExist) + return null; + + var url = s3Client.GeneratePreSignedURL(bucket, key, expirationDate.Value, null); + + lambdaLogger.LogInformation($"Presigned URL Expiring on {expirationDate:MMMM dd, yyyy} Generated: {url}"); + + return new Uri(url); + } + + static async Task DoesFileExist(string bucket, string key, IAmazonS3 s3Client, ILambdaLogger lambdaLogger) + { + try + { + var response = await s3Client.GetObjectAsync(bucket, key).ConfigureAwait(false); + return response is not null; + } + catch (AmazonS3Exception e) + { + lambdaLogger.LogError(e.Message); + return false; + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/Constants.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/Constants.cs new file mode 100644 index 0000000..209a1b9 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/Constants.cs @@ -0,0 +1,9 @@ +namespace LambdaTriggers.Common; + +public static class Constants +{ + public const string ImageFileNameQueryParameter = "filename"; + public const string UploadPhotoApiUrl = "https://322uo0ruod.execute-api.us-west-1.amazonaws.com/default"; + public const string GetThumbnailApiUrl = "https://1jk3r5j44h.execute-api.us-west-1.amazonaws.com/default"; + public const string ThumbnailSuffix = "_thumbnail.png"; +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/LambdaTriggers.Common.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/LambdaTriggers.Common.csproj new file mode 100644 index 0000000..cfadb03 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Common/LambdaTriggers.Common.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/GenerateThumbnail.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/GenerateThumbnail.cs new file mode 100644 index 0000000..bffc63e --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/GenerateThumbnail.cs @@ -0,0 +1,78 @@ +using System.Net; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.S3Events; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.S3; +using LambdaTriggers.Backend.Common; +using LambdaTriggers.Common; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace LambdaTriggers.GenerateThumbnail; + +public sealed class GenerateThumbnail : IDisposable +{ + static readonly IAmazonS3 _s3Client = new AmazonS3Client(); + + public static async Task FunctionHandler(S3Event evnt, ILambdaContext context) + { + var s3Event = evnt.Records?[0].S3; + if (s3Event is null || s3Event.Object.Key.EndsWith(Constants.ThumbnailSuffix)) + return; + + try + { + using var response = await _s3Client.GetObjectAsync(s3Event.Bucket.Name, s3Event.Object.Key); + if (response.HttpStatusCode is not HttpStatusCode.OK) + throw new InvalidOperationException("Failed to get S3 file"); + + using var imageMemoryStream = new MemoryStream(); + + await response.ResponseStream.CopyToAsync(imageMemoryStream).ConfigureAwait(false); + if (imageMemoryStream is null || imageMemoryStream.ToArray().Length < 1) + throw new InvalidOperationException($"The document '{s3Event.Object.Key}' is invalid"); + + using var thumbnail = await GetPNGThumbnail(imageMemoryStream).ConfigureAwait(false); + + var thumbnailName = S3Service.GenerateThumbnailFilename(s3Event.Object.Key); + + await S3Service.UploadContentToS3(_s3Client, s3Event.Bucket.Name, thumbnailName, thumbnail, context.Logger).ConfigureAwait(false); + } + catch (Exception e) + { + context.Logger.LogInformation($"Error creating thumbail for {s3Event.Object.Key} from bucket {s3Event.Bucket.Name}."); + context.Logger.LogInformation(e.ToString()); + throw; + } + } + + public void Dispose() + { + _s3Client.Dispose(); + } + + static async Task GetPNGThumbnail(Stream imageStream) + { + var resizeOptions = new ResizeOptions + { + Mode = ResizeMode.Max, + Size = new Size(200, 200) + }; + + imageStream.Position = 0; + using var image = await Image.LoadAsync(imageStream).ConfigureAwait(false); + + image.Mutate(imageContext => imageContext.Resize(resizeOptions)); + + var outputMemoryStream = new MemoryStream(); + await image.SaveAsPngAsync(outputMemoryStream).ConfigureAwait(false); + + return outputMemoryStream; + } + + static Task Main(string[] args) => + LambdaBootstrapBuilder.Create((S3Event s3Event, ILambdaContext context) => FunctionHandler(s3Event, context), new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/LambdaTriggers.GenerateThumbnail.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/LambdaTriggers.GenerateThumbnail.csproj new file mode 100644 index 0000000..73077d4 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/LambdaTriggers.GenerateThumbnail.csproj @@ -0,0 +1,36 @@ + + + + Exe + net7.0 + enable + Lambda + bootstrap + + true + + + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/Properties/launchSettings.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/Properties/launchSettings.json new file mode 100644 index 0000000..5870340 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net7.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-7.0.exe" + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/aws-lambda-tools-defaults.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..cc1205a --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GenerateThumbnail/aws-lambda-tools-defaults.json @@ -0,0 +1,28 @@ + +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile" : "VisualStudioToolkit", + "region" : "us-west-1", + "configuration" : "Release", + "function-runtime" : "provided.al2", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "bootstrap", + "msbuild-parameters" : "--self-contained true", + "framework" : "net7.0", + "function-name" : "LambdaTriggers_GenerateThumbnail", + "package-type" : "Zip", + "function-role" : "arn:aws:iam::723361041013:role/lambda_exec_LambdaTriggers-0", + "function-architecture" : "x86_64", + "function-subnets" : "", + "function-security-groups" : "", + "tracing-mode" : "Active", + "environment-variables" : "", + "image-tag" : "", + "function-description" : "" +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/GetThumbnail.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/GetThumbnail.cs new file mode 100644 index 0000000..d8fe5f1 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/GetThumbnail.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.S3; +using LambdaTriggers.Backend.Common; +using LambdaTriggers.Common; + +namespace LambdaTriggers.GetThumbnail; + +public sealed class GetThumbnail : IDisposable +{ + static readonly IAmazonS3 _s3Client = new AmazonS3Client(); + + public static async Task FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + if (request.QueryStringParameters is null + || !request.QueryStringParameters.TryGetValue(Constants.ImageFileNameQueryParameter, out var filename) + || filename is null) + { + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = (int)HttpStatusCode.BadRequest, + Body = request.QueryStringParameters?.Any() is true + ? $"Invalid Request. Query Parameter, \"{request.QueryStringParameters.First().Value}\", Not Supported" + : $"Invalid Request. Missing Query Parameter \"{Constants.ImageFileNameQueryParameter}\"" + }; + } + + var thumbnailFileName = S3Service.GenerateThumbnailFilename(filename); + var thumbnailUrl = await S3Service.GetFileUri(_s3Client, S3Service.BucketName, thumbnailFileName, context.Logger).ConfigureAwait(false); + + return thumbnailUrl switch + { + null => new() + { + StatusCode = (int)HttpStatusCode.NotFound, + Body = $"Thumbnail {thumbnailFileName} could not be located in {S3Service.BucketName}" + }, + _ => new() + { + StatusCode = (int)HttpStatusCode.OK, + Body = JsonSerializer.Serialize(thumbnailUrl), + } + }; + } + + public void Dispose() + { + _s3Client.Dispose(); + } + + static Task Main(string[] args) => + LambdaBootstrapBuilder.Create((APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) => FunctionHandler(request, context), new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/LambdaTriggers.GetThumbnail.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/LambdaTriggers.GetThumbnail.csproj new file mode 100644 index 0000000..f62e809 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/LambdaTriggers.GetThumbnail.csproj @@ -0,0 +1,33 @@ + + + Exe + net7.0 + enable + enable + Lambda + bootstrap + + true + + + true + + + + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/Properties/launchSettings.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/Properties/launchSettings.json new file mode 100644 index 0000000..5870340 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net7.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-7.0.exe" + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/aws-lambda-tools-defaults.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..3bc72db --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.GetThumbnail/aws-lambda-tools-defaults.json @@ -0,0 +1,27 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "VisualStudioToolkit", + "region": "us-west-1", + "configuration": "Release", + "function-runtime": "provided.al2", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "bootstrap", + "msbuild-parameters": "--self-contained true", + "framework": "net7.0", + "function-name": "LambdaTriggers_GetThumbnail", + "function-description": "", + "package-type": "Zip", + "function-role": "arn:aws:iam::723361041013:role/lambda_exec_LambdaTriggers-0", + "function-architecture": "x86_64", + "function-subnets": "", + "function-security-groups": "", + "tracing-mode": "Active", + "environment-variables": "", + "image-tag": "" +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/App.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/App.cs new file mode 100644 index 0000000..e3f1208 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/App.cs @@ -0,0 +1,9 @@ +namespace LambdaTriggers.Mobile; + +class App : Application +{ + public App(AppShell shell) + { + MainPage = shell; + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/AppShell.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/AppShell.cs new file mode 100644 index 0000000..a9608f5 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/AppShell.cs @@ -0,0 +1,9 @@ +namespace LambdaTriggers.Mobile; + +class AppShell : Shell +{ + public AppShell(PhotoPage photoPage) + { + Items.Add(photoPage); + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/GlobalUsings.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/GlobalUsings.cs new file mode 100644 index 0000000..4645a0b --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using CommunityToolkit.Mvvm.ComponentModel; +global using CommunityToolkit.Mvvm.Input; \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/LambdaTriggers.Mobile.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/LambdaTriggers.Mobile.csproj new file mode 100644 index 0000000..2ff4e3e --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/LambdaTriggers.Mobile.csproj @@ -0,0 +1,65 @@ + + + + net7.0-android;net7.0-ios;net7.0-maccatalyst + $(TargetFrameworks);net7.0-windows10.0.19041.0 + Exe + LambdaTriggers.Mobile + true + true + enable + + + LambdaTriggers.Mobile + + + com.companyname.lambdatriggers.mobile + cc5739ff-93ef-42c2-95e9-21d8898fcdb4 + + + 1.0 + 1 + + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + false + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/MauiProgram.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/MauiProgram.cs new file mode 100644 index 0000000..31255b4 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/MauiProgram.cs @@ -0,0 +1,41 @@ +using System.Net; +using CommunityToolkit.Maui; +using CommunityToolkit.Maui.Markup; +using LambdaTriggers.Common; +using Polly; +using Refit; + +namespace LambdaTriggers.Mobile; + +public class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder() + .UseMauiApp() + .UseMauiCommunityToolkit() + .UseMauiCommunityToolkitMarkup(); + + // App Shell + builder.Services.AddTransient(); + + // Services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(MediaPicker.Default); + builder.Services.AddSingleton(); + builder.Services.AddRefitClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri(Constants.UploadPhotoApiUrl)) + .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(3, sleepDurationProvider)); + + builder.Services.AddRefitClient() + .ConfigureHttpClient(client => client.BaseAddress = new Uri(Constants.GetThumbnailApiUrl)) + .AddTransientHttpErrorPolicy(builder => builder.OrResult(response => response.StatusCode is HttpStatusCode.NotFound).WaitAndRetryAsync(3, sleepDurationProvider)); + + // Pages + View Models + builder.Services.AddTransient(); + + return builder.Build(); + + static TimeSpan sleepDurationProvider(int attemptNumber) => TimeSpan.FromSeconds(Math.Pow(2, attemptNumber)); + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/Base/BaseContentPage.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/Base/BaseContentPage.cs new file mode 100644 index 0000000..436127f --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/Base/BaseContentPage.cs @@ -0,0 +1,14 @@ +namespace LambdaTriggers.Mobile; + +abstract class BaseContentPage : ContentPage where T : BaseViewModel +{ + protected BaseContentPage(T viewModel, string pageTitle) + { + base.BindingContext = viewModel; + + Padding = 12; + Title = pageTitle; + } + + protected new T BindingContext => (T)base.BindingContext; +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/PhotoPage.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/PhotoPage.cs new file mode 100644 index 0000000..c3d3957 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Pages/PhotoPage.cs @@ -0,0 +1,100 @@ +using CommunityToolkit.Maui.Markup; +using static CommunityToolkit.Maui.Markup.GridRowsColumns; + +namespace LambdaTriggers.Mobile; + +class PhotoPage : BaseContentPage +{ + public PhotoPage(PhotoViewModel photoViewModel) : base(photoViewModel, "Photo Page") + { + photoViewModel.Error += HandleError; + + Content = new Grid + { + RowDefinitions = Rows.Define( + (Row.Photo, Stars(5)), + (Row.UploadButton, Stars(2)), + (Row.ActivityIndicator, Star)), + + ColumnDefinitions = Columns.Define( + (Column.CapturedPhoto, Star), + (Column.Thumbnail, Star)), + + ColumnSpacing = 12, + + Children = + { + new ImageBorder + { + Content = new Grid + { + Children = + { + new Label() + .Row(0) + .Center() + .Text("Captured Photo") + .TextCenter(), + + new PhotoImage() + .Row(0) + .Bind(Image.SourceProperty, nameof(PhotoViewModel.CapturedPhoto), convert: (Stream? image) => image is not null ? ImageSource.FromStream(() => image) : null) + } + } + + }.Row(Row.Photo).Column(Column.CapturedPhoto), + + new ImageBorder + { + Content = new Grid + { + new Label() + .Row(0) + .Center() + .Text("Thumbnail") + .TextCenter(), + + new PhotoImage() + .Row(0) + .Bind(Image.SourceProperty, nameof(PhotoViewModel.ThumbnailPhotoUri), convert: (Uri? imageUri) => imageUri is not null ? ImageSource.FromUri(imageUri) : null) + } + }.Row(Row.Photo).Column(Column.Thumbnail), + + new Button() + .Row(Row.UploadButton).ColumnSpan(All()) + .Center() + .Text("Upload Photo") + .Bind(Button.CommandProperty, nameof(PhotoViewModel.UploadPhotoCommand)), + + new ActivityIndicator { IsRunning = true } + .Row(Row.ActivityIndicator).ColumnSpan(All()) + .Center() + .Bind(IsVisibleProperty, nameof(PhotoViewModel.IsCapturingAndUploadingPhoto)), + } + }; + } + + enum Row { Photo, UploadButton, ActivityIndicator } + enum Column { CapturedPhoto, Thumbnail } + + async void HandleError(object? sender, string message) => await Dispatcher.DispatchAsync(() => DisplayAlert("Error", message, "OK")); + + class ImageBorder : Border + { + public ImageBorder() + { + Stroke = new SolidColorBrush(Colors.Grey); + StrokeThickness = 2; + Padding = 12; + } + } + + class PhotoImage : Image + { + public PhotoImage() + { + Aspect = Aspect.Center; + this.Bind(IsVisibleProperty, nameof(Image.Source), source: RelativeBindingSource.Self, convert: (ImageSource? source) => source is not null); + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/AndroidManifest.xml b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..07a3108 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainActivity.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..f534a9f --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainActivity.cs @@ -0,0 +1,10 @@ +using Android.App; +using Android.Content.PM; +using Android.OS; + +namespace LambdaTriggers.Mobile; + +[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ScreenOrientation = ScreenOrientation.SensorLandscape, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] +public class MainActivity : MauiAppCompatActivity +{ +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainApplication.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..75551f0 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/MainApplication.cs @@ -0,0 +1,15 @@ +using Android.App; +using Android.Runtime; + +namespace LambdaTriggers.Mobile; + +[Application] +public class MainApplication : MauiApplication +{ + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/Resources/values/colors.xml b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/AppDelegate.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..a2d18d7 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,8 @@ +using Foundation; + +namespace LambdaTriggers.Mobile; +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Info.plist b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..8a79844 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,35 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + NSCameraUsageDescription + This app needs access to the camera to take photos. + NSMicrophoneUsageDescription + This app needs access to microphone for taking videos. + NSPhotoLibraryAddUsageDescription + This app needs access to the photo gallery for picking photos and videos. + NSPhotoLibraryUsageDescription + This app needs access to photos gallery for picking photos and videos. + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Program.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..f2b49e8 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,14 @@ +using ObjCRuntime; +using UIKit; + +namespace LambdaTriggers.Mobile; +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml new file mode 100644 index 0000000..901fba3 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..caab455 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/App.xaml.cs @@ -0,0 +1,23 @@ +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace LambdaTriggers.Mobile.WinUI; +/// +/// Provides application-specific behavior to supplement the default Application class. +/// +public partial class App : MauiWinUIApplication +{ + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/Package.appxmanifest b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..d473f05 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/app.manifest b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/app.manifest new file mode 100644 index 0000000..e64ccce --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/AppDelegate.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..a2d18d7 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,8 @@ +using Foundation; + +namespace LambdaTriggers.Mobile; +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Info.plist b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Info.plist new file mode 100644 index 0000000..669fc09 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Info.plist @@ -0,0 +1,37 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + NSCameraUsageDescription + This app needs access to the camera to take photos. + NSMicrophoneUsageDescription + This app needs access to microphone for taking videos. + NSPhotoLibraryAddUsageDescription + This app needs access to the photo gallery for picking photos and videos. + NSPhotoLibraryUsageDescription + This app needs access to photos gallery for picking photos and videos. + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Program.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Program.cs new file mode 100644 index 0000000..f2b49e8 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Platforms/iOS/Program.cs @@ -0,0 +1,14 @@ +using ObjCRuntime; +using UIKit; + +namespace LambdaTriggers.Mobile; +public class Program +{ + // This is the main entry point of the application. + static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Properties/launchSettings.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appicon.svg b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appiconfg.svg b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Regular.ttf b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..e2644a5 Binary files /dev/null and b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Regular.ttf differ diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Semibold.ttf b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..f75b525 Binary files /dev/null and b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Fonts/OpenSans-Semibold.ttf differ diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Images/dotnet_bot.svg b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..abfaff2 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Raw/AboutAssets.txt b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..15d6244 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Splash/splash.svg b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Colors.xaml b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Colors.xaml new file mode 100644 index 0000000..245758b --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Colors.xaml @@ -0,0 +1,44 @@ + + + + + #512BD4 + #DFD8F7 + #2B0B98 + White + Black + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + #F7B548 + #FFD590 + #FFE5B9 + #28C2D1 + #7BDDEF + #C3F2F4 + #3E8EED + #72ACF1 + #A7CBF6 + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Styles.xaml b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Styles.xaml new file mode 100644 index 0000000..dc4a034 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Resources/Styles/Styles.xaml @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IGetThumbnailApi.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IGetThumbnailApi.cs new file mode 100644 index 0000000..48eb920 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IGetThumbnailApi.cs @@ -0,0 +1,11 @@ +using LambdaTriggers.Common; +using Refit; + +namespace LambdaTriggers.Mobile; + +[Headers("Accept-Encoding: gzip", "Accept: application/json")] +public interface IGetThumbnailApi +{ + [Get($"/LambdaTriggers_GetThumbnail?{Constants.ImageFileNameQueryParameter}={{photoTitle}}")] + Task GetThumbnailUri(string photoTitle, CancellationToken token); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IUploadPhotosApi.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IUploadPhotosApi.cs new file mode 100644 index 0000000..2948803 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/IUploadPhotosApi.cs @@ -0,0 +1,11 @@ +using LambdaTriggers.Common; +using Refit; + +namespace LambdaTriggers.Mobile; + +[Headers("Accept-Encoding: gzip", "Accept: application/json")] +public interface IUploadPhotosAPI +{ + [Post($"/LambdaTriggers_UploadImage?{Constants.ImageFileNameQueryParameter}={{photoTitle}}"), Multipart] + Task UploadPhoto(string photoTitle, [AliasAs("photo")] StreamPart photoStream, CancellationToken token); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/PhotosApiService.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/PhotosApiService.cs new file mode 100644 index 0000000..e7ef659 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/Services/PhotosApiService.cs @@ -0,0 +1,20 @@ +using Refit; + +namespace LambdaTriggers.Mobile; + +class PhotosApiService +{ + readonly IUploadPhotosAPI _uploadPhotosApiClient; + readonly IGetThumbnailApi _getThumbnailApiClient; + + public PhotosApiService(IUploadPhotosAPI uploadPhotosApiClient, IGetThumbnailApi getThumbnailApiClient) => + (_uploadPhotosApiClient, _getThumbnailApiClient) = (uploadPhotosApiClient, getThumbnailApiClient); + + public async Task UploadPhoto(string photoTitle, FileResult photoMediaFile, CancellationToken token) + { + var fileStream = await photoMediaFile.OpenReadAsync().ConfigureAwait(false); + return await _uploadPhotosApiClient.UploadPhoto(photoTitle, new StreamPart(fileStream, $"{photoTitle}"), token).ConfigureAwait(false); + } + + public Task GetThumbnailUri(string photoTitle, CancellationToken token) => _getThumbnailApiClient.GetThumbnailUri(photoTitle, token); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/Base/BaseViewModel.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/Base/BaseViewModel.cs new file mode 100644 index 0000000..d449207 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/Base/BaseViewModel.cs @@ -0,0 +1,5 @@ +namespace LambdaTriggers.Mobile; + +abstract partial class BaseViewModel : ObservableObject +{ +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/PhotoViewModel.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/PhotoViewModel.cs new file mode 100644 index 0000000..5177b94 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.Mobile/ViewModels/PhotoViewModel.cs @@ -0,0 +1,97 @@ +namespace LambdaTriggers.Mobile; + +partial class PhotoViewModel : BaseViewModel +{ + readonly WeakEventManager _eventManager = new(); + + readonly IDispatcher _dispatcher; + readonly IMediaPicker _mediaPicker; + readonly PhotosApiService _photosApiService; + + [ObservableProperty, NotifyCanExecuteChangedFor(nameof(UploadPhotoCommand))] + bool _isCapturingAndUploadingPhoto; + + [ObservableProperty] + Stream? _capturedPhoto; + + [ObservableProperty] + Uri? _thumbnailPhotoUri; + + public PhotoViewModel(IDispatcher dispatcher, IMediaPicker mediaPicker, PhotosApiService photosApiService) + { + _dispatcher = dispatcher; + _mediaPicker = mediaPicker; + _photosApiService = photosApiService; + } + + public event EventHandler Error + { + add => _eventManager.AddEventHandler(value); + remove => _eventManager.RemoveEventHandler(value); + } + + bool CanUploadPhotoExecute => !IsCapturingAndUploadingPhoto; + + [RelayCommand(CanExecute = nameof(CanUploadPhotoExecute))] + async Task UploadPhoto(CancellationToken token) + { + CapturedPhoto = null; + ThumbnailPhotoUri = null; + + try + { + var storageReadPermissionResult = await _dispatcher.DispatchAsync(Permissions.RequestAsync); + + if (storageReadPermissionResult is not PermissionStatus.Granted) + { + OnError("Storage Read Permission Not Granted"); + return; + } + + var storageWritePermissionResult = await _dispatcher.DispatchAsync(Permissions.RequestAsync); + + if (storageWritePermissionResult is not PermissionStatus.Granted) + { + OnError("Storage Write Permission Not Granted"); + return; + } + + var cameraPermissionResult = await _dispatcher.DispatchAsync(Permissions.RequestAsync); + + if (cameraPermissionResult is not PermissionStatus.Granted) + { + OnError("Camera Permission Not Granted"); + return; + } + + IsCapturingAndUploadingPhoto = true; + + var photo = await _dispatcher.DispatchAsync(() => _mediaPicker.CapturePhotoAsync(new() + { + Title = Guid.NewGuid().ToString() + })).ConfigureAwait(false); + + if (photo is null) + return; + + ThumbnailPhotoUri = null; + CapturedPhoto = await photo.OpenReadAsync().ConfigureAwait(false); + + await _photosApiService.UploadPhoto(photo.FileName, photo, token).ConfigureAwait(false); + + ThumbnailPhotoUri = await _photosApiService.GetThumbnailUri(photo.FileName, token).ConfigureAwait(false); + + await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + } + catch (Exception e) + { + OnError(e.Message); + } + finally + { + IsCapturingAndUploadingPhoto = false; + } + } + + void OnError(in string message) => _eventManager.HandleEvent(this, message, nameof(Error)); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/LambdaTriggers.UploadImage.csproj b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/LambdaTriggers.UploadImage.csproj new file mode 100644 index 0000000..0918697 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/LambdaTriggers.UploadImage.csproj @@ -0,0 +1,36 @@ + + + Exe + net7.0 + enable + enable + Lambda + bootstrap + + true + + + true + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/Properties/launchSettings.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/Properties/launchSettings.json new file mode 100644 index 0000000..5870340 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net7.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-7.0.exe" + } + } +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/UploadImage.cs b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/UploadImage.cs new file mode 100644 index 0000000..31a9bc1 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/UploadImage.cs @@ -0,0 +1,63 @@ +using System.Net; +using System.Text.Json; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using Amazon.S3; +using HttpMultipartParser; +using LambdaTriggers.Backend.Common; +using LambdaTriggers.Common; + +namespace LambdaTriggers.UploadImage; + +public sealed class UploadImage +{ + static readonly IAmazonS3 _s3Client = new AmazonS3Client(); + + public static async Task FunctionHandler(APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) + { + if (request.QueryStringParameters is null + || !request.QueryStringParameters.TryGetValue(Constants.ImageFileNameQueryParameter, out var filename) + || filename is null) + { + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = (int)HttpStatusCode.BadRequest, + Body = request.QueryStringParameters?.Any() is true + ? $"Invalid Request. Query Parameter, \"{request.QueryStringParameters.First().Value}\", Not Supported" + : $"Invalid Request. Missing Query Parameter \"{Constants.ImageFileNameQueryParameter}\"" + }; + } + + try + { + var multipartFormParser = await MultipartFormDataParser.ParseAsync(new MemoryStream(Convert.FromBase64String(request.Body))); + var image = multipartFormParser.Files[0].Data; + + var photoUri = await S3Service.UploadContentToS3(_s3Client, S3Service.BucketName, filename, image, context.Logger); + context.Logger.LogInformation("Saved Photo to S3"); + + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = (int)HttpStatusCode.OK, + Body = JsonSerializer.Serialize(photoUri) + }; + } + catch (Exception ex) + { + context.Logger.LogError(ex.Message); + + return new APIGatewayHttpApiV2ProxyResponse + { + StatusCode = (int)HttpStatusCode.InternalServerError, + Body = JsonSerializer.Serialize(ex.Message) + }; + } + } + + static Task Main(string[] args) => + LambdaBootstrapBuilder.Create((APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context) => FunctionHandler(request, context), new DefaultLambdaJsonSerializer()) + .Build() + .RunAsync(); +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/aws-lambda-tools-defaults.json b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..9c10992 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.UploadImage/aws-lambda-tools-defaults.json @@ -0,0 +1,28 @@ + +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile" : "VisualStudioToolkit", + "region" : "us-west-1", + "configuration" : "Release", + "function-runtime" : "provided.al2", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "bootstrap", + "msbuild-parameters" : "--self-contained true", + "framework" : "net7.0", + "function-name" : "LambdaTriggers_UploadImage", + "function-description" : "", + "package-type" : "Zip", + "function-role" : "arn:aws:iam::723361041013:role/lambda_exec_LambdaTriggers-0", + "function-architecture" : "x86_64", + "function-subnets" : "", + "function-security-groups" : "", + "tracing-mode" : "Active", + "environment-variables" : "", + "image-tag" : "" +} \ No newline at end of file diff --git a/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.sln b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.sln new file mode 100644 index 0000000..d9ff99a --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/LambdaTriggers.sln @@ -0,0 +1,71 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.GenerateThumbnail", "LambdaTriggers.GenerateThumbnail\LambdaTriggers.GenerateThumbnail.csproj", "{170009F9-B173-4896-BA10-C510C1E4BAF3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.UploadImage", "LambdaTriggers.UploadImage\LambdaTriggers.UploadImage.csproj", "{B6E24922-FB57-4089-8140-E508D75018B2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Backend", "Backend", "{5CD827CF-5A01-4CDE-BAC5-A2CD3CC6194A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.Mobile", "LambdaTriggers.Mobile\LambdaTriggers.Mobile.csproj", "{EC4A1A6B-34F4-44F1-85C8-C95197D854E6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.Backend.Common", "LambdaTriggers.Backend.Common\LambdaTriggers.Backend.Common.csproj", "{CD869696-DCE3-4AD1-B8B4-853E48A95F86}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{31DC17FF-7664-4437-BAD4-949626A85F75}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile", "Mobile", "{4E8C7AA8-81D4-4DF5-A8B2-E935BC6212D7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.Common", "LambdaTriggers.Common\LambdaTriggers.Common.csproj", "{736602EF-FBDF-48AF-BF16-85C564653846}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LambdaTriggers.GetThumbnail", "LambdaTriggers.GetThumbnail\LambdaTriggers.GetThumbnail.csproj", "{41567193-4BFB-46A7-9776-50CE6E2E9112}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {170009F9-B173-4896-BA10-C510C1E4BAF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {170009F9-B173-4896-BA10-C510C1E4BAF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {170009F9-B173-4896-BA10-C510C1E4BAF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {170009F9-B173-4896-BA10-C510C1E4BAF3}.Release|Any CPU.Build.0 = Release|Any CPU + {B6E24922-FB57-4089-8140-E508D75018B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6E24922-FB57-4089-8140-E508D75018B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6E24922-FB57-4089-8140-E508D75018B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6E24922-FB57-4089-8140-E508D75018B2}.Release|Any CPU.Build.0 = Release|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Release|Any CPU.Build.0 = Release|Any CPU + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6}.Release|Any CPU.Deploy.0 = Release|Any CPU + {CD869696-DCE3-4AD1-B8B4-853E48A95F86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD869696-DCE3-4AD1-B8B4-853E48A95F86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD869696-DCE3-4AD1-B8B4-853E48A95F86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD869696-DCE3-4AD1-B8B4-853E48A95F86}.Release|Any CPU.Build.0 = Release|Any CPU + {736602EF-FBDF-48AF-BF16-85C564653846}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {736602EF-FBDF-48AF-BF16-85C564653846}.Debug|Any CPU.Build.0 = Debug|Any CPU + {736602EF-FBDF-48AF-BF16-85C564653846}.Release|Any CPU.ActiveCfg = Release|Any CPU + {736602EF-FBDF-48AF-BF16-85C564653846}.Release|Any CPU.Build.0 = Release|Any CPU + {41567193-4BFB-46A7-9776-50CE6E2E9112}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41567193-4BFB-46A7-9776-50CE6E2E9112}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41567193-4BFB-46A7-9776-50CE6E2E9112}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41567193-4BFB-46A7-9776-50CE6E2E9112}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {170009F9-B173-4896-BA10-C510C1E4BAF3} = {5CD827CF-5A01-4CDE-BAC5-A2CD3CC6194A} + {B6E24922-FB57-4089-8140-E508D75018B2} = {5CD827CF-5A01-4CDE-BAC5-A2CD3CC6194A} + {EC4A1A6B-34F4-44F1-85C8-C95197D854E6} = {4E8C7AA8-81D4-4DF5-A8B2-E935BC6212D7} + {CD869696-DCE3-4AD1-B8B4-853E48A95F86} = {5CD827CF-5A01-4CDE-BAC5-A2CD3CC6194A} + {736602EF-FBDF-48AF-BF16-85C564653846} = {31DC17FF-7664-4437-BAD4-949626A85F75} + {41567193-4BFB-46A7-9776-50CE6E2E9112} = {5CD827CF-5A01-4CDE-BAC5-A2CD3CC6194A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6A9396FE-D752-4780-B5D8-FA52E925D40E} + EndGlobalSection +EndGlobal diff --git a/SampleApplications/2023/LambdaTriggersSample/README.md b/SampleApplications/2023/LambdaTriggersSample/README.md new file mode 100644 index 0000000..6d52291 --- /dev/null +++ b/SampleApplications/2023/LambdaTriggersSample/README.md @@ -0,0 +1,13 @@ +# Lambda Triggers Sample + +A sample app demonstrating an end-to-end mobile workflow using [.NET MAUI](https://learn.microsoft.com/en-us/dotnet/maui/?view=net-maui-7.0), + [Serverless AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-csharp.html) + [AWS S3 Storage](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/csharp_s3_code_examples.html) in C#. + +This sample demonstrates how to use AWS Lambda's [HTTP API Gateway Triggers](https://aws.amazon.com/blogs/developer/deploy-an-existing-asp-net-core-web-api-to-aws-lambda/) + [S3 Triggers](https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html) to automatically generate thumbnails of an uploaded image from a mobile app. + +1. The .NET MAUI mobile app captures a photo +2. The .NET MAUI mobile app uploads photo to AWS via an AWS Lambda using an API Gateway HTTP trigger +3. The AWS Lambda API Gateway Function saves the image to AWS S3 Storage +4. An AWS Lambda S3 Trigger automatically generates a downscaled thumbnail of the image and saves the thumbnail image back to S3 Storage +5. The .NET MAUI mobile app retrives the thumbnail image via an AWS Lambda using an API Gateway HTTP trigger and displays it on screen + +![](https://user-images.githubusercontent.com/13558917/214541434-0244c7f0-cc13-4273-89b0-af5ffd9f9786.png)