[
  {
    "path": ".eslintrc.cjs",
    "content": "module.exports = {\n    \"env\": {\n        \"es2021\": true,\n        \"node\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"overrides\": [\n        {\n            \"env\": {\n                \"node\": true\n            },\n            \"files\": [\n                \".eslintrc.{js,cjs}\"\n            ],\n            \"parserOptions\": {\n                \"sourceType\": \"script\"\n            }\n        }\n    ],\n    \"parserOptions\": {\n        \"ecmaVersion\": \"latest\",\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n    }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Typescript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# package builds\nbuild\nbundle.js\n\n# CloudFormation packaged templates\npackaged-template.yml\n"
  },
  {
    "path": ".jshintrc",
    "content": "{\n    \"node\": true,\n    \"esversion\": 6\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017-2023 Evan Chiu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# serverless-galleria\n\nServerless batch photo manipulation and publishing\n\n## Design\n### Uploader\nThe Uploader stores images in an S3 bucket.  It doesn't own any buckets.\n\n![uploader](diagrams/uploader.png)\n\n* [uploader](uploader) - Serverless web application for uploading images to S3\n\n### Transformations\nA transform owns an S3 bucket, which it watches for incoming files.  When files are added, it runs a lambda function to transform the images and place them in another S3 bucket\n\n![transform](diagrams/transform.png)\n\n* [blur](blur) - Apply a configurable blur\n* [compress](compress) - Apply image compression to reduce image file size\n* [crop](crop) - Apply a configurable crop\n* [resize](resize) - Apply a configurable resize\n* [rotate](rotate) - Apply a configurable rotation with configurable background color\n* [sepia](sepia) - Apply sepia tone\n\n### Galleria\nThe Galleria reads from two S3 buckets, one for reading image thumbnails, the other for full-size images.  It doesn't own any buckets.\n\n![galleria](diagrams/galleria.png)\n\n* [galleria](galleria) - Beautiful web interface for displaying photo gallery\n\n## Setup\nFirst, plan your pipeline, as you'll build it backwards.  Here's a sample:\n\n![pipeline](diagrams/pipeline.png)\n\n### Steps\n1. Deploy Application\n    1. Create the thumbs bucket, as it's not owned by any transformations\n    1. Deploy the compress transform, with the resized bucket as its source, and thumbs bucket as its destination\n    1. Deploy the resize transform, with the rotated bucket as its source, and resized bucket as its destination\n    1. Deploy the rotate transform, with the originals bucket as its source, and rotated bucket as its destination\n    1. Deploy the uploader, with the originals bucket as its destination\n    1. Deploy the galleria, with originals as its full-size bucket, and thumbs as its thumb bucket\n1. Upload photos\n    1. In the [API Gateway Console](https://console.aws.amazon.com/apigateway), navigate to APIs / uploader / Dashboard\n    1. Find the Invocation url, something like *https://xxxxxxxxx.execute-api.region.amazonaws.com/Prod/*\n        1. (You can also set up [custom domain name](http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html))\n    1. Open the invocation url in your browser, and drag photos on to the drop point to upload\n1. View galleria\n    1. Set up a [custom domain name](http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html) for the galleria API, then open it in your browser\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "blur/README.md",
    "content": "# blur\n\nCopy and apply a blur to images using Lambda.\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the blurred output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Choose the blur pixel radius, store it in shell variable:\n  * `export BLUR_RADIUS=10`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~blur) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [blur](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~blur) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "blur/SAR_README.md",
    "content": "# blur\n\nServerless image blur\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG blurer.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~blur) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [blur](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~blur) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "blur/package.json",
    "content": "{\n  \"name\": \"blur\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Blur transformation for Serverless Galleria\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-blur-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET blurRadius=$BLUR_RADIUS\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Serverless\",\n    \"ImageMagick\",\n    \"Blur\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "blur/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "blur/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  const blurRadius = parseInt(process.env.BLUR_RADIUS);\n  if (typeof blurRadius !== \"number\") {\n    throw new Error(\"Error: Environment variable BLUR_RADIUS missing\");\n  }\n  console.log(`Transforming with blur radius (${blurRadius})`);\n  \n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.blur(blurRadius);\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}\n"
  },
  {
    "path": "blur/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by applying blur of a configured radius\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by applying blur of a configured radius\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n          BLUR_RADIUS:\n            Ref: blurRadius\n          NODE_OPTIONS: --enable-source-maps\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n  blurRadius:\n    Type: Number\n    Description: Pixel radius of the blur to apply\n    Default: 10\n"
  },
  {
    "path": "compress/README.md",
    "content": "# compress\n\nCopy and compress images using Lambda.\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the compressed output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Choose the JPEG compression quality, 1 to 100, 100 is best quality/largest file, (See [docs](http://www.graphicsmagick.org/GraphicsMagick.html#details-quality)), store it in shell variable:\n  * `export QUALITY=25`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~compress) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [compress](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~compress) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "compress/SAR_README.md",
    "content": "# compress\n\nServerless image compression\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG image compressor.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~compress) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [compress](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~compress) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "compress/package.json",
    "content": "{\n  \"name\": \"compress\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Copy and compress images using Lambda\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-compress-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET quality=$QUALITY\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Compress\",\n    \"Image\",\n    \"ImageMagick\",\n    \"S3\",\n    \"Serverless\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "compress/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "compress/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  const quality = parseInt(process.env.QUALITY);\n  if (typeof quality !== \"number\") {\n    throw new Error(\"Error: Environment variable QUALITY missing\");\n  }\n  console.log(`Transforming with quality (${quality})`);\n\n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.quality(quality);\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}\n"
  },
  {
    "path": "compress/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by compression to a configured quality level\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by compression to a configured quality level\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n          QUALITY:\n            Ref: quality\n          NODE_OPTIONS: --enable-source-maps\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n  quality:\n    Type: Number\n    Description: Quality of Jpeg output, 1 to 100, 100 is best quality/largest file\n    Default: 25\n"
  },
  {
    "path": "crop/README.md",
    "content": "# crop\n\nCopy and crop images using Lambda.\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the cropped output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Choose the width in pixels, store it in shell variable:\n  * `export WIDTH=600`\n* Choose the height in pixels, store it in shell variable:\n  * `export HEIGHT=400`\n* Choose the x in pixels (number of pixels right from left edge to remove), store it in shell variable:\n  * `export X=50`\n* Choose the y in pixels (number of pixels down from the top to remove), store it in shell variable:\n  * `export Y=50`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~crop) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [crop](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~crop) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "crop/SAR_README.md",
    "content": "# crop\n\nServerless image crop\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG crop tool.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~crop) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [crop](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~crop) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "crop/package.json",
    "content": "{\n  \"name\": \"crop\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Copy and crop images using Lambda\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-crop-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET width=$WIDTH height=$HEIGHT xCoordinate=$X yCoordinate=$Y\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Crop\",\n    \"Image\",\n    \"ImageMagick\",\n    \"Serverless\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "crop/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "crop/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  const width = parseInt(process.env.WIDTH);\n  const height = parseInt(process.env.HEIGHT);\n  let x = parseInt(process.env.X_COORDINATE);\n  let y = parseInt(process.env.Y_COORDINATE);\n  if (typeof width !== \"number\") {\n    throw new Error(\"Error: Environment variable WIDTH missing\");\n  }\n  if (typeof height !== \"number\") {\n    throw new Error(\"Error: Environment variable HEIGHT missing\");\n  }\n  if (typeof x !== \"number\") {\n    throw new Error(\"Error: Environment variable X_COORDINATE missing\");\n  }\n  if (typeof y !== \"number\") {\n    throw new Error(\"Error: Environment variable Y_COORDINATE missing\");\n  }\n  console.log(`Cropping ${JSON.stringify({ width, height, x, y })}`);\n\n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.crop(x, y, width, height);\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}"
  },
  {
    "path": "crop/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by cropping\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by cropping\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n          WIDTH:\n            Ref: width\n          HEIGHT:\n            Ref: height\n          X_COORDINATE:\n            Ref: xCoordinate\n          Y_COORDINATE:\n            Ref: yCoordinate\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n  width:\n    Type: Number\n    Description: Width of cropped image\n    Default: 600\n  height:\n    Type: Number\n    Description: Height of cropped image\n    Default: 400\n  xCoordinate:\n    Type: Number\n    Description: (x, y) is the upper left corner of the cropped region\n    Default: 50\n  yCoordinate:\n    Type: Number\n    Description: (x, y) is the upper left corner of the cropped region\n    Default: 50\n"
  },
  {
    "path": "galleria/README.md",
    "content": "# galleria\n\nServerless photo gallery\n\nDemo: https://galleria.evanchiu.com\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create an S3 bucket from which to read the thumbnails, store its name in shell variable:\n  * `export THUMB_BUCKET=bucket`\n* Create an S3 bucket from which to read the full size images, store its name in shell variable:\n  * `export FULL_BUCKET=bucket`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the code, thumnail, and full size buckets\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~galleria) page\n\n## Use\n* Go to [API Gateway](https://console.aws.amazon.com/apigateway/home) in the AWS Console to find the invoke URL and open it in your browser.\n* Optionally, you can set up a [custom domain](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html)\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [galleria](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~galleria) on the AWS Serverless Application Repository\n* Theme is [photo](https://freehtml5.co/photo-free-website-template-using-bootstrap-for-photographer/) from [freehtml5.co](https://freehtml5.co)\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "galleria/SAR_README.md",
    "content": "# galleria\n\nServerless web application displaying a photo gallery\n\nThis application is designed to be used with [serverless-galleria](https://github.com/evanchiu/serverless-galleria), but it can also function as a generic web interface to images in S3 buckets.\n\n## Deploy\n* Create an S3 bucket to hold the thumbnail images\n* Create an S3 bucket to hold the fullsize images\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, then the fullsize bucket will probably be created by one of your transforms\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~galleria) page\n\n## Use\n1. In the [API Gateway Console](https://console.aws.amazon.com/apigateway)\n1. Navigate to APIs / aws-serverless-repository-galleria / Settings\n    1. Hit Add Binary Media Type\n    1. Enter `*/*` in the box\n    1. Hit Save Changes\n    1. Navigate to APIs / aws-serverless-repository-galleria / Resources\n    1. Click the Actions dropdown\n    1. Click Deploy API\n        1. Deployment stage: **prod**\n        1. Deployment description: *Adding binary support*\n        1. Hit Deploy\n1. Set up a [custom domain name](http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html)\n1. Open the custom domain in your browser to view the gallery\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [galleria](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~galleria) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "galleria/package.json",
    "content": "{\n  \"name\": \"galleria\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Serverless photo gallery\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config && zip -r galleria.zip bundle.js public\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-galleria-$USER --parameter-overrides thumbBucket=$THUMB_BUCKET fullBucket=$FULL_BUCKET\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Galleria\",\n    \"Gallery\",\n    \"Image\",\n    \"Photo\",\n    \"S3\",\n    \"Serverless\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  },\n  \"dependencies\": {\n    \"mime-types\": \"^2.1.30\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  }\n}\n"
  },
  {
    "path": "galleria/public/index.template.html",
    "content": "<!DOCTYPE HTML>\n<html>\n  <head>\n    <title>Serverless Galleria</title>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\" />\n    <link rel=\"stylesheet\" href=\"https://galleria-static.evanchiu.com/assets/css/main.css\" />\n    <noscript><link rel=\"stylesheet\" href=\"https://galleria-static.evanchiu.com/assets/css/noscript.css\" /></noscript>\n  </head>\n  <body class=\"is-preload\">\n\n    <!-- Wrapper -->\n      <div id=\"wrapper\">\n\n        <!-- Header -->\n          <header id=\"header\">\n            <h1><strong>Serverless Galleria</strong> by Evan Chiu</h1>\n            <nav>\n              <ul>\n                <li><a href=\"#footer\" class=\"icon solid fa-info-circle\">About</a></li>\n              </ul>\n            </nav>\n          </header>\n\n        <!-- Main -->\n          <div id=\"main\">\n            {{photos}}\n          </div>\n\n    <!-- Footer -->\n      <footer id=\"footer\" class=\"panel\">\n        <div class=\"inner split\">\n          <div>\n            <section>\n              <h2>Hello</h2>\n              <p>Welcome to the Serverless Galleria demo. This a serverless batch photo manipulation and publishing suite of functions.  Serverless functions run on demand, so you pay only for the resources you use.</p>\n            </section>\n            <section>\n              <ul class=\"icons\">\n                <li><a href=\"https://github.com/evanchiu\" class=\"icon brands fa-github\"><span class=\"label\">GitHub</span></a></li>\n                <li><a href=\"https://linkedin.com/in/evanchiu\" class=\"icon brands fa-linkedin-in\"><span class=\"label\">LinkedIn</span></a></li>\n                <li><a href=\"https://twitter.com/evanchiu\" class=\"icon brands fa-twitter\"><span class=\"label\">Twitter</span></a></li>\n                <li><a href=\"https://dribbble.com/evanchiu\" class=\"icon brands fa-dribbble\"><span class=\"label\">Dribbble</span></a></li>\n                <li><a href=\"https://instagram.com/evanchiu\" class=\"icon brands fa-instagram\"><span class=\"label\">Instagram</span></a></li>\n                <li><a href=\"https://facebook.com/evan.chiu\" class=\"icon brands fa-facebook-f\"><span class=\"label\">Facebook</span></a></li>\n              </ul>\n            </section>\n            <p class=\"copyright\">\n              &copy;2017-2023 <a href=\"https://evanchiu.com/\">Evan Chiu</a>. Design: <a href=\"https://html5up.net/multiverse\">Multiverse</a> by <a href=\"http://html5up.net\">HTML5 UP</a>.\n            </p>\n          </div>\n          <div>\n            <section>\n              <h2>Try it out</h2>\n              <p>Serverless Galleria provides a web interface for drag and drop uploading files, several photo transformations to optimize the images for the web, and this beautiful front end for displaying the photos. To deploy your own stack, get started with the <a href=\"https://github.com/evanchiu/serverless-galleria\">readme</a>.</p>\n            </section>\n          </div>\n        </div>\n      </footer>\n  </div>\n\n<!-- Scripts -->\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/jquery.min.js\"></script>\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/jquery.poptrox.min.js\"></script>\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/browser.min.js\"></script>\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/breakpoints.min.js\"></script>\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/util.js\"></script>\n  <script src=\"https://galleria-static.evanchiu.com/assets/js/main.js\"></script>\n\n</body>\n</html>"
  },
  {
    "path": "galleria/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "galleria/src/index.js",
    "content": "import { readFile } from \"fs/promises\";\nimport { lookup } from \"mime-types\";\nimport { join, resolve } from \"path\";\nimport { done, get, list } from \"../../serverless-galleria-util\";\n\nconst THUMB_BUCKET = process.env.THUMB_BUCKET;\nconst FULL_BUCKET = process.env.FULL_BUCKET;\n\nexport async function handler(event) {\n  // Fail on mising config\n  if (!THUMB_BUCKET) {\n    console.error(\"Error: Environment variable THUMB_BUCKET missing\");\n    return done(500, '{\"message\":\"Internal Server Error\"}');\n  }\n  if (!FULL_BUCKET) {\n    console.error(\"Error: Environment variable FULL_BUCKET missing\");\n    return done(500, '{\"message\":\"Internal Server Error\"}');\n  }\n\n  if (\n    event.path.startsWith(\"/api/thumb/\") ||\n    event.path.startsWith(\"/api/full/\")\n  ) {\n    return imageRoute(event);\n  } else {\n    return servePublic(event);\n  }\n}\n\nasync function imageRoute(event) {\n  if (event.httpMethod !== \"GET\") {\n    return done(400, '{\"message\":\"Invalid HTTP Method\"}');\n  }\n\n  const bucket = event.path.startsWith(\"/api/thumb/\")\n    ? THUMB_BUCKET\n    : FULL_BUCKET;\n  const key = event.path.replace(/\\/api\\/(full|thumb)\\//, \"\");\n  const mimeType = lookup(key);\n\n  try {\n    const data = await get(bucket, key);\n    if (\n      mimeType === \"image/png\" ||\n      mimeType === \"image/jpeg\" ||\n      mimeType === \"image/x-icon\"\n    ) {\n      // Base 64 encode binary images\n      console.log(`Serving binary ${bucket}:${key} (${mimeType})`);\n      return done(200, data.toString(\"base64\"), mimeType, true);\n    } else {\n      console.log(`Serving text ${bucket}:${key} (${mimeType})`);\n      return done(200, data.toString(), mimeType);\n    }\n  } catch (error) {\n    console.error(error);\n    return done(500, '{\"message\":\"Internal Server Error\"}');\n  }\n}\n\nasync function servePublic(event) {\n  console.log(`Serving public for ${event.path}`);\n  // Set urlPath\n  let urlPath;\n  if (event.path === \"/\") {\n    return serveIndex(event);\n  } else {\n    urlPath = event.path;\n  }\n\n  // Determine the file's path on lambda's filesystem\n  const publicPath = join(process.env.LAMBDA_TASK_ROOT, \"public\");\n  const filePath = resolve(join(publicPath, urlPath));\n  const mimeType = lookup(filePath);\n\n  // Make sure the user doesn't try to break out of the public directory\n  if (!filePath.startsWith(publicPath)) {\n    console.log(\"forbidden\", filePath, publicPath);\n    return done(403, '{\"message\":\"Forbidden\"}');\n  }\n\n  // Attempt to read the file, give a 404 on error\n  try {\n    const data = await readFile(filePath);\n    if (\n      mimeType === \"image/png\" ||\n      mimeType === \"image/jpeg\" ||\n      mimeType === \"image/x-icon\" ||\n      mimeType === \"application/font-woff\" ||\n      mimeType === \"application/font-woff2\" ||\n      mimeType === \"application/vnd.ms-fontobject\" ||\n      mimeType === \"application/x-font-ttf\"\n    ) {\n      // Base 64 encode binary images\n      return done(200, data.toString(\"base64\"), mimeType, true);\n    } else {\n      return done(200, data.toString(), mimeType);\n    }\n  } catch (e) {\n    console.error(\"404\", e);\n    return done(404, '{\"message\":\"Not Found\"}');\n  }\n}\n\n// Serve the index page\nasync function serveIndex(event) {\n  console.log(\"Serving index\");\n  // Determine base path on whether the API Gateway stage is in the path or not\n  let base_path = \"/\";\n  if (event.requestContext.path.startsWith(\"/\" + event.requestContext.stage)) {\n    base_path = \"/\" + event.requestContext.stage + \"/\";\n  }\n\n  let filePath = join(\n    process.env.LAMBDA_TASK_ROOT,\n    \"public/index.template.html\"\n  );\n  const thumbBaseUrl =\n    \"https://\" + event.headers.Host + base_path + \"api/thumb/\";\n  const fullBaseUrl = \"https://\" + event.headers.Host + base_path + \"api/full/\";\n\n  // Read the file, fill in base_path and serve, or 404 on error\n  try {\n    const [indexTemplate, images] = await Promise.all([\n      readFile(filePath),\n      list(THUMB_BUCKET),\n    ]);\n\n    const html = images\n      .map((image) => {\n        return `<article class=\"thumb\">\\n  <a href=\"${\n          fullBaseUrl + image.Key\n        }\" class=\"image\"><img src=\"${\n          thumbBaseUrl + image.Key\n        }\" alt=\"\" /></a>\\n</article>\\n`;\n      })\n      .join(\"\\n\");\n\n    let output = indexTemplate.toString().replace(\"{{photos}}\", html);\n    return done(200, output, \"text/html\");\n  } catch (error) {\n    console.error(\"404\", error);\n    return done(404, '{\"message\":\"Not Found\"}');\n  }\n}\n"
  },
  {
    "path": "galleria/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Serverless photo gallery\n\nGlobals:\n  Api:\n    BinaryMediaTypes:\n      - '*~1*'\n\nResources:\n  galleria:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Serverless photo gallery\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: galleria.zip\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: thumbBucket\n        - S3ReadPolicy:\n            BucketName:\n              Ref: fullBucket\n      Timeout: 60\n      Events:\n        root:\n          Type: Api\n          Properties:\n            Path: /\n            Method: get\n        getProxy:\n          Type: Api\n          Properties:\n            Path: '/{proxy+}'\n            Method: get\n      Environment:\n        Variables:\n          THUMB_BUCKET:\n            Ref: thumbBucket\n          FULL_BUCKET:\n            Ref: fullBucket\nParameters:\n  thumbBucket:\n    Type: String\n    Description: Name of the S3 Bucket from which to read the thumbnails (must exist prior to deployment)\n  fullBucket:\n    Type: String\n    Description: Name of the S3 Bucket from which to read the full size images (must exist prior to deployment)\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"serverless-galleria\",\n  \"version\": \"1.2.0\",\n  \"description\": \"Serverless batch photo upload, manipulation, and publishing\",\n  \"private\": true,\n  \"workspaces\": [\n    \"blur\",\n    \"compress\",\n    \"crop\",\n    \"galleria\",\n    \"resize\",\n    \"rotate\",\n    \"sepia\",\n    \"serverless-galleria-util\",\n    \"uploader\"\n  ],\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "resize/README.md",
    "content": "# resize\n\nCopy and resize images using Lambda.\n\n## Max Dimension\n* For this function, you'll specify the max dimension in pixels. The function will keep the images in their original width/height ratios, limiting the larger dimension to the given maximum.\n* E.g. with a max dimension of 100, an 800 x 600 image will be 100 x 75, but a 200 x 400 image will be 50 x 100\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the resized output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Choose the max dimension in pixels, store it in shell variable:\n  * `export MAX_DIMENSION=300`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~resize) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [resize](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~resize) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "resize/SAR_README.md",
    "content": "# resize\n\nServerless image resize\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG resizer.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~resize) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [resize](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~resize) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "resize/package.json",
    "content": "{\n  \"name\": \"resize\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Copy and resize images using Lambda\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-resize-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET maxDimension=$MAX_DIMENSION\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Image\",\n    \"ImageMagick\",\n    \"Resize\",\n    \"Serverless\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "resize/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "resize/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  const maxDimension = parseInt(process.env.MAX_DIMENSION);\n  if (typeof maxDimension !== \"number\") {\n    throw new Error(\"Error: Environment variable MAX_DIMENSION missing\");\n  }\n  console.log(`Resizing to max dimension (${maxDimension})`);\n\n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.scaleToFit(maxDimension, maxDimension);\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}"
  },
  {
    "path": "resize/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by resizing to a configured max dimension\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by resizing to a configured max dimension\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n          MAX_DIMENSION:\n            Ref: maxDimension\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n  maxDimension:\n    Type: Number\n    Description: Maximum dimension length in pixels\n    Default: 300\n"
  },
  {
    "path": "rotate/README.md",
    "content": "# rotate\n\nCopy and rotate images using Lambda.\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the rotated output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Choose the number of degrees to rotate, store it in shell variable:\n  * `export ROTATE_DEGREES=30`\n* Choose the background color in hex (e.g. `#RRGGBB`), store it in shell variable:\n  * `export BACKGROUND_COLOR='#00CCFF'`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~rotate) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [rotate](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~rotate) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "rotate/SAR_README.md",
    "content": "# rotate\n\nServerless image rotation\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG rotator.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~rotate) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [rotate](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~rotate) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "rotate/package.json",
    "content": "{\n  \"name\": \"rotate\",\n  \"version\": \"1.2.1\",\n  \"description\": \"rotate transformation for Serverless Galleria\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-rotate-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET rotateDegrees=$ROTATE_DEGREES backgroundColor=\\\"$BACKGROUND_COLOR\\\"\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Image\",\n    \"ImageMagick\",\n    \"Rotate\",\n    \"Serverless\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "rotate/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "rotate/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  const rotateDegrees = parseInt(process.env.ROTATE_DEGREES);\n  const backgroundColor = process.env.BACKGROUND_COLOR;\n  if (typeof rotateDegrees !== \"number\") {\n    throw new Error(\"Error: Environment variable ROTATE_DEGREES missing\");\n  }\n  if (!backgroundColor.match(/^#[0-9a-fA-F]{6}$/)) {\n    throw new Error(\"Error: Expected BACKGROUND_COLOR hex format, e.g. \\\"#00CCFF\\\"\")\n  }\n  console.log(`Rotating ${JSON.stringify(rotateDegrees, backgroundColor)}`);\n\n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.background(Jimp.cssColorToHex(backgroundColor)).rotate(rotateDegrees);\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}"
  },
  {
    "path": "rotate/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by rotating a configured degree\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by rotating a configured degree\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n          ROTATE_DEGREES:\n            Ref: rotateDegrees\n          BACKGROUND_COLOR:\n            Ref: backgroundColor\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n  rotateDegrees:\n    Type: Number\n    Description: Number of degrees to rotate clockwise\n    Default: 10\n  backgroundColor:\n    Type: String\n    Description: Background color to fill in (#RRGGBB)\n    Default: '#000000'\n"
  },
  {
    "path": "sepia/README.md",
    "content": "# sepia\n\nCopy and apply a sepia tone to images using Lambda.\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create the S3 bucket for the sepia-toned output, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Choose a name, but do NOT create the S3 bucket input comes from, store its name in shell variable:\n  * `export SOURCE_BUCKET=bucket`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~sepia) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [sepia](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~sepia) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "sepia/SAR_README.md",
    "content": "# sepia\n\nServerless image sepia tone application\n\nThis application is designed to be used as a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) transform, but it can also function as a generic JPEG sepia application.\n\n## Deploy from the AWS Serverless Application Repository\n* Create the destination bucket\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, this bucket will be created by the following transform, unless this is the last transform.\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~sepia) page\n\n## Use\n* Images that you put into the source bucket will be transformed, then put into the destination bucket\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [sepia](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~sepia) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "sepia/package.json",
    "content": "{\n  \"name\": \"sepia\",\n  \"version\": \"1.2.1\",\n  \"description\": \"Sepia transformation for Serverless Galleria\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-sepia-$USER --parameter-overrides sourceBucket=$SOURCE_BUCKET destBucket=$DEST_BUCKET\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Serverless\",\n    \"ImageMagick\",\n    \"Sepia\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"dependencies\": {\n    \"jimp\": \"^0.22.10\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  }\n}\n"
  },
  {
    "path": "sepia/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "sepia/src/index.js",
    "content": "import Jimp from \"jimp\";\nimport { handle } from \"serverless-galleria-util\";\n\n/** Handle the event from s3 */\nexport async function handler(event) {\n  console.log(`Applying sepia`);\n\n  await handle(event, async (inBuffer) => {\n    const image = await Jimp.read(inBuffer);\n    image.sepia();\n    return image.getBufferAsync(Jimp.MIME_JPEG);\n  });\n}"
  },
  {
    "path": "sepia/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Transforms images by applying sepia tone\nResources:\n  transform:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Transforms images by applying sepia tone\n      Handler: bundle.handler\n      Runtime: nodejs18.x\n      CodeUri: bundle.js\n      MemorySize: 1536\n      Policies:\n        - S3ReadPolicy:\n            BucketName:\n              Ref: sourceBucket\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 300\n      Events:\n        upload:\n          Type: S3\n          Properties:\n            Bucket:\n              Ref: source\n            Events: s3:ObjectCreated:*\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n  source:\n    Type: AWS::S3::Bucket\n    Properties:\n      BucketName:\n        Ref: sourceBucket\nParameters:\n  sourceBucket:\n    Type: String\n    Description: Name of the S3 Bucket to read source images from (must NOT exist prior to deployment)\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put transformed images into (must exist prior to deployment)\n"
  },
  {
    "path": "serverless-galleria-util/index.js",
    "content": "import {\n  GetObjectCommand,\n  ListObjectsV2Command,\n  PutObjectCommand,\n  S3Client,\n} from \"@aws-sdk/client-s3\";\nconst s3 = new S3Client();\n\n/** Handle the s3 event by loading the s3 record files from s3, running the given transformer on each, saving to the dest bucket */\nexport async function handle(event, transformer) {\n  // Fail on mising config\n  const destBucket = process.env.DEST_BUCKET;\n  if (!destBucket) {\n    throw new Error(\"Error: Environment variable DEST_BUCKET missing\");\n  }\n\n  // Transform all records\n  await Promise.all(\n    event.Records.map(async (record) => {\n      const srcBucket = record.s3.bucket.name;\n      const key = decodeURIComponent(record.s3.object.key.replace(/\\+/g, \" \"));\n      console.log(\n        `Transforming ${srcBucket}:${key} to ${destBucket}:${key}...`\n      );\n      const original = await get(srcBucket, key);\n      const modified = await transformer(original);\n      await put(destBucket, key, modified);\n      console.log(`Transformed ${srcBucket}:${key} to ${destBucket}:${key}`);\n    })\n  );\n}\n\n/** Get file from S3 as a Buffer */\nexport async function get(bucket, key) {\n  const response = await s3.send(\n    new GetObjectCommand({\n      Bucket: bucket,\n      Key: key,\n    })\n  );\n  return Buffer.concat(await response.Body.toArray());\n}\n\n/** Put data into S3 */\nexport async function put(bucket, key, data) {\n  return s3.send(\n    new PutObjectCommand({\n      Bucket: bucket,\n      Key: key,\n      Body: data,\n    })\n  );\n}\n\n/** List the contents of an S3 bucket (up to first 1000 items) */\nexport async function list(bucket) {\n  const response = await s3.send(new ListObjectsV2Command({ Bucket: bucket }));\n  return response.Contents;\n}\n\n/** We're done with this API Gateway lambda, return to the client with given parameters */\nexport function done(\n  statusCode,\n  body,\n  contentType = \"application/json\",\n  isBase64Encoded = false\n) {\n  return {\n    statusCode: statusCode,\n    isBase64Encoded: isBase64Encoded,\n    body: body,\n    headers: {\n      \"Content-Type\": contentType,\n    },\n  };\n}"
  },
  {
    "path": "serverless-galleria-util/package.json",
    "content": "{\n  \"name\": \"serverless-galleria-util\",\n  \"version\": \"1.2.0\",\n  \"description\": \"Utility functions shared across serverless galleria lambdas\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "serverless-galleria-util/rollup.config.js",
    "content": "import { nodeResolve } from '@rollup/plugin-node-resolve';\nimport commonjs from '@rollup/plugin-commonjs';\nimport json from '@rollup/plugin-json';\n\nexport default {\n\tinput: 'src/index.js',\n\toutput: {\n\t\tfile: 'bundle.js',\n        sourcemap: \"inline\",\n\t\tformat: 'cjs'\n\t},\n    external: [/@aws-sdk\\/.*/],\n    plugins: [nodeResolve({preferBuiltins: true}), commonjs(), json()]\n};"
  },
  {
    "path": "uploader/.eslintrc.cjs",
    "content": "module.exports = {\n    \"env\": {\n        \"es2021\": true,\n        \"node\": true\n    },\n    \"extends\": \"eslint:recommended\",\n    \"overrides\": [\n        {\n            \"env\": {\n                \"node\": true\n            },\n            \"files\": [\n                \".eslintrc.{js,cjs}\"\n            ],\n            \"parserOptions\": {\n                \"sourceType\": \"script\"\n            }\n        }\n    ],\n    \"parserOptions\": {\n        \"ecmaVersion\": \"latest\",\n        \"sourceType\": \"module\"\n    },\n    \"rules\": {\n    }\n}\n"
  },
  {
    "path": "uploader/README.md",
    "content": "# uploader\n\nServerless web application for uploading files to S3\n\n## Deploy with CloudFormation\n\nPrerequisites: [Node.js](https://nodejs.org/en/) and [AWS CLI](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) installed\n\n* Create an [AWS](https://aws.amazon.com/) Account and [IAM User](https://aws.amazon.com/iam/) with the `AdministratorAccess` AWS [Managed Policy](http://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html)\n* Run `aws configure` to put store that user's credentials in `~/.aws/credentials`\n* Create an S3 bucket for storing the Lambda code and store its name in a shell variable with:\n  * `export CODE_BUCKET=bucket`\n* Create an S3 bucket for saving the uploaded files, store its name in shell variable:\n  * `export DEST_BUCKET=bucket`\n* Npm install:\n  * `npm install`\n* Build:\n  * `npm run build`\n* Upload package to S3, transform the CloudFormation template:\n  * `npm run package`\n* Deploy to CloudFormation:\n  * `npm run deploy`\n\n## Deploy from the AWS Serverless Application Repository\n* Create the code and destination buckets\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~uploader) page\n\n## Use\n* Go to [API Gateway](https://console.aws.amazon.com/apigateway/home) in the AWS Console to find the invoke URL and open it in your browser.\n* Files you upload will be stored in the configured S3 bucket\n* Optionally, you can set up a [custom domain](https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html)\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [uploader](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~uploader) on the AWS Serverless Application Repository\n\n## Limitations\nUploads happen in a single post.  The [lambda invocation payload limit is 6 MB](https://docs.aws.amazon.com/lambda/latest/dg/limits.html), and it gets transferred into lambda with [base64](https://en.wikipedia.org/wiki/Base64) encoding, which adds 33% overhead, in addition to the rest of the payload. The expected maximum upload size is around 4 MB.\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "uploader/SAR_README.md",
    "content": "# uploader\n\nServerless web application for uploading files to S3\n\nThis application is designed to be used with [serverless-galleria](https://github.com/evanchiu/serverless-galleria), but it can also function as a generic web to S3 file uploader.\n\n## Deploy\n* Create an S3 bucket to hold the uploaded content\n  * Note that if you're using a [serverless-galleria](https://github.com/evanchiu/serverless-galleria) image processing pipeline, the bucket you'll upload to will be created by the transform\n* Hit \"Deploy\" from the [application](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~uploader) page\n\n## Use\n1. In the [API Gateway Console](https://console.aws.amazon.com/apigateway)\n1. Navigate to APIs / serverlessrepo-uploader / Dashboard\n    1. Find the Invocation url, something like *https://xxxxxxxxx.execute-api.region.amazonaws.com/Prod/*\n    1. (You can also set up [custom domain name](http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html))\n1. Open the invocation url in your browser, and drag photos on to the drop point to upload\n\n## Links\n* [serverless-galleria](https://github.com/evanchiu/serverless-galleria) on Github\n* [uploader](https://serverlessrepo.aws.amazon.com/#/applications/arn:aws:serverlessrepo:us-east-1:233054207705:applications~uploader) on the AWS Serverless Application Repository\n\n## License\n&copy; 2017-2023 [Evan Chiu](https://evanchiu.com). This project is available under the terms of the MIT license.\n"
  },
  {
    "path": "uploader/package.json",
    "content": "{\n  \"name\": \"uploader\",\n  \"version\": \"1.2.2\",\n  \"description\": \"Serverless web application for uploading files to S3\",\n  \"type\": \"module\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"lint\": \"eslint src\",\n    \"build\": \"rollup --config && zip -r uploader.zip bundle.js public\",\n    \"package\": \"aws cloudformation package --template-file template.yml --output-template-file packaged-template.yml --s3-bucket $CODE_BUCKET\",\n    \"deploy\": \"aws cloudformation deploy --template-file packaged-template.yml --capabilities CAPABILITY_IAM --stack-name dev-uploader-$USER --parameter-overrides destBucket=$DEST_BUCKET\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/evanchiu/serverless-galleria.git\"\n  },\n  \"keywords\": [\n    \"Serverless\",\n    \"Image\",\n    \"Upload\",\n    \"Uploader\",\n    \"S3\"\n  ],\n  \"author\": \"Evan Chiu <evan@evanchiu.com>\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/evanchiu/serverless-galleria/issues\"\n  },\n  \"homepage\": \"https://github.com/evanchiu/serverless-galleria#readme\",\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^25.0.7\",\n    \"@rollup/plugin-json\": \"^6.0.1\",\n    \"@rollup/plugin-node-resolve\": \"^15.2.3\",\n    \"eslint\": \"^8.52.0\",\n    \"rollup\": \"^4.1.4\"\n  },\n  \"dependencies\": {\n    \"mime-types\": \"^2.1.17\",\n    \"serverless-galleria-util\": \"1.2.0\"\n  }\n}\n"
  },
  {
    "path": "uploader/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <title>Uploader</title>\n  <style>\n    .aligner {\n      text-align: center;\n    }\n    #drop {\n      height: 300px;\n      line-height: 300px;\n      width: 300px;\n      border-radius: 150px;\n      margin-left: auto;\n      margin-right: auto;\n      background-color: #f60;\n      color: #fff;\n      text-align: center;\n    }\n  </style>\n</head>\n<body>\n  <div class=\"aligner\">\n    <div id=\"drop\"><h2>Drop files here!</h2></div>\n    <div id=\"list\">\n      <h2>Uploaded Files:</h2>\n    </div>\n  </div>\n\n  <script type=\"text/javascript\">\n    let drop = document.getElementById('drop');\n    let list = document.getElementById('list');\n    let basePath = '{{base_path}}'; // filled in by the lambda before serving this page\n    let uploadBaseUrl = basePath + 'api/file/';\n\n    // Do nothing on drag events\n    function cancel(e) {\n      e.preventDefault();\n      return false;\n    }\n\n    // Upload on drop events\n    function handleDrop(e) {\n      e.preventDefault();\n      let dt    = e.dataTransfer;\n      let files = dt.files;\n      for (let i = 0; i<files.length; i++) {\n        let file = files[i];\n        let reader = new FileReader();\n        reader.addEventListener('loadend', function(e){\n          fetch(uploadBaseUrl + file.name, {\n            method: \"POST\",\n            body: new Blob([reader.result], {type: file.type})\n          })\n          .then((response) => {\n            if (response.ok) {\n              let uploadedFileNode = document.createElement('div');\n              uploadedFileNode.innerHTML = file.name;\n              list.appendChild(uploadedFileNode);\n            } else {\n              alert('Error uploading [' + file.name + ']. Max upload size is ~4MB.');\n            }\n          });\n        });\n        reader.readAsArrayBuffer(file);\n      }\n      return false;\n    }\n\n    // Listen to events\n    drop.addEventListener('dragenter', cancel);\n    drop.addEventListener('dragover', cancel);\n    drop.addEventListener('drop', handleDrop);\n  </script>\n</body>\n<!-- Based on https://www.netlify.com/blog/2016/11/17/serverless-file-uploads/ -->\n</html>\n"
  },
  {
    "path": "uploader/rollup.config.js",
    "content": "import rollupConfig from \"serverless-galleria-util/rollup.config.js\";\nexport default rollupConfig;"
  },
  {
    "path": "uploader/src/index.js",
    "content": "import { readFile } from \"fs/promises\";\nimport { lookup } from \"mime-types\";\nimport { join, resolve } from \"path\";\nimport { done, put } from \"serverless-galleria-util\";\n\nconst DEST_BUCKET = process.env.DEST_BUCKET;\n\nexport async function handler(event) {\n  // Fail on mising config\n  if (!DEST_BUCKET) {\n    console.error(\"Error: Environment variable DEST_BUCKET missing\");\n    return done(500, '{\"message\":\"Internal Server Error\"}');\n  }\n\n  if (event.path.startsWith(\"/api/file/\")) {\n    return fileRoute(event);\n  } else {\n    return servePublic(event);\n  }\n}\n\nasync function fileRoute(event) {\n  console.log(\"Serving fileRoute\");\n  if (event.httpMethod === \"POST\") {\n    let key = event.path.replace(\"/api/file/\", \"\");\n\n    // Get the body data\n    let body = event.body;\n    if (event.isBase64Encoded) {\n      console.log(\"body is base-64 encoded\");\n      body = Buffer.from(event.body, \"base64\");\n    }\n\n    try {\n      await put(DEST_BUCKET, key, body);\n\n      let message = \"Saved \" + DEST_BUCKET + \":\" + key;\n      console.log(message);\n      return done(200, JSON.stringify({ message }));\n    } catch (error) {\n      console.error(error);\n      return done(500, '{\"message\":\"error saving\"}');\n    }\n  } else {\n    return done(400, '{\"message\":\"Invalid HTTP Method\"}');\n  }\n}\n\nasync function servePublic(event) {\n  console.log(`Serving public for ${event.path}`);\n  // Set urlPath\n  let urlPath;\n  if (event.path === \"/\") {\n    return serveIndex(event);\n  } else {\n    urlPath = event.path;\n  }\n\n  // Determine the file's path on lambda's filesystem\n  const publicPath = join(process.env.LAMBDA_TASK_ROOT, \"public\");\n  const filePath = resolve(join(publicPath, urlPath));\n  const mimeType = lookup(filePath);\n\n  // Make sure the user doesn't try to break out of the public directory\n  if (!filePath.startsWith(publicPath)) {\n    console.log(\"forbidden\", filePath, publicPath);\n    return done(403, '{\"message\":\"Forbidden\"}', \"application/json\");\n  }\n\n  // Attempt to read the file, give a 404 on error\n  try {\n    const data = await readFile(filePath);\n    if (\n      mimeType === \"image/png\" ||\n      mimeType === \"image/jpeg\" ||\n      mimeType === \"image/x-icon\"\n    ) {\n      // Base 64 encode binary images\n      return done(200, data.toString(\"base64\"), mimeType, true);\n    } else {\n      return done(200, data.toString(), mimeType);\n    }\n  } catch (e) {\n    console.error(\"404\", e);\n    return done(404, '{\"message\":\"Not Found\"}');\n  }\n}\n\n// Serve the index page\nasync function serveIndex(event) {\n  console.log(\"Serving index\");\n  // Determine base path on whether the API Gateway stage is in the path or not\n  let base_path = \"/\";\n  if (event.requestContext.path.startsWith(\"/\" + event.requestContext.stage)) {\n    base_path = \"/\" + event.requestContext.stage + \"/\";\n  }\n\n  let filePath = join(process.env.LAMBDA_TASK_ROOT, \"public/index.html\");\n  // Read the file, fill in base_path and serve, or 404 on error\n  try {\n    const data = await readFile(filePath);\n    let content = data.toString().replace(/{{base_path}}/g, base_path);\n    return done(200, content, \"text/html\");\n  } catch (error) {\n    console.error(\"404\", error);\n    return done(404, '{\"message\":\"Not Found\"}');\n  }\n}\n"
  },
  {
    "path": "uploader/template.yml",
    "content": "AWSTemplateFormatVersion: 2010-09-09\nTransform: AWS::Serverless-2016-10-31\nDescription: Serverless web application for uploading files to S3\n\nGlobals:\n  Api:\n    BinaryMediaTypes:\n      - '*~1*'\n\nResources:\n  uploader:\n    Type: AWS::Serverless::Function\n    Properties:\n      Description: Serverless web application for uploading files to S3\n      Handler: src/index.handler\n      Runtime: nodejs18.x\n      CodeUri: package.zip\n      MemorySize: 1536\n      Policies:\n        - S3WritePolicy:\n            BucketName:\n              Ref: destBucket\n      Timeout: 60\n      Events:\n        root:\n          Type: Api\n          Properties:\n            Path: /\n            Method: get\n        getProxy:\n          Type: Api\n          Properties:\n            Path: '/{proxy+}'\n            Method: get\n        postProxy:\n          Type: Api\n          Properties:\n            Path: '/{proxy+}'\n            Method: post\n      Environment:\n        Variables:\n          DEST_BUCKET:\n            Ref: destBucket\n\nParameters:\n  destBucket:\n    Type: String\n    Description: Name of the S3 Bucket to put uploaded files into (must exist prior to deployment)\n"
  }
]