diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..43c6a7fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,248 @@ + +# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### OSX ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### PyCharm ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### PyCharm Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Build folder + +*/build/* + +.aws-sam/ + +packaged.yaml + +# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode diff --git a/README.md b/README.md new file mode 100644 index 00000000..6ee10958 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# Thumbnail Creator + +This project contains source code and supporting files for a serverless application - Thumbnail Creator. This application creates a thumbnail for image, pdf, and video files once uploaded into an S3 bucket. See [Usage](##usage) section. + +The application uses several AWS services including a Lambda function and S3 buckets with three custom Lambda Layers. These resources are defined in the `template.yaml` file in this project. You can update the template, e.g., change parameters' `Default` value and update your application. + +## Architecture +![Overview](images/overview.png "Overview") +1. Upload image, video, or pdf file to the S3 upload bucket +2. Thumbnail files will be created and stored in the S3 result bucket + + +## Lambda Function +* [app.py](apps/app.py): Create a thumbnail for the image, pdf, and video file that uploaded to an S3 bucket +* [sharedutils](sharedutils/): a custom Lambda Layer [filetype](https://github.com/h2non/filetype.py) - Python package to infer binary file types + +Note: this application use below three custom Lambda Layers and you must deploy these Lambda Layers before deploy this application. + 1. [ghostscript]( https://serverlessrepo.aws.amazon.com/applications/us-east-1/154387959412/ghostscript-lambda-layer ) + 2. [imagemagic]( https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:145266761615:applications~image-magick-lambda-layer ) + 3. [ffmpeg](https://serverlessrepo.aws.amazon.com/applications/us-east-1/145266761615/ffmpeg-lambda-layer) + +### Deploy Thumbnail Creator application using CloudFormation stack +#### Step 1: Launch CloudFormation stack +[![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?&templateURL=https://yc-deploy.s3.amazonaws.com/69a73d8311ce2a47f0edf3a45868f8c0.template) + +Click *Next* to continue + +#### Step 2: Specify stack details + +| Name | Description | Example value | +|:--- |:------------|:------------| +| Stack name | any valid stack name | filethumbnail | +| ImageMagickLayer | ImageMagick Lambda Layer ARN | arn:aws:lambda:us-east-1::layer:image-magick:1 | +| GhostscriptLayer | Ghostscript Lambda Layer ARN | arn:aws:lambda:us-east-1::layer:image-magick:1 | +| FfmpegLayer | Ffmpeg Lambda Layer ARN | arn:aws:lambda:us-east-1::layer:image-magick:1 | +| ConversionFileType | a valid file type | jpg | +| ConversionMimeType | a valid mime type | image/jpeg | +| ThumbnailWidth | a valid number | 150 | + +#### Step 3: Configure stack options +Leave it as is and click **Next** + +#### Step 4: Review +Make sure all checkboxes under Capabilities section are **CHECKED** + +Click *Create stack* + +### Deploy Thumbnail Creator application using SAM CLI + +To use the SAM CLI, you need the following tools. + +* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +* [Python 3 installed](https://www.python.org/downloads/) +* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) + +To build and deploy your application for the first time, run the following in your shell: + +```bash +sam build --use-container +``` + +Above command will build the source of the application. The SAM CLI installs dependencies defined in `requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. + +To package the application, run the following in your shell: +```bash +sam package --output-template-file packaged.yaml --s3-bucket BUCKETNAME --region us-east-1 +``` +Above command will package the application and upload it to the S3 bucket you specified. + +Run the following in your shell to deploy the application to AWS: +```bash +sam deploy --template-file packaged.yaml --stack-name STACKNAME --s3-bucket BUCKETNAME \ + --parameter-overrides 'ImageMagickLayer=layerARN GhostscriptLayer=layerARN FfmpegLayer=layerARN ThumbnailWidth=150' \ + --capabilities CAPABILITY_IAM --region us-east-1 +``` + +You should see the cloudformation resource like below +![Resources](images/stack_output.png "Resources") + +## Usage +1. Upload files to the `UploadBucket` +2. Thumbnail files will be created and stored in the `ResultsBucket` + +## Cleanup +To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: + +```bash +aws cloudformation delete-stack --stack-name stackname +``` diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/app.py b/apps/app.py new file mode 100644 index 00000000..5fe49465 --- /dev/null +++ b/apps/app.py @@ -0,0 +1,84 @@ +import boto3 +import filetype +import json +import os +import re +import urllib.parse +import uuid + +s3_client = boto3.client('s3') + + +def lambda_handler(event, context): + + bucketName = event['Records'][0]['s3']['bucket']['name'] + InputFile = urllib.parse.unquote_plus( + event['Records'][0]['s3']['object']['key'], encoding='utf-8') + fileToThumbnail(InputFile, bucketName) + + +def fileToThumbnail(InputFileName, bucketName): + + print("Process file: " + InputFileName) + + ThumbnailFileName = os.path.splitext( + InputFileName)[0] + os.getenv('EXTENSION') + print("Output file: " + ThumbnailFileName) + + fileExtension = os.path.splitext(InputFileName)[1] + print("file extension: " + fileExtension) + + downloadFilePath = '/tmp/' + str(uuid.uuid4()) + fileExtension + s3_client.download_file(bucketName, InputFileName, downloadFilePath) + + fileType = filetype.guess(downloadFilePath).mime + print("file type: " + fileType) + + uploadFilePath = '/tmp/' + ThumbnailFileName + processed = True + + if "application/pdf" in fileType: + print("Process pdf file: " + downloadFilePath) + os.system( + "/opt/bin/convert -density " + + os.getenv('THUMB_WIDTH') + + " " + + downloadFilePath + + "[0] -quality 90 " + + uploadFilePath) + elif "image/" in fileType: + print("Process image file: " + downloadFilePath) + os.system( + "/opt/bin/convert " + + downloadFilePath + + " -quiet -resize " + + str(int( os.getenv('THUMB_WIDTH') ) * 2) + + "x " + + uploadFilePath) + elif "video/" in fileType: + print("Process video file: " + downloadFilePath) + os.system( + "/opt/bin/ffmpeg " + + "-loglevel error -y -i " + + downloadFilePath + + " -vf thumbnail,scale=" + + str(int( os.getenv('THUMB_WIDTH') ) * 2) + + ":-1 -frames:v 1 " + + uploadFilePath) + else: + processed = False + print("This file extension is not supported.") + + if processed: + s3_client.upload_file( + uploadFilePath, + os.getenv('OUTPUT_BUCKET'), + ThumbnailFileName) + clearTmpFiles(uploadFilePath, downloadFilePath) + else: + os.remove(downloadFilePath) + + +def clearTmpFiles(uploadFileName, downloadFileName): + os.remove(uploadFileName) + os.remove(downloadFileName) diff --git a/apps/requirements.txt b/apps/requirements.txt new file mode 100644 index 00000000..663bd1f6 --- /dev/null +++ b/apps/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/images/overview.png b/images/overview.png new file mode 100644 index 00000000..09914827 Binary files /dev/null and b/images/overview.png differ diff --git a/images/stack_output.png b/images/stack_output.png new file mode 100644 index 00000000..4b3f35f1 Binary files /dev/null and b/images/stack_output.png differ diff --git a/sharedutils/__init__.py b/sharedutils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/sharedutils/examples/__init__.py b/sharedutils/examples/__init__.py new file mode 100644 index 00000000..40a96afc --- /dev/null +++ b/sharedutils/examples/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/sharedutils/examples/buffer.py b/sharedutils/examples/buffer.py new file mode 100644 index 00000000..369ad26e --- /dev/null +++ b/sharedutils/examples/buffer.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +import filetype + + +def main(): + f = open('tests/fixtures/sample.jpg', 'rb') + data = f.read() + + kind = filetype.guess(data) + if kind is None: + print('Cannot guess file type!') + return + + print('File extension: %s' % kind.extension) + print('File MIME type: %s' % kind.mime) + + +if __name__ == '__main__': + main() diff --git a/sharedutils/examples/bytes.py b/sharedutils/examples/bytes.py new file mode 100644 index 00000000..e1b406c3 --- /dev/null +++ b/sharedutils/examples/bytes.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +import filetype + + +def main(): + buf = bytearray([0xFF, 0xD8, 0xFF, 0x00, 0x08]) + kind = filetype.guess(buf) + + if kind is None: + print('Cannot guess file type!') + return + + print('File extension: %s' % kind.extension) + print('File MIME type: %s' % kind.mime) + + +if __name__ == '__main__': + main() diff --git a/sharedutils/examples/file.py b/sharedutils/examples/file.py new file mode 100644 index 00000000..c9f46019 --- /dev/null +++ b/sharedutils/examples/file.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +import filetype + + +def main(): + kind = filetype.guess('tests/fixtures/sample.jpg') + if kind is None: + print('Cannot guess file type!') + return + + print('File extension: %s' % kind.extension) + print('File MIME type: %s' % kind.mime) + + +if __name__ == '__main__': + main() diff --git a/sharedutils/filetype-1.0.7.dist-info/INSTALLER b/sharedutils/filetype-1.0.7.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/sharedutils/filetype-1.0.7.dist-info/LICENSE b/sharedutils/filetype-1.0.7.dist-info/LICENSE new file mode 100644 index 00000000..d5dbb7e3 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Tomás Aparicio + +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/sharedutils/filetype-1.0.7.dist-info/METADATA b/sharedutils/filetype-1.0.7.dist-info/METADATA new file mode 100644 index 00000000..fa377842 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/METADATA @@ -0,0 +1,182 @@ +Metadata-Version: 2.1 +Name: filetype +Version: 1.0.7 +Summary: Infer file type and MIME type of any file/buffer. No external dependencies. +Home-page: https://github.com/h2non/filetype.py +Author: Tomas Aparicio +Author-email: tomas@aparicio.me +License: MIT +Download-URL: https://github.com/h2non/filetype.py/tarball/master +Keywords: file libmagic magic infer numbers magicnumbers discovery mime type kind +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: System +Classifier: Topic :: System :: Filesystems +Classifier: Topic :: Utilities + +filetype.py |Build Status| |PyPI| |Pyversions| |API| +==================================================== + +Small and dependency free `Python`_ package to infer file type and MIME +type checking the `magic numbers`_ signature of a file or buffer. + +This is a Python port from `filetype`_ Go package. + +Features +-------- + +- Simple and friendly API +- Supports a `wide range`_ of file types +- Provides file extension and MIME type inference +- File discovery by extension or MIME type +- File discovery by kind (image, video, audio…) +- `Pluggable`_: add new custom type matchers +- `Fast`_, even processing large files +- Only first 261 bytes representing the max file header is required, so + you can just `pass a list of bytes`_ +- Dependency free (just Python code, no C extensions, no libmagic + bindings) +- Cross-platform file recognition + +Installation +------------ + +:: + + pip install filetype + +API +--- + +See `annotated API reference`_. + +Examples +-------- + +Simple file type checking +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import filetype + + def main(): + kind = filetype.guess('tests/fixtures/sample.jpg') + if kind is None: + print('Cannot guess file type!') + return + + print('File extension: %s' % kind.extension) + print('File MIME type: %s' % kind.mime) + + if __name__ == '__main__': + main() + +Supported types +--------------- + +Image +^^^^^ + +- **jpg** - ``image/jpeg`` +- **jpx** - ``image/jpx`` +- **png** - ``image/png`` +- **gif** - ``image/gif`` +- **webp** - ``image/webp`` +- **cr2** - ``image/x-canon-cr2`` +- **tif** - ``image/tiff`` +- **bmp** - ``image/bmp`` +- **jxr** - ``image/vnd.ms-photo`` +- **psd** - ``image/vnd.adobe.photoshop`` +- **ico** - ``image/x-icon`` +- **heic** - ``image/heic`` + +Video +^^^^^ + +- **mp4** - ``video/mp4`` +- **m4v** - ``video/x-m4v`` +- **mkv** - ``video/x-matroska`` +- **webm** - ``video/webm`` +- **mov** - ``video/quicktime`` +- **avi** - ``video/x-msvideo`` +- **wmv** - ``video/x-ms-wmv`` +- **mpg** - ``video/mpeg`` +- **flv** - ``video/x-flv`` + +Audio +^^^^^ + +- **mid** - ``audio/midi`` +- **mp3** - ``audio/mpeg`` +- **m4a** - ``audio/m4a`` +- **ogg** - ``audio/ogg`` +- **flac** - ``audio/x-flac`` +- **wav** - ``audio/x-wav`` +- **amr** - ``audio/amr`` + +Archive +^^^^^^^ + +- **epub** - ``application/epub+zip`` +- **zip** - ``application/zip`` +- **tar** - ``application/x-tar`` +- **rar** - ``application/x-rar-compressed`` +- **gz** - ``application/gzip`` +- **bz2** - ``application/x-bzip2`` +- **7z** - ``application/x-7z-compressed`` +- **xz** - ``application/x-xz`` +- **pdf** - ``application/pdf`` +- **exe** - ``application/x-msdownload`` +- **swf** - ``application/x-shockwave-flash`` + +- **rtf** - ``application/rtf`` +- **eot** - ``application/octet-stream`` +- **ps** - ``application/postscript`` +- **sqlite** - ``application/x-sqlite3`` +- **nes** - ``application/x-nintendo-nes-rom`` +- **crx** - ``application/x-google-chrome-extension`` +- **cab** - ``application/vnd.ms-cab-compressed`` +- **deb** - ``application/x-deb`` +- **ar** - ``application/x-unix-archive`` +- **Z** - ``application/x-compress`` +- **lz** - ``application/x-lzip`` + +Font +^^^^ + +- **woff** - ``application/font-woff`` +- **woff2** - ``application/font-woff`` +- **ttf** - ``application/font-sfnt`` +- **otf** - ``application/font-sfnt`` + +.. _Python: http://python.org +.. _magic numbers: https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files +.. _filetype: https://github.com/h2non/filetype +.. _wide range: #supported-types +.. _Pluggable: #add-additional-file-type-matchers +.. _Fast: #benchmarks +.. _pass a list of bytes: #file-header +.. _annotated API reference: https://h2non.github.io/filetype.py/ + +.. |Build Status| image:: https://travis-ci.org/h2non/filetype.py.svg?branch=master + :target: https://travis-ci.org/h2non/filetype.py +.. |PyPI| image:: https://img.shields.io/pypi/v/filetype.svg?maxAge=2592000?style=flat-square + :target: https://pypi.python.org/pypi/filetype +.. |Pyversions| image:: https://img.shields.io/pypi/pyversions/filetype.svg?style=flat-square + :target: https://pypi.python.org/pypi/filetype +.. |API| image:: https://img.shields.io/badge/api-docs-green.svg + :target: https://h2non.github.io/filetype.py + + diff --git a/sharedutils/filetype-1.0.7.dist-info/RECORD b/sharedutils/filetype-1.0.7.dist-info/RECORD new file mode 100644 index 00000000..3325a6f9 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/RECORD @@ -0,0 +1,41 @@ +examples/__init__.py,sha256=iwhKnzeBJLKxpRVjvzwiRE63_zNpIBfaKLITauVph-0,24 +examples/__pycache__/__init__.cpython-38.pyc,, +examples/__pycache__/buffer.cpython-38.pyc,, +examples/__pycache__/bytes.cpython-38.pyc,, +examples/__pycache__/file.cpython-38.pyc,, +examples/buffer.py,sha256=OmvRDe0QMfckVZUqdwH4NTn6HlIIL4jH5VyHnUSiENw,429 +examples/bytes.py,sha256=oy509xeqHvPDgr8ZdhPg_CU01cfnyvtpk5-iNNQ_5FQ,412 +examples/file.py,sha256=bmQBW-0nOCD2PYOBLrHmGpgkpetMPEyZQC1PF1uaoQ0,383 +filetype-1.0.7.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +filetype-1.0.7.dist-info/LICENSE,sha256=jkTiqjWzcb3MhWvPDSRCpBDdVf3maw38L83wdtl5Rqw,1082 +filetype-1.0.7.dist-info/METADATA,sha256=Cjt61ttejyl_fKoco2tmIu7WUYlA1pmpZPRIwUOVgYI,5387 +filetype-1.0.7.dist-info/RECORD,, +filetype-1.0.7.dist-info/WHEEL,sha256=8zNYZbwQSXoB9IfXOjPfeNwvAsALAjffgk27FqvCWbo,110 +filetype-1.0.7.dist-info/top_level.txt,sha256=jZV7PA6PoZ2Vvkc5q0q1-tx4rneVdp6AwM82jQe4Akc,18 +filetype-1.0.7.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 +filetype/__init__.py,sha256=s6mwvxqSfEQMm_DTXBtv0-XmcNHsEBR4pvUOu5cA618,223 +filetype/__pycache__/__init__.cpython-38.pyc,, +filetype/__pycache__/filetype.cpython-38.pyc,, +filetype/__pycache__/helpers.cpython-38.pyc,, +filetype/__pycache__/match.cpython-38.pyc,, +filetype/__pycache__/utils.cpython-38.pyc,, +filetype/filetype.py,sha256=SBYUBugfBQSO9z7zyWaXOak6UpLUlmZZ--5FpN0fybM,2122 +filetype/helpers.py,sha256=colb2oYpbup8nu7uSofRYVO-mTgKKbUDwHP6ofq_gJQ,2596 +filetype/match.py,sha256=_EVyaOqL86S6DwQeaWus4wsiShdk9Hs2H8381-xQc5k,2479 +filetype/types/__init__.py,sha256=eGrhhDtyFv81xq24HdHeRnHwDi1XSJQLQgfa6eq57TI,1448 +filetype/types/__pycache__/__init__.cpython-38.pyc,, +filetype/types/__pycache__/archive.cpython-38.pyc,, +filetype/types/__pycache__/audio.cpython-38.pyc,, +filetype/types/__pycache__/base.cpython-38.pyc,, +filetype/types/__pycache__/font.cpython-38.pyc,, +filetype/types/__pycache__/image.cpython-38.pyc,, +filetype/types/__pycache__/isobmff.cpython-38.pyc,, +filetype/types/__pycache__/video.cpython-38.pyc,, +filetype/types/archive.py,sha256=MGgsT3XzuTU22SvHxhim-7roLdM-mRfNYU75RtKIT9E,12601 +filetype/types/audio.py,sha256=OKwnKKHqGqs0NfHevRZL_Th23rRO7C6YJMoBHpiNYj4,3832 +filetype/types/base.py,sha256=dX0TtkuHidfE6cIoGQW_bb_pxscc4vR7tM_DAUZX9g4,675 +filetype/types/font.py,sha256=89gtQcZv0pxB6BXK_Osdkuj_S69Q7RjcJWeyQ1Z9EcE,2312 +filetype/types/image.py,sha256=GVyfSOsQd_uMCAOTdZYRRQcyQRxHee8fVHj7ymSp-HA,6549 +filetype/types/isobmff.py,sha256=Fpglfy3k69nUPDjfk4G9J_lgRxzLQFMyN37vAuPIT9s,928 +filetype/types/video.py,sha256=qxFSclZArelJR7N68N7Lq3pJ8KI0PPAzc_MkPMLBNhc,5362 +filetype/utils.py,sha256=-QqcAtAbR4UaAa-0CeVGMYPhitPzZrrJ-qSZCN4TBrg,1670 diff --git a/sharedutils/filetype-1.0.7.dist-info/WHEEL b/sharedutils/filetype-1.0.7.dist-info/WHEEL new file mode 100644 index 00000000..8b701e93 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.33.6) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/sharedutils/filetype-1.0.7.dist-info/top_level.txt b/sharedutils/filetype-1.0.7.dist-info/top_level.txt new file mode 100644 index 00000000..e857439b --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/top_level.txt @@ -0,0 +1,2 @@ +examples +filetype diff --git a/sharedutils/filetype-1.0.7.dist-info/zip-safe b/sharedutils/filetype-1.0.7.dist-info/zip-safe new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/sharedutils/filetype-1.0.7.dist-info/zip-safe @@ -0,0 +1 @@ + diff --git a/sharedutils/filetype/__init__.py b/sharedutils/filetype/__init__.py new file mode 100644 index 00000000..39d9a4f8 --- /dev/null +++ b/sharedutils/filetype/__init__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .filetype import * # noqa +from .helpers import * # noqa +from .match import * # noqa + +# Current package semver version +__version__ = version = '1.0.7' diff --git a/sharedutils/filetype/filetype.py b/sharedutils/filetype/filetype.py new file mode 100644 index 00000000..457152ef --- /dev/null +++ b/sharedutils/filetype/filetype.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .match import match +from .types import TYPES, Type + +# Expose supported matchers types +types = TYPES + + +def guess(obj): + """ + Infers the type of the given input. + + Function is overloaded to accept multiple types in input + and peform the needed type inference based on it. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + The matched type instance. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj) if obj else None + + +def guess_mime(obj): + """ + Infers the file type of the given input + and returns its MIME type. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + The matched MIME type as string. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + kind = guess(obj) + return kind.mime if kind else kind + + +def guess_extension(obj): + """ + Infers the file type of the given input + and returns its RFC file extension. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + The matched file extension as string. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + kind = guess(obj) + return kind.extension if kind else kind + + +def get_type(mime=None, ext=None): + """ + Returns the file type instance searching by + MIME type or file extension. + + Args: + ext: file extension string. E.g: jpg, png, mp4, mp3 + mime: MIME string. E.g: image/jpeg, video/mpeg + + Returns: + The matched file type instance. Otherwise None. + """ + for kind in types: + if kind.extension == ext or kind.mime == mime: + return kind + return None + + +def add_type(instance): + """ + Adds a new type matcher instance to the supported types. + + Args: + instance: Type inherited instance. + + Returns: + None + """ + if not isinstance(instance, Type): + raise TypeError('instance must inherit from filetype.types.Type') + + types.insert(0, instance) diff --git a/sharedutils/filetype/helpers.py b/sharedutils/filetype/helpers.py new file mode 100644 index 00000000..03de2322 --- /dev/null +++ b/sharedutils/filetype/helpers.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from .types import TYPES +from .match import ( + image_match, font_match, + video_match, audio_match, archive_match +) + + +def is_extension_supported(ext): + """ + Checks if the given extension string is + one of the supported by the file matchers. + + Args: + ext (str): file extension string. E.g: jpg, png, mp4, mp3 + + Returns: + True if the file extension is supported. + Otherwise False. + """ + for kind in TYPES: + if kind.extension is ext: + return True + return False + + +def is_mime_supported(mime): + """ + Checks if the given MIME type string is + one of the supported by the file matchers. + + Args: + mime (str): MIME string. E.g: image/jpeg, video/mpeg + + Returns: + True if the MIME type is supported. + Otherwise False. + """ + for kind in TYPES: + if kind.mime is mime: + return True + return False + + +def is_image(obj): + """ + Checks if a given input is a supported type image. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + True if obj is a valid image. Otherwise False. + + Raises: + TypeError: if obj is not a supported type. + """ + return image_match(obj) is not None + + +def is_archive(obj): + """ + Checks if a given input is a supported type archive. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + True if obj is a valid archive. Otherwise False. + + Raises: + TypeError: if obj is not a supported type. + """ + return archive_match(obj) is not None + + +def is_audio(obj): + """ + Checks if a given input is a supported type audio. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + True if obj is a valid audio. Otherwise False. + + Raises: + TypeError: if obj is not a supported type. + """ + return audio_match(obj) is not None + + +def is_video(obj): + """ + Checks if a given input is a supported type video. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + True if obj is a valid video. Otherwise False. + + Raises: + TypeError: if obj is not a supported type. + """ + return video_match(obj) is not None + + +def is_font(obj): + """ + Checks if a given input is a supported type font. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + True if obj is a valid font. Otherwise False. + + Raises: + TypeError: if obj is not a supported type. + """ + return font_match(obj) is not None diff --git a/sharedutils/filetype/match.py b/sharedutils/filetype/match.py new file mode 100644 index 00000000..fe820d96 --- /dev/null +++ b/sharedutils/filetype/match.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .types import ARCHIVE as archive_matchers +from .types import AUDIO as audio_matchers +from .types import FONT as font_matchers +from .types import IMAGE as image_matchers +from .types import VIDEO as video_matchers +from .types import TYPES +from .utils import get_bytes + + +def match(obj, matchers=TYPES): + """ + Matches the given input againts the available + file type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if type matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + buf = get_bytes(obj) + + for matcher in matchers: + if matcher.match(buf): + return matcher + + return None + + +def image_match(obj): + """ + Matches the given input againts the available + image type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj, image_matchers) + + +def font_match(obj): + """ + Matches the given input againts the available + font type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj, font_matchers) + + +def video_match(obj): + """ + Matches the given input againts the available + video type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj, video_matchers) + + +def audio_match(obj): + """ + Matches the given input againts the available + autio type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj, audio_matchers) + + +def archive_match(obj): + """ + Matches the given input againts the available + archive type matchers. + + Args: + obj: path to file, bytes or bytearray. + + Returns: + Type instance if matches. Otherwise None. + + Raises: + TypeError: if obj is not a supported type. + """ + return match(obj, archive_matchers) diff --git a/sharedutils/filetype/types/__init__.py b/sharedutils/filetype/types/__init__.py new file mode 100644 index 00000000..87d95b84 --- /dev/null +++ b/sharedutils/filetype/types/__init__.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from . import archive +from . import audio +from . import font +from . import image +from . import video +from .base import Type # noqa + +# Supported image types +IMAGE = ( + image.Jpeg(), + image.Jpx(), + image.Png(), + image.Gif(), + image.Webp(), + image.Cr2(), + image.Tiff(), + image.Bmp(), + image.Jxr(), + image.Psd(), + image.Ico(), + image.Heic(), + image.Dcm(), +) + +# Supported video types +VIDEO = ( + video.Mp4(), + video.M4v(), + video.Mkv(), + video.Mov(), + video.Avi(), + video.Wmv(), + video.Mpeg(), + video.Webm(), + video.Flv(), +) + +# Supported audio types +AUDIO = ( + audio.Midi(), + audio.Mp3(), + audio.M4a(), + audio.Ogg(), + audio.Flac(), + audio.Wav(), + audio.Amr(), +) + +# Supported font types +FONT = (font.Woff(), font.Woff2(), font.Ttf(), font.Otf()) + +# Supported archive container types +ARCHIVE = ( + archive.Epub(), + archive.Zip(), + archive.Tar(), + archive.Rar(), + archive.Gz(), + archive.Bz2(), + archive.SevenZ(), + archive.Pdf(), + archive.Exe(), + archive.Swf(), + archive.Rtf(), + archive.Nes(), + archive.Crx(), + archive.Cab(), + archive.Eot(), + archive.Ps(), + archive.Xz(), + archive.Sqlite(), + archive.Deb(), + archive.Ar(), + archive.Z(), + archive.Lz(), +) + +# Expose supported type matchers +TYPES = list(VIDEO + IMAGE + AUDIO + FONT + ARCHIVE) diff --git a/sharedutils/filetype/types/archive.py b/sharedutils/filetype/types/archive.py new file mode 100644 index 00000000..5cde4b3d --- /dev/null +++ b/sharedutils/filetype/types/archive.py @@ -0,0 +1,515 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .base import Type + + +class Epub(Type): + """ + Implements the EPUB archive type matcher. + """ + MIME = 'application/epub+zip' + EXTENSION = 'epub' + + def __init__(self): + super(Epub, self).__init__( + mime=Epub.MIME, + extension=Epub.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 57 and + buf[0] == 0x50 and buf[1] == 0x4B and + buf[2] == 0x3 and buf[3] == 0x4 and + buf[30] == 0x6D and buf[31] == 0x69 and + buf[32] == 0x6D and buf[33] == 0x65 and + buf[34] == 0x74 and buf[35] == 0x79 and + buf[36] == 0x70 and buf[37] == 0x65 and + buf[38] == 0x61 and buf[39] == 0x70 and + buf[40] == 0x70 and buf[41] == 0x6C and + buf[42] == 0x69 and buf[43] == 0x63 and + buf[44] == 0x61 and buf[45] == 0x74 and + buf[46] == 0x69 and buf[47] == 0x6F and + buf[48] == 0x6E and buf[49] == 0x2F and + buf[50] == 0x65 and buf[51] == 0x70 and + buf[52] == 0x75 and buf[53] == 0x62 and + buf[54] == 0x2B and buf[55] == 0x7A and + buf[56] == 0x69 and buf[57] == 0x70) + + +class Zip(Type): + """ + Implements the Zip archive type matcher. + """ + MIME = 'application/zip' + EXTENSION = 'zip' + + def __init__(self): + super(Zip, self).__init__( + mime=Zip.MIME, + extension=Zip.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x50 and buf[1] == 0x4B and + (buf[2] == 0x3 or buf[2] == 0x5 or + buf[2] == 0x7) and + (buf[3] == 0x4 or buf[3] == 0x6 or + buf[3] == 0x8)) + + +class Tar(Type): + """ + Implements the Tar archive type matcher. + """ + MIME = 'application/x-tar' + EXTENSION = 'tar' + + def __init__(self): + super(Tar, self).__init__( + mime=Tar.MIME, + extension=Tar.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 261 and + buf[257] == 0x75 and + buf[258] == 0x73 and + buf[259] == 0x74 and + buf[260] == 0x61 and + buf[261] == 0x72) + + +class Rar(Type): + """ + Implements the RAR archive type matcher. + """ + MIME = 'application/x-rar-compressed' + EXTENSION = 'rar' + + def __init__(self): + super(Rar, self).__init__( + mime=Rar.MIME, + extension=Rar.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 6 and + buf[0] == 0x52 and + buf[1] == 0x61 and + buf[2] == 0x72 and + buf[3] == 0x21 and + buf[4] == 0x1A and + buf[5] == 0x7 and + (buf[6] == 0x0 or + buf[6] == 0x1)) + + +class Gz(Type): + """ + Implements the GZ archive type matcher. + """ + MIME = 'application/gzip' + EXTENSION = 'gz' + + def __init__(self): + super(Gz, self).__init__( + mime=Gz.MIME, + extension=Gz.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 2 and + buf[0] == 0x1F and + buf[1] == 0x8B and + buf[2] == 0x8) + + +class Bz2(Type): + """ + Implements the BZ2 archive type matcher. + """ + MIME = 'application/x-bzip2' + EXTENSION = 'bz2' + + def __init__(self): + super(Bz2, self).__init__( + mime=Bz2.MIME, + extension=Bz2.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 2 and + buf[0] == 0x42 and + buf[1] == 0x5A and + buf[2] == 0x68) + + +class SevenZ(Type): + """ + Implements the SevenZ (7z) archive type matcher. + """ + MIME = 'application/x-7z-compressed' + EXTENSION = '7z' + + def __init__(self): + super(SevenZ, self).__init__( + mime=SevenZ.MIME, + extension=SevenZ.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 5 and + buf[0] == 0x37 and + buf[1] == 0x7A and + buf[2] == 0xBC and + buf[3] == 0xAF and + buf[4] == 0x27 and + buf[5] == 0x1C) + + +class Pdf(Type): + """ + Implements the PDF archive type matcher. + """ + MIME = 'application/pdf' + EXTENSION = 'pdf' + + def __init__(self): + super(Pdf, self).__init__( + mime=Pdf.MIME, + extension=Pdf.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x25 and + buf[1] == 0x50 and + buf[2] == 0x44 and + buf[3] == 0x46) + + +class Exe(Type): + """ + Implements the EXE archive type matcher. + """ + MIME = 'application/x-msdownload' + EXTENSION = 'exe' + + def __init__(self): + super(Exe, self).__init__( + mime=Exe.MIME, + extension=Exe.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 1 and + buf[0] == 0x4D and + buf[1] == 0x5A) + + +class Swf(Type): + """ + Implements the SWF archive type matcher. + """ + MIME = 'application/x-shockwave-flash' + EXTENSION = 'swf' + + def __init__(self): + super(Swf, self).__init__( + mime=Swf.MIME, + extension=Swf.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 2 and + (buf[0] == 0x43 or + buf[0] == 0x46) and + buf[1] == 0x57 and + buf[2] == 0x53) + + +class Rtf(Type): + """ + Implements the RTF archive type matcher. + """ + MIME = 'application/rtf' + EXTENSION = 'rtf' + + def __init__(self): + super(Rtf, self).__init__( + mime=Rtf.MIME, + extension=Rtf.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 4 and + buf[0] == 0x7B and + buf[1] == 0x5C and + buf[2] == 0x72 and + buf[3] == 0x74 and + buf[4] == 0x66) + + +class Nes(Type): + """ + Implements the NES archive type matcher. + """ + MIME = 'application/x-nintendo-nes-rom' + EXTENSION = 'nes' + + def __init__(self): + super(Nes, self).__init__( + mime=Nes.MIME, + extension=Nes.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x4E and + buf[1] == 0x45 and + buf[2] == 0x53 and + buf[3] == 0x1A) + + +class Crx(Type): + """ + Implements the CRX archive type matcher. + """ + MIME = 'application/x-google-chrome-extension' + EXTENSION = 'crx' + + def __init__(self): + super(Crx, self).__init__( + mime=Crx.MIME, + extension=Crx.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x43 and + buf[1] == 0x72 and + buf[2] == 0x32 and + buf[3] == 0x34) + + +class Cab(Type): + """ + Implements the CAB archive type matcher. + """ + MIME = 'application/vnd.ms-cab-compressed' + EXTENSION = 'cab' + + def __init__(self): + super(Cab, self).__init__( + mime=Cab.MIME, + extension=Cab.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + ((buf[0] == 0x4D and + buf[1] == 0x53 and + buf[2] == 0x43 and + buf[3] == 0x46) or + (buf[0] == 0x49 and + buf[1] == 0x53 and + buf[2] == 0x63 and + buf[3] == 0x28))) + + +class Eot(Type): + """ + Implements the EOT archive type matcher. + """ + MIME = 'application/octet-stream' + EXTENSION = 'eot' + + def __init__(self): + super(Eot, self).__init__( + mime=Eot.MIME, + extension=Eot.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 35 and + buf[34] == 0x4C and + buf[35] == 0x50 and + ((buf[8] == 0x02 and + buf[9] == 0x00 and + buf[10] == 0x01) or + (buf[8] == 0x01 and + buf[9] == 0x00 and + buf[10] == 0x00) or + (buf[8] == 0x02 and + buf[9] == 0x00 and + buf[10] == 0x02))) + + +class Ps(Type): + """ + Implements the PS archive type matcher. + """ + MIME = 'application/postscript' + EXTENSION = 'ps' + + def __init__(self): + super(Ps, self).__init__( + mime=Ps.MIME, + extension=Ps.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 1 and + buf[0] == 0x25 and + buf[1] == 0x21) + + +class Xz(Type): + """ + Implements the XS archive type matcher. + """ + MIME = 'application/x-xz' + EXTENSION = 'xz' + + def __init__(self): + super(Xz, self).__init__( + mime=Xz.MIME, + extension=Xz.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 5 and + buf[0] == 0xFD and + buf[1] == 0x37 and + buf[2] == 0x7A and + buf[3] == 0x58 and + buf[4] == 0x5A and + buf[5] == 0x00) + + +class Sqlite(Type): + """ + Implements the Sqlite DB archive type matcher. + """ + MIME = 'application/x-sqlite3' + EXTENSION = 'sqlite' + + def __init__(self): + super(Sqlite, self).__init__( + mime=Sqlite.MIME, + extension=Sqlite.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x53 and + buf[1] == 0x51 and + buf[2] == 0x4C and + buf[3] == 0x69) + + +class Deb(Type): + """ + Implements the DEB archive type matcher. + """ + MIME = 'application/x-deb' + EXTENSION = 'deb' + + def __init__(self): + super(Deb, self).__init__( + mime=Deb.MIME, + extension=Deb.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 20 and + buf[0] == 0x21 and + buf[1] == 0x3C and + buf[2] == 0x61 and + buf[3] == 0x72 and + buf[4] == 0x63 and + buf[5] == 0x68 and + buf[6] == 0x3E and + buf[7] == 0x0A and + buf[8] == 0x64 and + buf[9] == 0x65 and + buf[10] == 0x62 and + buf[11] == 0x69 and + buf[12] == 0x61 and + buf[13] == 0x6E and + buf[14] == 0x2D and + buf[15] == 0x62 and + buf[16] == 0x69 and + buf[17] == 0x6E and + buf[18] == 0x61 and + buf[19] == 0x72 and + buf[20] == 0x79) + + +class Ar(Type): + """ + Implements the AR archive type matcher. + """ + MIME = 'application/x-unix-archive' + EXTENSION = 'ar' + + def __init__(self): + super(Ar, self).__init__( + mime=Ar.MIME, + extension=Ar.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 6 and + buf[0] == 0x21 and + buf[1] == 0x3C and + buf[2] == 0x61 and + buf[3] == 0x72 and + buf[4] == 0x63 and + buf[5] == 0x68 and + buf[6] == 0x3E) + + +class Z(Type): + """ + Implements the Z archive type matcher. + """ + MIME = 'application/x-compress' + EXTENSION = 'Z' + + def __init__(self): + super(Z, self).__init__( + mime=Z.MIME, + extension=Z.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 1 and + ((buf[0] == 0x1F and + buf[1] == 0xA0) or + (buf[0] == 0x1F and + buf[1] == 0x9D))) + + +class Lz(Type): + """ + Implements the Lz archive type matcher. + """ + MIME = 'application/x-lzip' + EXTENSION = 'lz' + + def __init__(self): + super(Lz, self).__init__( + mime=Lz.MIME, + extension=Lz.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x4C and + buf[1] == 0x5A and + buf[2] == 0x49 and + buf[3] == 0x50) diff --git a/sharedutils/filetype/types/audio.py b/sharedutils/filetype/types/audio.py new file mode 100644 index 00000000..5dafba5c --- /dev/null +++ b/sharedutils/filetype/types/audio.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .base import Type + + +class Midi(Type): + """ + Implements the Midi audio type matcher. + """ + MIME = 'audio/midi' + EXTENSION = 'midi' + + def __init__(self): + super(Midi, self).__init__( + mime=Midi.MIME, + extension=Midi.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x4D and + buf[1] == 0x54 and + buf[2] == 0x68 and + buf[3] == 0x64) + + +class Mp3(Type): + """ + Implements the MP3 audio type matcher. + """ + MIME = 'audio/mpeg' + EXTENSION = 'mp3' + + def __init__(self): + super(Mp3, self).__init__( + mime=Mp3.MIME, + extension=Mp3.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 2 and + ((buf[0] == 0x49 and + buf[1] == 0x44 and + buf[2] == 0x33) or + (buf[0] == 0xFF and + buf[1] == 0xfb))) + + +class M4a(Type): + """ + Implements the M4A audio type matcher. + """ + MIME = 'audio/m4a' + EXTENSION = 'm4a' + + def __init__(self): + super(M4a, self).__init__( + mime=M4a.MIME, + extension=M4a.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 10 and + ((buf[4] == 0x66 and + buf[5] == 0x74 and + buf[6] == 0x79 and + buf[7] == 0x70 and + buf[8] == 0x4D and + buf[9] == 0x34 and + buf[10] == 0x41) or + (buf[0] == 0x4D and + buf[1] == 0x34 and + buf[2] == 0x41 and + buf[3] == 0x20))) + + +class Ogg(Type): + """ + Implements the OGG audio type matcher. + """ + MIME = 'audio/ogg' + EXTENSION = 'ogg' + + def __init__(self): + super(Ogg, self).__init__( + mime=Ogg.MIME, + extension=Ogg.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x4F and + buf[1] == 0x67 and + buf[2] == 0x67 and + buf[3] == 0x53) + + +class Flac(Type): + """ + Implements the FLAC audio type matcher. + """ + MIME = 'audio/x-flac' + EXTENSION = 'flac' + + def __init__(self): + super(Flac, self).__init__( + mime=Flac.MIME, + extension=Flac.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x66 and + buf[1] == 0x4C and + buf[2] == 0x61 and + buf[3] == 0x43) + + +class Wav(Type): + """ + Implements the WAV audio type matcher. + """ + MIME = 'audio/x-wav' + EXTENSION = 'wav' + + def __init__(self): + super(Wav, self).__init__( + mime=Wav.MIME, + extension=Wav.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 11 and + buf[0] == 0x52 and + buf[1] == 0x49 and + buf[2] == 0x46 and + buf[3] == 0x46 and + buf[8] == 0x57 and + buf[9] == 0x41 and + buf[10] == 0x56 and + buf[11] == 0x45) + + +class Amr(Type): + """ + Implements the AMR audio type matcher. + """ + MIME = 'audio/amr' + EXTENSION = 'amr' + + def __init__(self): + super(Amr, self).__init__( + mime=Amr.MIME, + extension=Amr.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 11 and + buf[0] == 0x23 and + buf[1] == 0x21 and + buf[2] == 0x41 and + buf[3] == 0x4D and + buf[4] == 0x52 and + buf[5] == 0x0A) diff --git a/sharedutils/filetype/types/base.py b/sharedutils/filetype/types/base.py new file mode 100644 index 00000000..8213da1a --- /dev/null +++ b/sharedutils/filetype/types/base.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + + +class Type(object): + """ + Represents the file type object inherited by + specific file type matchers. + Provides convenient accessor and helper methods. + """ + def __init__(self, mime, extension): + self.__mime = mime + self.__extension = extension + + @property + def mime(self): + return self.__mime + + @property + def extension(self): + return self.__extension + + @property + def is_extension(self, extension): + return self.__extension is extension + + @property + def is_mime(self, mime): + return self.__mime is mime + + def match(self, buf): + raise NotImplementedError diff --git a/sharedutils/filetype/types/font.py b/sharedutils/filetype/types/font.py new file mode 100644 index 00000000..bdecf397 --- /dev/null +++ b/sharedutils/filetype/types/font.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .base import Type + + +class Woff(Type): + """ + Implements the WOFF font type matcher. + """ + MIME = 'application/font-woff' + EXTENSION = 'woff' + + def __init__(self): + super(Woff, self).__init__( + mime=Woff.MIME, + extension=Woff.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 7 and + buf[0] == 0x77 and + buf[1] == 0x4F and + buf[2] == 0x46 and + buf[3] == 0x46 and + buf[4] == 0x00 and + buf[5] == 0x01 and + buf[6] == 0x00 and + buf[7] == 0x00) + + +class Woff2(Type): + """ + Implements the WOFF2 font type matcher. + """ + MIME = 'application/font-woff' + EXTENSION = 'woff2' + + def __init__(self): + super(Woff2, self).__init__( + mime=Woff2.MIME, + extension=Woff2.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 7 and + buf[0] == 0x77 and + buf[1] == 0x4F and + buf[2] == 0x46 and + buf[3] == 0x32 and + buf[4] == 0x00 and + buf[5] == 0x01 and + buf[6] == 0x00 and + buf[7] == 0x00) + + +class Ttf(Type): + """ + Implements the TTF font type matcher. + """ + MIME = 'application/font-sfnt' + EXTENSION = 'ttf' + + def __init__(self): + super(Ttf, self).__init__( + mime=Ttf.MIME, + extension=Ttf.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 4 and + buf[0] == 0x00 and + buf[1] == 0x01 and + buf[2] == 0x00 and + buf[3] == 0x00 and + buf[4] == 0x00) + + +class Otf(Type): + """ + Implements the OTF font type matcher. + """ + MIME = 'application/font-sfnt' + EXTENSION = 'otf' + + def __init__(self): + super(Otf, self).__init__( + mime=Otf.MIME, + extension=Otf.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 4 and + buf[0] == 0x4F and + buf[1] == 0x54 and + buf[2] == 0x54 and + buf[3] == 0x4F and + buf[4] == 0x00) diff --git a/sharedutils/filetype/types/image.py b/sharedutils/filetype/types/image.py new file mode 100644 index 00000000..0c62e32c --- /dev/null +++ b/sharedutils/filetype/types/image.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .base import Type +from .isobmff import IsoBmff + + +class Jpeg(Type): + """ + Implements the JPEG image type matcher. + """ + MIME = 'image/jpeg' + EXTENSION = 'jpg' + + def __init__(self): + super(Jpeg, self).__init__( + mime=Jpeg.MIME, + extension=Jpeg.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 2 and + buf[0] == 0xFF and + buf[1] == 0xD8 and + buf[2] == 0xFF) + + +class Jpx(Type): + """ + Implements the JPEG2000 image type matcher. + """ + + MIME = "image/jpx" + EXTENSION = "jpx" + + def __init__(self): + super(Jpx, self).__init__(mime=Jpx.MIME, extension=Jpx.EXTENSION) + + def match(self, buf): + return ( + len(buf) > 50 + and buf[0] == 0x00 + and buf[1] == 0x00 + and buf[2] == 0x00 + and buf[3] == 0x0C + and buf[16:24] == b"ftypjp2 " + ) + + +class Png(Type): + """ + Implements the PNG image type matcher. + """ + MIME = 'image/png' + EXTENSION = 'png' + + def __init__(self): + super(Png, self).__init__( + mime=Png.MIME, + extension=Png.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x89 and + buf[1] == 0x50 and + buf[2] == 0x4E and + buf[3] == 0x47) + + +class Gif(Type): + """ + Implements the GIF image type matcher. + """ + MIME = 'image/gif' + EXTENSION = 'gif' + + def __init__(self): + super(Gif, self).__init__( + mime=Gif.MIME, + extension=Gif.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 2 and + buf[0] == 0x47 and + buf[1] == 0x49 and + buf[2] == 0x46) + + +class Webp(Type): + """ + Implements the WEBP image type matcher. + """ + MIME = 'image/webp' + EXTENSION = 'webp' + + def __init__(self): + super(Webp, self).__init__( + mime=Webp.MIME, + extension=Webp.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 13 and + buf[0] == 0x52 and + buf[1] == 0x49 and + buf[2] == 0x46 and + buf[3] == 0x46 and + buf[8] == 0x57 and + buf[9] == 0x45 and + buf[10] == 0x42 and + buf[11] == 0x50 and + buf[12] == 0x56 and + buf[13] == 0x50) + + +class Cr2(Type): + """ + Implements the CR2 image type matcher. + """ + MIME = 'image/x-canon-cr2' + EXTENSION = 'cr2' + + def __init__(self): + super(Cr2, self).__init__( + mime=Cr2.MIME, + extension=Cr2.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 9 and + ((buf[0] == 0x49 and buf[1] == 0x49 and + buf[2] == 0x2A and buf[3] == 0x0) or + (buf[0] == 0x4D and buf[1] == 0x4D and + buf[2] == 0x0 and buf[3] == 0x2A)) and + buf[8] == 0x43 and buf[9] == 0x52) + + +class Tiff(Type): + """ + Implements the TIFF image type matcher. + """ + MIME = 'image/tiff' + EXTENSION = 'tif' + + def __init__(self): + super(Tiff, self).__init__( + mime=Tiff.MIME, + extension=Tiff.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 3 and + ((buf[0] == 0x49 and buf[1] == 0x49 and + buf[2] == 0x2A and buf[3] == 0x0) or + (buf[0] == 0x4D and buf[1] == 0x4D and + buf[2] == 0x0 and buf[3] == 0x2A))) + + +class Bmp(Type): + """ + Implements the BMP image type matcher. + """ + MIME = 'image/bmp' + EXTENSION = 'bmp' + + def __init__(self): + super(Bmp, self).__init__( + mime=Bmp.MIME, + extension=Bmp.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 1 and + buf[0] == 0x42 and + buf[1] == 0x4D) + + +class Jxr(Type): + """ + Implements the JXR image type matcher. + """ + MIME = 'image/vnd.ms-photo' + EXTENSION = 'jxr' + + def __init__(self): + super(Jxr, self).__init__( + mime=Jxr.MIME, + extension=Jxr.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 2 and + buf[0] == 0x49 and + buf[1] == 0x49 and + buf[2] == 0xBC) + + +class Psd(Type): + """ + Implements the PSD image type matcher. + """ + MIME = 'image/vnd.adobe.photoshop' + EXTENSION = 'psd' + + def __init__(self): + super(Psd, self).__init__( + mime=Psd.MIME, + extension=Psd.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x38 and + buf[1] == 0x42 and + buf[2] == 0x50 and + buf[3] == 0x53) + + +class Ico(Type): + """ + Implements the ICO image type matcher. + """ + MIME = 'image/x-icon' + EXTENSION = 'ico' + + def __init__(self): + super(Ico, self).__init__( + mime=Ico.MIME, + extension=Ico.EXTENSION, + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x00 and + buf[1] == 0x00 and + buf[2] == 0x01 and + buf[3] == 0x00) + + +class Heic(IsoBmff): + """ + Implements the HEIC image type matcher. + """ + MIME = 'image/heic' + EXTENSION = 'heic' + + def __init__(self): + super(Heic, self).__init__( + mime=Heic.MIME, + extension=Heic.EXTENSION + ) + + def match(self, buf): + if not self._is_isobmff(buf): + return False + + major_brand, minor_version, compatible_brands = self._get_ftyp(buf) + if major_brand == 'heic': + return True + if major_brand in ['mif1', 'msf1'] and 'heic' in compatible_brands: + return True + return False + + +class Dcm(Type): + + MIME = 'application/dicom' + EXTENSION = 'dcm' + OFFSET = 128 + + def __init__(self): + super(Dcm, self).__init__( + mime=Dcm.MIME, + extension=Dcm.EXTENSION + ) + + def match(self, buf): + return (len(buf) > Dcm.OFFSET + 4 and + buf[Dcm.OFFSET + 0] == 0x44 and + buf[Dcm.OFFSET + 1] == 0x49 and + buf[Dcm.OFFSET + 2] == 0x43 and + buf[Dcm.OFFSET + 3] == 0x4D) diff --git a/sharedutils/filetype/types/isobmff.py b/sharedutils/filetype/types/isobmff.py new file mode 100644 index 00000000..3d5a1fc8 --- /dev/null +++ b/sharedutils/filetype/types/isobmff.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +import codecs + +from .base import Type + + +class IsoBmff(Type): + """ + Implements the ISO-BMFF base type. + """ + def __init__(self, mime, extension): + super(IsoBmff, self).__init__( + mime=mime, + extension=extension + ) + + def _is_isobmff(self, buf): + if len(buf) < 16 or buf[4:8] != b'ftyp': + return False + if len(buf) < int(codecs.encode(buf[0:4], 'hex'), 16): + return False + return True + + def _get_ftyp(self, buf): + ftyp_len = int(codecs.encode(buf[0:4], 'hex'), 16) + major_brand = buf[8:12].decode() + minor_version = int(codecs.encode(buf[12:16], 'hex'), 16) + compatible_brands = [] + for i in range(16, ftyp_len, 4): + compatible_brands.append(buf[i:i+4].decode()) + + return major_brand, minor_version, compatible_brands diff --git a/sharedutils/filetype/types/video.py b/sharedutils/filetype/types/video.py new file mode 100644 index 00000000..ea336716 --- /dev/null +++ b/sharedutils/filetype/types/video.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import + +from .base import Type +from .isobmff import IsoBmff + + +class Mp4(IsoBmff): + """ + Implements the MP4 video type matcher. + """ + MIME = 'video/mp4' + EXTENSION = 'mp4' + + def __init__(self): + super(Mp4, self).__init__( + mime=Mp4.MIME, + extension=Mp4.EXTENSION + ) + + def match(self, buf): + if not self._is_isobmff(buf): + return False + + major_brand, minor_version, compatible_brands = self._get_ftyp(buf) + return major_brand in ['mp41', 'mp42', 'isom'] + + +class M4v(Type): + """ + Implements the M4V video type matcher. + """ + MIME = 'video/x-m4v' + EXTENSION = 'm4v' + + def __init__(self): + super(M4v, self).__init__( + mime=M4v.MIME, + extension=M4v.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 10 and + buf[0] == 0x0 and buf[1] == 0x0 and + buf[2] == 0x0 and buf[3] == 0x1C and + buf[4] == 0x66 and buf[5] == 0x74 and + buf[6] == 0x79 and buf[7] == 0x70 and + buf[8] == 0x4D and buf[9] == 0x34 and + buf[10] == 0x56) + + +class Mkv(Type): + """ + Implements the MKV video type matcher. + """ + MIME = 'video/x-matroska' + EXTENSION = 'mkv' + + def __init__(self): + super(Mkv, self).__init__( + mime=Mkv.MIME, + extension=Mkv.EXTENSION + ) + + def match(self, buf): + return ((len(buf) > 15 and + buf[0] == 0x1A and buf[1] == 0x45 and + buf[2] == 0xDF and buf[3] == 0xA3 and + buf[4] == 0x93 and buf[5] == 0x42 and + buf[6] == 0x82 and buf[7] == 0x88 and + buf[8] == 0x6D and buf[9] == 0x61 and + buf[10] == 0x74 and buf[11] == 0x72 and + buf[12] == 0x6F and buf[13] == 0x73 and + buf[14] == 0x6B and buf[15] == 0x61) or + (len(buf) > 38 and + buf[31] == 0x6D and buf[32] == 0x61 and + buf[33] == 0x74 and buf[34] == 0x72 and + buf[35] == 0x6f and buf[36] == 0x73 and + buf[37] == 0x6B and buf[38] == 0x61)) + + +class Webm(Type): + """ + Implements the WebM video type matcher. + """ + MIME = 'video/webm' + EXTENSION = 'webm' + + def __init__(self): + super(Webm, self).__init__( + mime=Webm.MIME, + extension=Webm.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x1A and + buf[1] == 0x45 and + buf[2] == 0xDF and + buf[3] == 0xA3) + + +class Mov(IsoBmff): + """ + Implements the MOV video type matcher. + """ + MIME = 'video/quicktime' + EXTENSION = 'mov' + + def __init__(self): + super(Mov, self).__init__( + mime=Mov.MIME, + extension=Mov.EXTENSION + ) + + def match(self, buf): + if not self._is_isobmff(buf): + return False + + major_brand, minor_version, compatible_brands = self._get_ftyp(buf) + return major_brand == 'qt ' + + +class Avi(Type): + """ + Implements the AVI video type matcher. + """ + MIME = 'video/x-msvideo' + EXTENSION = 'avi' + + def __init__(self): + super(Avi, self).__init__( + mime=Avi.MIME, + extension=Avi.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 10 and + buf[0] == 0x52 and + buf[1] == 0x49 and + buf[2] == 0x46 and + buf[3] == 0x46 and + buf[8] == 0x41 and + buf[9] == 0x56 and + buf[10] == 0x49) + + +class Wmv(Type): + """ + Implements the WMV video type matcher. + """ + MIME = 'video/x-ms-wmv' + EXTENSION = 'wmv' + + def __init__(self): + super(Wmv, self).__init__( + mime=Wmv.MIME, + extension=Wmv.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 9 and + buf[0] == 0x30 and + buf[1] == 0x26 and + buf[2] == 0xB2 and + buf[3] == 0x75 and + buf[4] == 0x8E and + buf[5] == 0x66 and + buf[6] == 0xCF and + buf[7] == 0x11 and + buf[8] == 0xA6 and + buf[9] == 0xD9) + + +class Flv(Type): + """ + Implements the FLV video type matcher. + """ + MIME = 'video/x-flv' + EXTENSION = 'flv' + + def __init__(self): + super(Flv, self).__init__( + mime=Flv.MIME, + extension=Flv.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x46 and + buf[1] == 0x4C and + buf[2] == 0x56 and + buf[3] == 0x01) + + +class Mpeg(Type): + """ + Implements the MPEG video type matcher. + """ + MIME = 'video/mpeg' + EXTENSION = 'mpg' + + def __init__(self): + super(Mpeg, self).__init__( + mime=Mpeg.MIME, + extension=Mpeg.EXTENSION + ) + + def match(self, buf): + return (len(buf) > 3 and + buf[0] == 0x0 and + buf[1] == 0x0 and + buf[2] == 0x1 and + buf[3] >= 0xb0 and + buf[3] <= 0xbf) diff --git a/sharedutils/filetype/utils.py b/sharedutils/filetype/utils.py new file mode 100644 index 00000000..e5f2a076 --- /dev/null +++ b/sharedutils/filetype/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +_NUM_SIGNATURE_BYTES = 262 + + +def get_signature_bytes(path): + """ + Reads file from disk and returns the first 262 bytes + of data representing the magic number header signature. + + Args: + path: path string to file. + + Returns: + First 262 bytes of the file content as bytearray type. + """ + with open(path, 'rb') as fp: + return bytearray(fp.read(_NUM_SIGNATURE_BYTES)) + + +def signature(array): + """ + Returns the first 262 bytes of the given bytearray + as part of the file header signature. + + Args: + array: bytearray to extract the header signature. + + Returns: + First 262 bytes of the file content as bytearray type. + """ + length = len(array) + index = _NUM_SIGNATURE_BYTES if length > _NUM_SIGNATURE_BYTES else length + + return array[:index] + + +def get_bytes(obj): + """ + Infers the input type and reads the first 262 bytes, + returning a sliced bytearray. + + Args: + obj: path to readable, file, bytes or bytearray. + + Returns: + First 262 bytes of the file content as bytearray type. + + Raises: + TypeError: if obj is not a supported type. + """ + try: + obj = obj.read(_NUM_SIGNATURE_BYTES) + except AttributeError: + # duck-typing as readable failed - we'll try the other options + pass + + kind = type(obj) + + if kind is bytearray: + return signature(obj) + + if kind is str: + return get_signature_bytes(obj) + + if kind is bytes: + return signature(obj) + + if kind is memoryview: + return signature(obj).tolist() + + raise TypeError('Unsupported type as file input: %s' % kind) diff --git a/sharedutils/requirements.txt b/sharedutils/requirements.txt new file mode 100644 index 00000000..e69de29b diff --git a/template.yaml b/template.yaml new file mode 100644 index 00000000..2330a75a --- /dev/null +++ b/template.yaml @@ -0,0 +1,89 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + thumbnail_creator + + SAM Template for thumbnail_creator + +Globals: + Function: + Timeout: 180 + +Parameters: + ImageMagickLayer: + Type: String + Default: "arn:aws:lambda:us-east-1::layer:image-magick:1" + GhostscriptLayer: + Type: String + Default: "arn:aws:lambda:us-east-1::layer:ghostscript:1" + FfmpegLayer: + Type: String + Default: "arn:aws:lambda:us-east-1::layer:ffmpeg:1" + ConversionFileType: + Type: String + Default: jpg + ConversionMimeType: + Type: String + Default: image/jpeg + ThumbnailWidth: + Type: Number + Default: 150 + +Resources: + UploadBucket: + Type: AWS::S3::Bucket + + ResultsBucket: + Type: AWS::S3::Bucket + + CreateThumbnail: + Type: AWS::Serverless::Function + Properties: + CodeUri: apps/ + Handler: app.lambda_handler + Runtime: python3.8 + MemorySize: 2048 + Layers: + - !Ref ImageMagickLayer + - !Ref GhostscriptLayer + - !Ref FfmpegLayer + - !Ref FileTypeUtils + Policies: + - S3CrudPolicy: + BucketName: !Sub "${AWS::StackName}-*" + Environment: + Variables: + OUTPUT_BUCKET: !Ref ResultsBucket + EXTENSION: !Sub '.${ConversionFileType}' + MIME_TYPE: !Ref ConversionMimeType + THUMB_WIDTH: !Ref ThumbnailWidth + Events: + FileUpload: + Type: S3 + Properties: + Bucket: !Ref UploadBucket + Events: s3:ObjectCreated:* + + FileTypeUtils: + Type: AWS::Serverless::LayerVersion + Properties: + Description: FileType package + ContentUri: 'sharedutils/' + CompatibleRuntimes: + - python3.8 + Metadata: + BuildMethod: python3.8 + +Outputs: + CreateThumbnail: + Description: "CreateThumbnail Lambda Function ARN" + Value: !GetAtt CreateThumbnail.Arn + CreateThumbnailIamRole: + Description: "Implicit IAM Role created for CreateThumbnail function" + Value: !GetAtt CreateThumbnailRole.Arn + UploadBucket: + Description: "Upload S3 bucket" + Value: !Ref UploadBucket + ResultsBucket: + Description: "Results S3 bucket" + Value: !Ref ResultsBucket