Compare commits

...

11 Commits

Author SHA1 Message Date
d68b0b2744 Merge pull request #4 from domtes/removing_unneded_files
Removing unneded file
2020-11-22 11:01:55 +01:00
8d2a513945 Removing unneded file 2020-11-22 11:00:35 +01:00
e01cec63e9 Merge branch 'master' of github.com:domtes/once 2020-11-22 10:55:38 +01:00
8b5fbf3ddf Reformatting sources with black 2020-11-22 10:52:48 +01:00
84b4ad5305 Updating dependencies after breaking changes in AWS CDK 2020-11-22 10:50:36 +01:00
f5df37d979 Merge pull request #3 from druizz90/master
Update parameters for building lambda dependencies
2020-08-06 17:38:47 +02:00
druizz90
eaf449c67a Add parameter for building the lambda function dependencies as user instead of root 2020-08-06 17:08:11 +02:00
c518fcaf14 Merge pull request #2 from domtes/remove_pip_and_setuptools_files
Removing setuptools and pip files.
2020-08-02 22:10:12 +02:00
ec22897c96 Removing setuptools and pip files.
Updating README to use `poetry`.
2020-08-02 22:09:14 +02:00
3c95f31f8d Merge pull request #1 from domtes/move_to_poetry
Moving to poetry for dependency management.
2020-08-02 22:02:17 +02:00
f71d5d8039 Moving to poetry for dependency management. 2020-08-02 22:00:12 +02:00
16 changed files with 1771 additions and 959 deletions

27
Pipfile
View File

@@ -1,27 +0,0 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
setuptools = {editable = true, file = "file:///Users/domenico/dev/once"}
"aws-cdk.core" = ">=1.45"
"aws-cdk.aws-apigatewayv2" = ">=1.45"
"aws-cdk.aws-dynamodb" = ">=1.45"
"aws-cdk.aws-lambda" = ">=1.45"
"aws-cdk.aws-s3" = ">=1.45"
click = "*"
requests = "*"
Pygments = "*"
urllib3 = {editable = true, file = "file:///Users/domenico/dev/once"}
"aws-cdk.aws-certificatemanager" = ">=1.45"
"aws-cdk.aws-cloudformation" = ">=1.45"
"aws-cdk.aws-events" = ">=1.45"
"aws-cdk.aws-events-targets" = ">=1.45"
"aws-cdk.aws-logs" = ">=1.45"
"aws-cdk.aws-route53" = ">=1.45"
[requires]
python_version = "3.7"

521
Pipfile.lock generated
View File

@@ -1,521 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "85a3f58b964eac12c6836bec87d6450451436d461081ec6f4d8ca5d96f7333ae"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==19.3.0"
},
"aws-cdk.assets": {
"hashes": [
"sha256:20c78b332f9134cec0a1f4b90c981b596c0997c03908a269d7a9ec902f2d957e",
"sha256:3128cd3a44f2dea9165f4c424f2925bfaea8e0f924a0c3c8e285068e95d454f2"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-apigateway": {
"hashes": [
"sha256:297bd75142d206a4f8ec6882ead8fde6ab0c1cda9995dd76e77a4cfc3203ce6e",
"sha256:6d8e3436dc466ce677228f6bb14c7650f48b272a8ca7d4938e1ec48b36e3780c"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-apigatewayv2": {
"hashes": [
"sha256:61a0e44b2dd593fab42364d0e0bb3245fa70e164dc25fd94d5cc64e114d442e0",
"sha256:7c6822259ea779ae48b1d050d2e05641556a848e1ae4ba2f62743574353c6689"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-applicationautoscaling": {
"hashes": [
"sha256:211b43bb9667dbc3ec62c7f743fef9a02588310b62f544c35eb3492846a84230",
"sha256:6dadbb5f81c5a0816e59c290b0139333683db2e87aebd736bef6e31c52300468"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-autoscaling": {
"hashes": [
"sha256:5d9bf9109c4f26bb6f02fefbf08da5b65fc9aad033e5a6c67922a0b4f922df43",
"sha256:c35de70bdb63fa6baf6d43dc9da436ef548e638905eb9a2c43e54e79eb90184b"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-autoscaling-common": {
"hashes": [
"sha256:c0e33bffac360e9144964a9cce260a80acfb1a62ba64efc56b7bfc32f499863a",
"sha256:f7ed4878182d90b0e4db3094cb76abaa3dfc70ec5b3cee16eff518515e360092"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-autoscaling-hooktargets": {
"hashes": [
"sha256:3b445bf9906daa29d794419acf9a2371d2faadba2d46be5d8fd6cc8e168b2bad",
"sha256:87a82a2be98dd80a21bca9213de14a99cdf1e015248c868c2792348b2f33e8f3"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-batch": {
"hashes": [
"sha256:2bdefe2c7ee793b70cefbbcf252172968f74635e173660ecc153fd7334839597",
"sha256:c65d0c01cf9ef5f5c128f469840f61fe9e6f6e8639cbe0cac9c4d697d1f22024"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-certificatemanager": {
"hashes": [
"sha256:23e23fb077c4523a7d9c4530e9968e1f2aaf991392ece18c174922e738c1dea7",
"sha256:bab5a2cea794674b78fe691ad85daf8849f2a4667ffa250de5a2bc9043afcf9d"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-cloudformation": {
"hashes": [
"sha256:48d60f6e26dbc9804ac18c51aa73d60842a08ee37c9d8ff54f22f50248ea7bb2",
"sha256:67ca3005557990775fff55eaca0e6a6faa7a28462d7192a85438203760dd4b40"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-cloudfront": {
"hashes": [
"sha256:a15b43e83645c1f00813b27f3a99d83bd3e77ac0070d1bb98141be474bc8fb8f",
"sha256:d7c1039945c6a0c5c4771afef08ae30ccaf37db21c6b82ad6a7db3648246f224"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-cloudwatch": {
"hashes": [
"sha256:7d94bb25a46892ea0e827c844bc0bedc70b8031ef48955641c5d184eb6f269bc",
"sha256:97602eeef05a09e350fcfc7bf627505b1630e8b8393b82072c386d1f74560857"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-codebuild": {
"hashes": [
"sha256:7bde7fb4e1d033b885dcba4bbd55136c1cee417e3ada8950c93b8765b588e273",
"sha256:ead186ecdf8c39062cfd95cf3e9a2cca8e8e6a5a4194bc15b4f4a4a6f43cfdca"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-codecommit": {
"hashes": [
"sha256:0783e9ea449c208bde96a945990d21d913e9f3c532fc117224d1f6a4b595d7e7",
"sha256:58e4a3ec4ed94367a0ebfac1caa58fba7152fc80ace7567b069b733117a571ab"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-codepipeline": {
"hashes": [
"sha256:17de3c5117d3393ccc3f683e7108f02c60f447bcc009040d21589fdae357e7be",
"sha256:61b1a16e42731c5590307556b8859c7989471147d81497e8139a4e4f10911764"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-cognito": {
"hashes": [
"sha256:5177efe18088dc8029976a2bf648caa3b20773ab290e5a6bb81da2d8e9439ca8",
"sha256:870ca391ba0aefccd5d5246d6804e75b1bd4eb4d9e3c5791fa2b92ea264bc75d"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-dynamodb": {
"hashes": [
"sha256:540ae28d1832dfe183b19377d97ff7b4a10b6c3dcdab4b7741e5f6443963ceb2",
"sha256:941a5c88160588f65b5a5481a8b0600844c4b839998e287ae18eb43fc9a1c500"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-ec2": {
"hashes": [
"sha256:45ea01ff8bb055333f5987140c13ba49b0a07658515b147f0eb4f5c7d65dca94",
"sha256:87674a4724e41fcbb3e51a77f0cac29745bb2bb917341aba5b454bc156540ee3"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-ecr": {
"hashes": [
"sha256:ca76cc9a4321544097962cc8c7fb0d1e41307fcf91427768f5356741114b2009",
"sha256:d73c28c618e2417002c6ef3dece5de630a7678eea4c845cf080e9a540de3b8a4"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-ecr-assets": {
"hashes": [
"sha256:34896d4b6f275c368d4e65921ecc205bc9180e934d40678b87fdd1d3a136f93b",
"sha256:a24407c8dde824c1f0558cc5b166d421efaf57b96612957816334c25f9465a83"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-ecs": {
"hashes": [
"sha256:11eee2c5a026fb349699413e99c36bb0cd31fd06f673f8f5a26a92bac8fa91cb",
"sha256:85163a64ad2e2308bc79b4cb9100ea9ad27e9bbbfe05f7df0e1bbf59a6b9e71d"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-elasticloadbalancing": {
"hashes": [
"sha256:06fd8232eeb51952e31f55a38bae02d937ddb5c11829630691100fa55d9479e3",
"sha256:fef69ff521c63e62b5829697fc306cfb17ad38639caa3b2ca674b95b8861ccbf"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-elasticloadbalancingv2": {
"hashes": [
"sha256:00ac6d6cb779df5425e9f724464818aeb85d7fb86cf47061f1345fd01262a35a",
"sha256:21432ebc23700b6776d9e0b0e0ee7ce565aea4f6644aa01a3eea90e32b55f3eb"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-events": {
"hashes": [
"sha256:59d046aa1a2c5e04a8655981d0a8fc1ac1320b66d0b373fb5d91b3352a21448b",
"sha256:6d6a52ca4860bfbaba620a5761904a3ffec8d86a6399f3dc232e04164e9d2f9f"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-events-targets": {
"hashes": [
"sha256:8fe20271b73be9334922e94639858061499184c2dde11986fe849b54a9fdbdf2",
"sha256:dfe372641c833c9a911c9e8ea4720ef34ba568cf52a188599eb47e02c00e4e35"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-iam": {
"hashes": [
"sha256:a4db816cbcd3642caf741dc934e8d2a09cd4e7ecfee1b325da28cfbf23318a25",
"sha256:e94b635244ba8f4595e7ec5c52c0cc84e23226f92ef2d9c7c87a8980b66acfc7"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-kinesis": {
"hashes": [
"sha256:199c976e03d0521f0127842a78e1ce4d1ba9ff4a336310c8ad32831af0040f5d",
"sha256:ac44a1c8b8b7ab105b4911397047af037e8596ca6da8b3efdba3b040718f4914"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-kms": {
"hashes": [
"sha256:3949d3deeae0718d91acfec33e251959b980505a350cbf2de366b113fcd9ce0c",
"sha256:84780491977946ed9c21d35d70bbca21220b72ef27f0c2c56450235446126e9c"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-lambda": {
"hashes": [
"sha256:3120ff8b58541b9102651899832286fbe5cddd54e2a9dcebc984e08d59d03286",
"sha256:f77ca23ce9c0ba3187e91541286dd14b6fc8e22f87f7aecdbce1c79755146113"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-logs": {
"hashes": [
"sha256:9db12970b2303431de70502e925515e5734e2c5e0d68c5aadba63a1595cbd076",
"sha256:be1b892cbc7bbfe7fe67823360f39c2c0f76650d0a24760091b718b37ecec638"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-route53": {
"hashes": [
"sha256:7ee747898b9df6c24332b68937b96b17316dea31b3a67d7d36763c77cab1bc9f",
"sha256:c4e6a80c7042f4216c198fbe3a1153e91c7228f938510f8301e9365e811069f7"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-route53-targets": {
"hashes": [
"sha256:db5f5c62bd0089ef38c4383423bc06c3337a379ec47d0ee2da28034bae8c8fab",
"sha256:e9af2e9e013579a4ccc5036e28fba7b979e8bcc6853873b7f79d21b22bbc0cad"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-s3": {
"hashes": [
"sha256:16239f51ffb80820ae0c103f053ceb41a765dd5efa4f739a33352d093afc8698",
"sha256:64b261b88a0232cd09b05c7219e19ee8a59fb478ca2cb8b54ad728f1c421237e"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.aws-s3-assets": {
"hashes": [
"sha256:1ba312dbb33243aad60e0d7e634e801c045308eec2cbe73a7dee642bcc29994e",
"sha256:d5d61e8a54059b8c2ac986785d89012bbd3a97379925e07d7f7579a430b91e45"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-sam": {
"hashes": [
"sha256:c974f21e8104698395e0820951bfa9243e5ed990368a250935b4bc1964a2bc1d",
"sha256:d0a852e2638cc2d7aa1562ea3cad382ab70925a8bbbb277889e1150c455acbf7"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-secretsmanager": {
"hashes": [
"sha256:10445782936ee2d699745fc455576a39c5612d40e01fc5e793e9fd72efa685a3",
"sha256:159dae0fcce7fa1458c6d4e152e615ab85024190d74dbd2022e38e9eb0d30b98"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-servicediscovery": {
"hashes": [
"sha256:5b00f7eda3029c0d322da94ecd5d9e5dbebc2cc52e507d329342d0db2f6d2e88",
"sha256:d006da251dd61eb63664a37dec417538907f4c25c8ded6467b0bb151754fa8eb"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-sns": {
"hashes": [
"sha256:732f3ce0c193b84027a2a6df7ad6ab7b7ba3eade0ecb2c6effa8403990575119",
"sha256:8f3f4b26deb8846e167ee9e8d044265c4ed2e418e44d9cf7db4954e7b04405d9"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-sns-subscriptions": {
"hashes": [
"sha256:8b6270507c09eb50fc3c8f2cf40aa4c6ac0250535a603a19b8935e1e0ec8007f",
"sha256:d80c4bdc3ed7c9c16ff45ff4e944e392ec61a222cb101a29d806b91f72d044c9"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-sqs": {
"hashes": [
"sha256:16e148f6dff195e821053176c2817bdc7cf6693f1ba9d51ddb7739c8d5d7e54c",
"sha256:a6276ee9235f094b6bf718edd9dcd639844ee1e5f96c772dd33100a14e6b3de3"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-ssm": {
"hashes": [
"sha256:30775b64b5b3a6974854170406e268061d1891f606d98f9bccdfccc87a3d5a0d",
"sha256:6e1a91b268dcec24d53b8699f468b6440b71bb76dc141ef5e66480f523e24e75"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.aws-stepfunctions": {
"hashes": [
"sha256:57076364fa525de48659689fa0fa22f1f9d5a223515d969405a2285ac7cd679b",
"sha256:7895371bf34e4c56846e54ece1c505d31d176948ff7b50c33865d59f6da9c1d8"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.cdk-assets-schema": {
"hashes": [
"sha256:21222ba4d02d6db2c4abf40884e7a43910084cfc57d16c50a4441a1058ba2ea6",
"sha256:f0a95af77c2884e49ef6d64c41bb79b0f3ad4f6ee95b8bddba8f928258a83a68"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.cloud-assembly-schema": {
"hashes": [
"sha256:458f80265a9f520d63d1fd755ae958f3773fb4d6d932b0ae31bda1fb5ee90b08",
"sha256:caf1e24a963c28171b29d523ae81a5a64b8897c95ffe9cb04b574dccf6af194f"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.core": {
"hashes": [
"sha256:1f7fbceb0b02064f4349f6ad7030247d2a422cddf035847da1d479ee7c9d18e3",
"sha256:f224f584df1a2ce354bc42ba7d0c870d08bcd53c3f56e9d4b991ab2be08d6bef"
],
"index": "pypi",
"version": "==1.45.0"
},
"aws-cdk.custom-resources": {
"hashes": [
"sha256:05bf20b40a2ddcf364a538eb8c211160d84c909f485c83ef585cc511f0b9292b",
"sha256:920449820eee21fd644c18d3bf026a7e850bbd1cd01d57ed25f892d16f56b1ab"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.cx-api": {
"hashes": [
"sha256:2ea1fd39e855f6ce8f1f5465d4215061946e09ea6b2105929331e80ba04f636b",
"sha256:2eb53f791ab9b2dbc6be564c3aa125aafb93224f3e85a299c11d41721f4d72c4"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"aws-cdk.region-info": {
"hashes": [
"sha256:516cdeabc1104d19e6c5b7a40a4cdf80c00e98d997cb08781892d183d1ec7064",
"sha256:53e2ba371ae574cde0b0c09782d26faa9e5f40bdf323784b2b250386a1cb2f32"
],
"markers": "python_version >= '3.6'",
"version": "==1.45.0"
},
"cattrs": {
"hashes": [
"sha256:616972ae3dfa6e623a40ad3cb845420e64942989152774ab055e5c2b2f89f997",
"sha256:b7ab5cf8ad127c42eefd01410c1c6e28569a45a255ea80ed968511873c433c7a"
],
"version": "==1.0.0"
},
"certifi": {
"hashes": [
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
],
"version": "==2020.4.5.2"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"index": "pypi",
"version": "==7.1.2"
},
"constructs": {
"hashes": [
"sha256:ba2e0c3ff46e08095307ba45e9dcbe2e16599c2761ca661c62024121fb331cb9",
"sha256:ce96789470fe6a05c3ba168f6fbf4bd41e6daa01b62731bb7728941ce1d3312a"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.3"
},
"idna": {
"hashes": [
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.9"
},
"jsii": {
"hashes": [
"sha256:953d81df16cd292f32512e024acd3409c26b16e0b91dfb73090a78974d362d54",
"sha256:f3950287f6cb592931963eeaedc5c223ba4a2f02f0c45108584affcd66685257"
],
"markers": "python_version >= '3.6'",
"version": "==1.6.0"
},
"publication": {
"hashes": [
"sha256:0248885351febc11d8a1098d5c8e3ab2dabcf3e8c0c96db1e17ecd12b53afbe6",
"sha256:68416a0de76dddcdd2930d1c8ef853a743cc96c82416c4e4d3b5d901c6276dc4"
],
"version": "==0.0.3"
},
"pygments": {
"hashes": [
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
],
"index": "pypi",
"version": "==2.6.1"
},
"python-dateutil": {
"hashes": [
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.1"
},
"requests": {
"hashes": [
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
],
"index": "pypi",
"version": "==2.23.0"
},
"setuptools": {
"editable": true,
"file": "file:///Users/domenico/dev/once"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.15.0"
},
"typing-extensions": {
"hashes": [
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
],
"version": "==3.7.4.2"
},
"urllib3": {
"editable": true,
"file": "file:///Users/domenico/dev/once"
}
},
"develop": {}
}

View File

@@ -22,7 +22,7 @@ Make sure you have installed the latest CDK version for your platform, following
Install the required dependencies (you can use a virtualenv for this), with the following command: Install the required dependencies (you can use a virtualenv for this), with the following command:
pip install -r requirements.txt poetry install
The deployment can be then initiated, from the project root directory, with the following command: The deployment can be then initiated, from the project root directory, with the following command:
@@ -64,13 +64,9 @@ If you need more details about creating a public hosted zone on AWS, consult the
## Uploading a file ## Uploading a file
To make the `once` command available you can install it using pip, with the following command:
$ pip install .
Once the service and the client have been correctly installed and configured, you can upload a local file running the `once` command. Once the service and the client have been correctly installed and configured, you can upload a local file running the `once` command.
once <file_toshare> poetry run once <file_toshare>
The URL can be shared to download the file, only once. The URL can be shared to download the file, only once.

51
app.py
View File

@@ -9,48 +9,51 @@ from aws_cdk import core
from once.once_stack import OnceStack, CustomDomainStack from once.once_stack import OnceStack, CustomDomainStack
ONCE_CONFIG_FILE = os.getenv('ONCE_CONFIG_FILE', os.path.expanduser('~/.once')) ONCE_CONFIG_FILE = os.getenv("ONCE_CONFIG_FILE", os.path.expanduser("~/.once"))
SECRET_KEY = os.getenv('SECRET_KEY') SECRET_KEY = os.getenv("SECRET_KEY")
CUSTOM_DOMAIN = os.getenv('CUSTOM_DOMAIN') CUSTOM_DOMAIN = os.getenv("CUSTOM_DOMAIN")
HOSTED_ZONE_NAME = os.getenv('HOSTED_ZONE_NAME') HOSTED_ZONE_NAME = os.getenv("HOSTED_ZONE_NAME")
HOSTED_ZONE_ID = os.getenv('HOSTED_ZONE_ID') HOSTED_ZONE_ID = os.getenv("HOSTED_ZONE_ID")
def generate_random_key() -> str: def generate_random_key() -> str:
return base64.b64encode(os.urandom(128)).decode('utf-8') return base64.b64encode(os.urandom(128)).decode("utf-8")
def generate_config(secret_key: Optional[str] = None, def generate_config(
secret_key: Optional[str] = None,
custom_domain: str = None, custom_domain: str = None,
hosted_zone_name: str = None, hosted_zone_name: str = None,
hosted_zone_id: str = None) -> configparser.ConfigParser: hosted_zone_id: str = None,
) -> configparser.ConfigParser:
config = configparser.ConfigParser() config = configparser.ConfigParser()
config['once'] = { config["once"] = {
'secret_key': secret_key or generate_random_key(), "secret_key": secret_key or generate_random_key(),
} }
config['deployment'] = {} config["deployment"] = {}
if all([custom_domain, hosted_zone_name, hosted_zone_id]): if all([custom_domain, hosted_zone_name, hosted_zone_id]):
config['once']['base_url'] = f'https://{custom_domain}' config["once"]["base_url"] = f"https://{custom_domain}"
config['deployment'] = { config["deployment"] = {
'custom_domain': custom_domain, "custom_domain": custom_domain,
'hosted_zone_name': hosted_zone_name, "hosted_zone_name": hosted_zone_name,
'hosted_zone_id': hosted_zone_id "hosted_zone_id": hosted_zone_id,
} }
return config return config
def get_config(config_gile: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser: def get_config(config_gile: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser:
if not os.path.exists(ONCE_CONFIG_FILE): if not os.path.exists(ONCE_CONFIG_FILE):
print(f'Generating configuration file at {ONCE_CONFIG_FILE}') print(f"Generating configuration file at {ONCE_CONFIG_FILE}")
with open(ONCE_CONFIG_FILE, 'w') as config_file: with open(ONCE_CONFIG_FILE, "w") as config_file:
config = generate_config( config = generate_config(
secret_key=SECRET_KEY, secret_key=SECRET_KEY,
custom_domain=CUSTOM_DOMAIN, custom_domain=CUSTOM_DOMAIN,
hosted_zone_name=HOSTED_ZONE_NAME, hosted_zone_name=HOSTED_ZONE_NAME,
hosted_zone_id=HOSTED_ZONE_ID) hosted_zone_id=HOSTED_ZONE_ID,
)
config.write(config_file) config.write(config_file)
else: else:
config = configparser.ConfigParser() config = configparser.ConfigParser()
@@ -61,14 +64,14 @@ def get_config(config_gile: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser
def main(): def main():
config = get_config() config = get_config()
kwargs = {'secret_key': config['once']['secret_key']} kwargs = {"secret_key": config["once"]["secret_key"]}
if config.has_section('deployment'): if config.has_section("deployment"):
kwargs.update(config['deployment']) kwargs.update(config["deployment"])
app = core.App() app = core.App()
once = OnceStack(app, 'once', **kwargs) once = OnceStack(app, "once", **kwargs)
app.synth() app.synth()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -1,6 +1,6 @@
''' """
Simple command to share one-time files Simple command to share one-time files
''' """
import os import os
import base64 import base64
@@ -17,9 +17,9 @@ import requests
from pygments import highlight, lexers, formatters from pygments import highlight, lexers, formatters
ONCE_CONFIG_FILE = os.getenv('ONCE_CONFIG_FILE', os.path.expanduser('~/.once')) ONCE_CONFIG_FILE = os.getenv("ONCE_CONFIG_FILE", os.path.expanduser("~/.once"))
ONCE_SIGNATURE_HEADER = 'x-once-signature' ONCE_SIGNATURE_HEADER = "x-once-signature"
ONCE_TIMESTAMP_FORMAT = '%Y%m%d%H%M%S%f' ONCE_TIMESTAMP_FORMAT = "%Y%m%d%H%M%S%f"
def highlight_json(obj): def highlight_json(obj):
@@ -33,7 +33,7 @@ def echo_obj(obj):
def get_config(config_file: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser: def get_config(config_file: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser:
if not os.path.exists(config_file): if not os.path.exists(config_file):
raise ValueError(f'Config file not found at {config_file}') raise ValueError(f"Config file not found at {config_file}")
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(ONCE_CONFIG_FILE) config.read(ONCE_CONFIG_FILE)
return config return config
@@ -41,59 +41,57 @@ def get_config(config_file: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser
def api_req(method: str, url: str, verbose: bool = False, **kwargs): def api_req(method: str, url: str, verbose: bool = False, **kwargs):
config = get_config() config = get_config()
if not config.has_option('once', 'base_url'): if not config.has_option("once", "base_url"):
raise ValueError(f'Configuration file at {ONCE_CONFIG_FILE} misses `base_url` option') raise ValueError(f"Configuration file at {ONCE_CONFIG_FILE} misses `base_url` option")
base_url = os.getenv('ONCE_API_URL', config['once']['base_url']) base_url = os.getenv("ONCE_API_URL", config["once"]["base_url"])
secret_key = base64.b64decode(os.getenv('ONCE_SECRET_KEY', config['once']['secret_key'])) secret_key = base64.b64decode(os.getenv("ONCE_SECRET_KEY", config["once"]["secret_key"]))
method = method.lower() method = method.lower()
if method not in ['get', 'post']: if method not in ["get", "post"]:
raise ValueError(f'Unsupported HTTP method "{method}"') raise ValueError(f'Unsupported HTTP method "{method}"')
actual_url = urljoin(base_url, url) actual_url = urljoin(base_url, url)
if verbose: if verbose:
print(f'{method.upper()} {actual_url}') print(f"{method.upper()} {actual_url}")
req = requests.Request(method=method, url=actual_url, **kwargs).prepare() req = requests.Request(method=method, url=actual_url, **kwargs).prepare()
plain_text = req.path_url.encode('utf-8') plain_text = req.path_url.encode("utf-8")
hmac_obj = hmac.new(secret_key, msg=plain_text, digestmod=hashlib.sha256) hmac_obj = hmac.new(secret_key, msg=plain_text, digestmod=hashlib.sha256)
req.headers[ONCE_SIGNATURE_HEADER] = base64.b64encode(hmac_obj.digest()) req.headers[ONCE_SIGNATURE_HEADER] = base64.b64encode(hmac_obj.digest())
response = requests.Session().send(req) response = requests.Session().send(req)
if verbose: if verbose:
print(f'Server response status: {response.status_code}') print(f"Server response status: {response.status_code}")
echo_obj(response.json()) echo_obj(response.json())
return response return response
@click.command('share') @click.command("share")
@click.argument('file', type=click.File(mode='rb'), required=True) @click.argument("file", type=click.File(mode="rb"), required=True)
@click.option('--verbose', '-v', is_flag=True, default=False, help='Enables verbose output.') @click.option("--verbose", "-v", is_flag=True, default=False, help="Enables verbose output.")
def share(file: click.File, verbose: bool): def share(file: click.File, verbose: bool):
entry = api_req('GET', '/', entry = api_req(
params={ "GET",
'f': quote_plus(os.path.basename(file.name)), "/",
't': datetime.utcnow().strftime(ONCE_TIMESTAMP_FORMAT) params={"f": quote_plus(os.path.basename(file.name)), "t": datetime.utcnow().strftime(ONCE_TIMESTAMP_FORMAT)},
}, verbose=verbose,
verbose=verbose).json() ).json()
once_url = entry['once_url'] once_url = entry["once_url"]
upload_data = entry['presigned_post'] upload_data = entry["presigned_post"]
files = {'file': file} files = {"file": file}
upload_started = time.time() upload_started = time.time()
response = requests.post(upload_data['url'], response = requests.post(upload_data["url"], data=upload_data["fields"], files=files)
data=upload_data['fields'],
files=files)
upload_time = time.time() - upload_started upload_time = time.time() - upload_started
print(f"File uploaded in {upload_time}s") print(f"File uploaded in {upload_time}s")
print(f"File can be downloaded once at: {once_url}") print(f"File can be downloaded once at: {once_url}")
if __name__ == '__main__': if __name__ == "__main__":
share() share()

View File

@@ -1,3 +0,0 @@
click
pygments
requests

View File

@@ -9,16 +9,16 @@ from boto3.dynamodb.conditions import Key
def is_debug_enabled() -> bool: def is_debug_enabled() -> bool:
value = os.getenv('DEBUG', 'false').lower() value = os.getenv("DEBUG", "false").lower()
if value in ['false', '0']: if value in ["false", "0"]:
return False return False
else: else:
return bool(value) return bool(value)
DEBUG = is_debug_enabled() DEBUG = is_debug_enabled()
FILES_BUCKET = os.getenv('FILES_BUCKET') FILES_BUCKET = os.getenv("FILES_BUCKET")
FILES_TABLE_NAME = os.getenv('FILES_TABLE_NAME') FILES_TABLE_NAME = os.getenv("FILES_TABLE_NAME")
log = logging.getLogger() log = logging.getLogger()
@@ -29,28 +29,27 @@ else:
def on_event(event, context): def on_event(event, context):
log.debug(f'Event received: {event}') log.debug(f"Event received: {event}")
log.debug(f'Context is: {context}') log.debug(f"Context is: {context}")
log.debug(f'Debug mode is {DEBUG}') log.debug(f"Debug mode is {DEBUG}")
log.debug(f'Files bucket is "{FILES_BUCKET}"') log.debug(f'Files bucket is "{FILES_BUCKET}"')
dynamodb = boto3.client('dynamodb') dynamodb = boto3.client("dynamodb")
response = dynamodb.scan( response = dynamodb.scan(
TableName=FILES_TABLE_NAME, TableName=FILES_TABLE_NAME,
Select='ALL_ATTRIBUTES', Select="ALL_ATTRIBUTES",
FilterExpression='deleted = :deleted', FilterExpression="deleted = :deleted",
ExpressionAttributeValues={ ExpressionAttributeValues={":deleted": {"BOOL": True}},
':deleted': {'BOOL': True} )
})
s3 = boto3.client('s3') s3 = boto3.client("s3")
for item in response['Items']: for item in response["Items"]:
object_name = item['object_name']['S'] object_name = item["object_name"]["S"]
log.info(f'Deleting file {object_name}') log.info(f"Deleting file {object_name}")
try: try:
s3.delete_object(Bucket=FILES_BUCKET, Key=object_name) s3.delete_object(Bucket=FILES_BUCKET, Key=object_name)
except: except:
log.exception('Could not delete file {object_name}') log.exception("Could not delete file {object_name}")
response = dynamodb.delete_item(TableName=FILES_TABLE_NAME, Key={'id': item['id']}) response = dynamodb.delete_item(TableName=FILES_TABLE_NAME, Key={"id": item["id"]})
log.debug(f'dynamodb delete item: {response}') log.debug(f"dynamodb delete item: {response}")

View File

@@ -9,31 +9,36 @@ import boto3
def is_debug_enabled() -> bool: def is_debug_enabled() -> bool:
value = os.getenv('DEBUG', 'false').lower() value = os.getenv("DEBUG", "false").lower()
if value in ['false', '0']: if value in ["false", "0"]:
return False return False
else: else:
return bool(value) return bool(value)
DEBUG = is_debug_enabled() DEBUG = is_debug_enabled()
FILES_BUCKET = os.getenv('FILES_BUCKET') FILES_BUCKET = os.getenv("FILES_BUCKET")
FILES_TABLE_NAME = os.getenv('FILES_TABLE_NAME') FILES_TABLE_NAME = os.getenv("FILES_TABLE_NAME")
PRESIGNED_URL_EXPIRES_IN = int(os.getenv('PRESIGNED_URL_EXPIRES_IN', 20)) PRESIGNED_URL_EXPIRES_IN = int(os.getenv("PRESIGNED_URL_EXPIRES_IN", 20))
MASKED_USER_AGENTS = os.getenv('MASKED_USER_AGENTS', ','.join([ MASKED_USER_AGENTS = os.getenv(
'^Facebook.*', "MASKED_USER_AGENTS",
'^Google.*', ",".join(
'^Instagram.*', [
'^LinkedIn.*', "^Facebook.*",
'^Outlook.*', "^Google.*",
'^Reddit.*', "^Instagram.*",
'^Slack.*', "^LinkedIn.*",
'^Skype.*', "^Outlook.*",
'^SnapChat.*', "^Reddit.*",
'^Telegram.*', "^Slack.*",
'^Twitter.*', "^Skype.*",
'^WhatsApp.*' "^SnapChat.*",
])).split(',') "^Telegram.*",
"^Twitter.*",
"^WhatsApp.*",
]
),
).split(",")
log = logging.getLogger() log = logging.getLogger()
@@ -44,55 +49,45 @@ else:
def on_event(event, context): def on_event(event, context):
log.debug(f'Event received: {event}') log.debug(f"Event received: {event}")
log.debug(f'Context is: {context}') log.debug(f"Context is: {context}")
log.debug(f'Debug mode is {DEBUG}') log.debug(f"Debug mode is {DEBUG}")
log.debug(f'Files bucket is "{FILES_BUCKET}"') log.debug(f'Files bucket is "{FILES_BUCKET}"')
entry_id = event['pathParameters']['entry_id'] entry_id = event["pathParameters"]["entry_id"]
filename = urllib.parse.unquote_plus(event['pathParameters']['filename']) filename = urllib.parse.unquote_plus(event["pathParameters"]["filename"])
object_name = f'{entry_id}/{filename}' object_name = f"{entry_id}/{filename}"
dynamodb = boto3.client('dynamodb') dynamodb = boto3.client("dynamodb")
entry = dynamodb.get_item( entry = dynamodb.get_item(TableName=FILES_TABLE_NAME, Key={"id": {"S": entry_id}})
TableName=FILES_TABLE_NAME,
Key={'id': {'S': entry_id}})
log.debug(f'Matched Dynamodb entry: {entry}') log.debug(f"Matched Dynamodb entry: {entry}")
if 'Item' not in entry or 'deleted' in entry['Item']: if "Item" not in entry or "deleted" in entry["Item"]:
error_message = f'Entry not found: {object_name}' error_message = f"Entry not found: {object_name}"
log.info(error_message) log.info(error_message)
return {'statusCode': 404, 'body': error_message} return {"statusCode": 404, "body": error_message}
# Some rich clients try to get a preview of any link pasted # Some rich clients try to get a preview of any link pasted
# into text controls. # into text controls.
user_agent = event['headers'].get('user-agent', '') user_agent = event["headers"].get("user-agent", "")
is_masked_agent = any([re.match(agent, user_agent) for agent in MASKED_USER_AGENTS]) is_masked_agent = any([re.match(agent, user_agent) for agent in MASKED_USER_AGENTS])
if is_masked_agent: if is_masked_agent:
log.info('Serving possible link preview. Download prevented.') log.info("Serving possible link preview. Download prevented.")
return { return {"statusCode": 200, "headers": {}}
'statusCode': 200,
'headers': {}
}
s3 = boto3.client('s3') s3 = boto3.client("s3")
download_url = s3.generate_presigned_url( download_url = s3.generate_presigned_url(
'get_object', "get_object", Params={"Bucket": FILES_BUCKET, "Key": object_name}, ExpiresIn=PRESIGNED_URL_EXPIRES_IN
Params={'Bucket': FILES_BUCKET, 'Key': object_name}, )
ExpiresIn=PRESIGNED_URL_EXPIRES_IN)
dynamodb.update_item( dynamodb.update_item(
TableName=FILES_TABLE_NAME, TableName=FILES_TABLE_NAME,
Key={'id': {'S': entry_id}}, Key={"id": {"S": entry_id}},
UpdateExpression='SET deleted = :deleted', UpdateExpression="SET deleted = :deleted",
ExpressionAttributeValues={':deleted': {'BOOL': True}}) ExpressionAttributeValues={":deleted": {"BOOL": True}},
)
log.info(f'Entry {object_name} marked as deleted') log.info(f"Entry {object_name} marked as deleted")
return { return {"statusCode": 301, "headers": {"Location": download_url}}
'statusCode': 301,
'headers': {
'Location': download_url
}
}

View File

@@ -17,25 +17,25 @@ from botocore.exceptions import ClientError
def is_debug_enabled() -> bool: def is_debug_enabled() -> bool:
value = os.getenv('DEBUG', 'false').lower() value = os.getenv("DEBUG", "false").lower()
if value in ['false', '0']: if value in ["false", "0"]:
return False return False
else: else:
return bool(value) return bool(value)
DEBUG = is_debug_enabled() DEBUG = is_debug_enabled()
APP_URL = os.getenv('APP_URL') APP_URL = os.getenv("APP_URL")
EXPIRATION_TIMEOUT = int(os.getenv('EXPIRATION_TIMEOUT', 60*5)) EXPIRATION_TIMEOUT = int(os.getenv("EXPIRATION_TIMEOUT", 60 * 5))
FILES_BUCKET = os.getenv('FILES_BUCKET') FILES_BUCKET = os.getenv("FILES_BUCKET")
FILES_TABLE_NAME = os.getenv('FILES_TABLE_NAME') FILES_TABLE_NAME = os.getenv("FILES_TABLE_NAME")
S3_REGION_NAME = os.getenv('S3_REGION_NAME', 'eu-west-1') S3_REGION_NAME = os.getenv("S3_REGION_NAME", "eu-west-1")
S3_SIGNATURE_VERSION = os.getenv('S3_SIGNATURE_VERSION', 's3v4') S3_SIGNATURE_VERSION = os.getenv("S3_SIGNATURE_VERSION", "s3v4")
SECRET_KEY = base64.b64decode(os.getenv('SECRET_KEY')) SECRET_KEY = base64.b64decode(os.getenv("SECRET_KEY"))
SIGNATURE_HEADER = os.getenv('SIGNATURE_HEADER', 'x-once-signature') SIGNATURE_HEADER = os.getenv("SIGNATURE_HEADER", "x-once-signature")
SIGNATURE_TIME_TOLERANCE = int(os.getenv('SIGNATURE_TIME_TOLERANCE', 5)) SIGNATURE_TIME_TOLERANCE = int(os.getenv("SIGNATURE_TIME_TOLERANCE", 5))
TIMESTAMP_FORMAT_STRING = os.getenv('TIMESTAMP_FORMAT_STRING', '%d%m%Y%H%M%S') TIMESTAMP_FORMAT_STRING = os.getenv("TIMESTAMP_FORMAT_STRING", "%d%m%Y%H%M%S")
TIMESTAMP_PARAMETER_FORMAT = '%Y%m%d%H%M%S%f' TIMESTAMP_PARAMETER_FORMAT = "%Y%m%d%H%M%S%f"
log = logging.getLogger() log = logging.getLogger()
@@ -53,45 +53,38 @@ class UnauthorizedError(Exception):
pass pass
def create_presigned_post(bucket_name: str, object_name: str, def create_presigned_post(bucket_name: str, object_name: str, fields=None, conditions=None, expiration=3600) -> Dict:
fields=None, conditions=None, expiration=3600) -> Dict: """
'''
Generate a presigned URL S3 POST request to upload a file Generate a presigned URL S3 POST request to upload a file
''' """
s3_client = boto3.client('s3', s3_client = boto3.client("s3", region_name=S3_REGION_NAME, config=Config(signature_version=S3_SIGNATURE_VERSION))
region_name=S3_REGION_NAME,
config=Config(signature_version=S3_SIGNATURE_VERSION))
return s3_client.generate_presigned_post( return s3_client.generate_presigned_post(
bucket_name, object_name, bucket_name, object_name, Fields=fields, Conditions=conditions, ExpiresIn=expiration
Fields=fields, )
Conditions=conditions,
ExpiresIn=expiration)
def validate_signature(event: Dict, secret_key: bytes) -> bool: def validate_signature(event: Dict, secret_key: bytes) -> bool:
canonicalized_url = event['rawPath'] canonicalized_url = event["rawPath"]
if 'queryStringParameters' in event: if "queryStringParameters" in event:
qs = urlencode(event['queryStringParameters'], quote_via=quote_plus) qs = urlencode(event["queryStringParameters"], quote_via=quote_plus)
canonicalized_url = f'{canonicalized_url}?{qs}' canonicalized_url = f"{canonicalized_url}?{qs}"
plain_text = canonicalized_url.encode('utf-8') plain_text = canonicalized_url.encode("utf-8")
log.debug(f'Plain text: {plain_text}') log.debug(f"Plain text: {plain_text}")
encoded_signature = event['headers'][SIGNATURE_HEADER] encoded_signature = event["headers"][SIGNATURE_HEADER]
log.debug(f'Received signature: {encoded_signature}') log.debug(f"Received signature: {encoded_signature}")
signature_value = base64.b64decode(encoded_signature) signature_value = base64.b64decode(encoded_signature)
hmac_obj = hmac.new(secret_key, hmac_obj = hmac.new(secret_key, msg=plain_text, digestmod=hashlib.sha256)
msg=plain_text,
digestmod=hashlib.sha256)
calculated_signature = hmac_obj.digest() calculated_signature = hmac_obj.digest()
return calculated_signature == signature_value return calculated_signature == signature_value
def validate_timestamp(timestamp: str, current_time: datetime=None) -> bool: def validate_timestamp(timestamp: str, current_time: datetime = None) -> bool:
if current_time is None: if current_time is None:
current_time = datetime.utcnow() current_time = datetime.utcnow()
@@ -99,68 +92,63 @@ def validate_timestamp(timestamp: str, current_time: datetime=None) -> bool:
file_loading_time = datetime.strptime(timestamp, TIMESTAMP_PARAMETER_FORMAT) file_loading_time = datetime.strptime(timestamp, TIMESTAMP_PARAMETER_FORMAT)
return current_time - file_loading_time <= timedelta(seconds=SIGNATURE_TIME_TOLERANCE) return current_time - file_loading_time <= timedelta(seconds=SIGNATURE_TIME_TOLERANCE)
except: except:
log.error(f'Could not validate timestamp {timestamp} according to the format: {TIMESTAMP_PARAMETER_FORMAT}') log.error(f"Could not validate timestamp {timestamp} according to the format: {TIMESTAMP_PARAMETER_FORMAT}")
return False return False
def on_event(event, context): def on_event(event, context):
log.debug(f'Event received: {event}') log.debug(f"Event received: {event}")
log.debug(f'Context is: {context}') log.debug(f"Context is: {context}")
log.debug(f'Requests library version: {requests.__version__}') log.debug(f"Requests library version: {requests.__version__}")
log.debug(f'Debug mode is {DEBUG}') log.debug(f"Debug mode is {DEBUG}")
log.debug(f'App URL is "{APP_URL}"') log.debug(f'App URL is "{APP_URL}"')
log.debug(f'Files bucket is "{FILES_BUCKET}"') log.debug(f'Files bucket is "{FILES_BUCKET}"')
log.debug(f'Files Dynamodb table name is "{FILES_TABLE_NAME}"') log.debug(f'Files Dynamodb table name is "{FILES_TABLE_NAME}"')
log.debug(f'S3 region name is: "{S3_REGION_NAME}"') log.debug(f'S3 region name is: "{S3_REGION_NAME}"')
log.debug(f'S3 signature algorithm version is "{S3_SIGNATURE_VERSION}"') log.debug(f'S3 signature algorithm version is "{S3_SIGNATURE_VERSION}"')
log.debug(f'Pre-signed urls will expire after {EXPIRATION_TIMEOUT} seconds') log.debug(f"Pre-signed urls will expire after {EXPIRATION_TIMEOUT} seconds")
q = event.get('queryStringParameters', {}) q = event.get("queryStringParameters", {})
filename = unquote_plus(q.get('f')) filename = unquote_plus(q.get("f"))
timestamp = unquote_plus(q.get('t')) timestamp = unquote_plus(q.get("t"))
response_code = 200 response_code = 200
response = {} response = {}
try: try:
if filename is None: if filename is None:
raise BadRequestError('Provide a valid value for the `f` query parameter') raise BadRequestError("Provide a valid value for the `f` query parameter")
if timestamp is None: if timestamp is None:
raise BadRequestError('Please provide a valid value for the `t` query parameter') raise BadRequestError("Please provide a valid value for the `t` query parameter")
if not validate_timestamp(timestamp): if not validate_timestamp(timestamp):
log.error('Request timestamp is not valid') log.error("Request timestamp is not valid")
raise UnauthorizedError('Your request cannot be authorized') raise UnauthorizedError("Your request cannot be authorized")
if not validate_signature(event, SECRET_KEY): if not validate_signature(event, SECRET_KEY):
log.error('Request signature is not valid') log.error("Request signature is not valid")
raise UnauthorizedError('Your request cannot be authorized') raise UnauthorizedError("Your request cannot be authorized")
domain = string.ascii_uppercase + string.ascii_lowercase + string.digits domain = string.ascii_uppercase + string.ascii_lowercase + string.digits
entry_id = ''.join(random.choice(domain) for _ in range(6)) entry_id = "".join(random.choice(domain) for _ in range(6))
object_name = f'{entry_id}/{filename}' object_name = f"{entry_id}/{filename}"
response['once_url'] = f'{APP_URL}{entry_id}/{quote(filename)}' response["once_url"] = f"{APP_URL}{entry_id}/{quote(filename)}"
dynamodb = boto3.client('dynamodb') dynamodb = boto3.client("dynamodb")
dynamodb.put_item( dynamodb.put_item(TableName=FILES_TABLE_NAME, Item={"id": {"S": entry_id}, "object_name": {"S": object_name}})
TableName=FILES_TABLE_NAME,
Item={
'id': {'S': entry_id},
'object_name': {'S': object_name}
})
log.debug(f'Creating pre-signed post for {object_name} on ' log.debug(
f'{FILES_BUCKET} (expiration={EXPIRATION_TIMEOUT})') f"Creating pre-signed post for {object_name} on " f"{FILES_BUCKET} (expiration={EXPIRATION_TIMEOUT})"
)
presigned_post = create_presigned_post( presigned_post = create_presigned_post(
bucket_name=FILES_BUCKET, bucket_name=FILES_BUCKET, object_name=object_name, expiration=EXPIRATION_TIMEOUT
object_name=object_name, )
expiration=EXPIRATION_TIMEOUT)
log.info(f'Authorized upload request for {object_name}') log.info(f"Authorized upload request for {object_name}")
log.debug(f'Presigned-Post response: {presigned_post}') log.debug(f"Presigned-Post response: {presigned_post}")
response['presigned_post'] = presigned_post response["presigned_post"] = presigned_post
except BadRequestError as e: except BadRequestError as e:
response_code = 400 response_code = 400
response = dict(message=str(e)) response = dict(message=str(e))
@@ -172,7 +160,7 @@ def on_event(event, context):
response = dict(message=str(e)) response = dict(message=str(e))
finally: finally:
return { return {
'statusCode': response_code, "statusCode": response_code,
'headers': {'Content-Type': 'application/json'}, "headers": {"Content-Type": "application/json"},
'body': json.dumps(response) "body": json.dumps(response),
} }

View File

@@ -2,9 +2,10 @@ import os
from typing import Optional from typing import Optional
import jsii import jsii
from aws_cdk import( from aws_cdk import (
core, core,
aws_apigatewayv2 as apigw, aws_apigatewayv2 as apigw,
aws_apigatewayv2_integrations as integrations,
aws_certificatemanager as certmgr, aws_certificatemanager as certmgr,
aws_cloudformation as cfn, aws_cloudformation as cfn,
aws_dynamodb as dynamodb, aws_dynamodb as dynamodb,
@@ -14,13 +15,14 @@ from aws_cdk import(
aws_logs as logs, aws_logs as logs,
aws_route53 as route53, aws_route53 as route53,
aws_route53_targets as route53_targets, aws_route53_targets as route53_targets,
aws_s3 as s3) aws_s3 as s3,
)
from .utils import make_python_zip_bundle from .utils import make_python_zip_bundle
BASE_PATH = os.path.dirname(os.path.abspath(__file__)) BASE_PATH = os.path.dirname(os.path.abspath(__file__))
LOG_RETENTION = getattr(logs.RetentionDays, os.getenv('LOG_RETENTION', 'TWO_WEEKS')) LOG_RETENTION = getattr(logs.RetentionDays, os.getenv("LOG_RETENTION", "TWO_WEEKS"))
@jsii.implements(route53.IAliasRecordTarget) @jsii.implements(route53.IAliasRecordTarget)
@@ -28,149 +30,184 @@ class ApiGatewayV2Domain(object):
def __init__(self, domain_name: apigw.CfnDomainName): def __init__(self, domain_name: apigw.CfnDomainName):
self.domain_name = domain_name self.domain_name = domain_name
@jsii.member(jsii_name='bind') @jsii.member(jsii_name="bind")
def bind(self, _record: route53.IRecordSet) -> route53.AliasRecordTargetConfig: def bind(self, _record: route53.IRecordSet) -> route53.AliasRecordTargetConfig:
return { return {
'dnsName': self.domain_name.get_att('RegionalDomainName').to_string(), "dnsName": self.domain_name.get_att("RegionalDomainName").to_string(),
'hostedZoneId': self.domain_name.get_att('RegionalHostedZoneId').to_string() "hostedZoneId": self.domain_name.get_att("RegionalHostedZoneId").to_string(),
} }
class CustomDomainStack(cfn.NestedStack): class CustomDomainStack(cfn.NestedStack):
def __init__(self, scope: core.Construct, id: str, def __init__(
self,
scope: core.Construct,
id: str,
hosted_zone_id: str, hosted_zone_id: str,
hosted_zone_name: str, hosted_zone_name: str,
domain_name: str, domain_name: str,
api: apigw.HttpApi): api: apigw.HttpApi,
):
super().__init__(scope, id) super().__init__(scope, id)
hosted_zone = route53.HostedZone.from_hosted_zone_attributes(self, id='dns-hosted-zone', hosted_zone = route53.HostedZone.from_hosted_zone_attributes(
hosted_zone_id=hosted_zone_id, self, id="dns-hosted-zone", hosted_zone_id=hosted_zone_id, zone_name=hosted_zone_name
zone_name=hosted_zone_name) )
certificate = certmgr.DnsValidatedCertificate(self, 'tls-certificate', certificate = certmgr.DnsValidatedCertificate(
self,
"tls-certificate",
domain_name=domain_name, domain_name=domain_name,
hosted_zone=hosted_zone, hosted_zone=hosted_zone,
validation_method=certmgr.ValidationMethod.DNS) validation_method=certmgr.ValidationMethod.DNS,
)
custom_domain = apigw.CfnDomainName(self, 'custom-domain', custom_domain = apigw.CfnDomainName(
self,
"custom-domain",
domain_name=domain_name, domain_name=domain_name,
domain_name_configurations=[ domain_name_configurations=[
apigw.CfnDomainName.DomainNameConfigurationProperty( apigw.CfnDomainName.DomainNameConfigurationProperty(certificate_arn=certificate.certificate_arn)
certificate_arn=certificate.certificate_arn)]) ],
)
custom_domain.node.add_dependency(api) custom_domain.node.add_dependency(api)
custom_domain.node.add_dependency(certificate) custom_domain.node.add_dependency(certificate)
api_mapping = apigw.CfnApiMapping(self, 'custom-domain-mapping', api_mapping = apigw.CfnApiMapping(
api_id=api.http_api_id, self, "custom-domain-mapping", api_id=api.http_api_id, domain_name=domain_name, stage="$default"
domain_name=domain_name, )
stage='$default')
api_mapping.node.add_dependency(custom_domain) api_mapping.node.add_dependency(custom_domain)
route53.ARecord(self, 'custom-domain-record', route53.ARecord(
self,
"custom-domain-record",
target=route53.RecordTarget.from_alias(ApiGatewayV2Domain(custom_domain)), target=route53.RecordTarget.from_alias(ApiGatewayV2Domain(custom_domain)),
zone=hosted_zone, zone=hosted_zone,
record_name=domain_name) record_name=domain_name,
)
class OnceStack(core.Stack): class OnceStack(core.Stack):
def __init__(self, scope: core.Construct, id: str, def __init__(
self,
scope: core.Construct,
id: str,
secret_key: str, secret_key: str,
custom_domain: Optional[str] = None, custom_domain: Optional[str] = None,
hosted_zone_id: Optional[str] = None, hosted_zone_id: Optional[str] = None,
hosted_zone_name: Optional[str] = None, hosted_zone_name: Optional[str] = None,
**kwargs) -> None: **kwargs,
) -> None:
super().__init__(scope, id, **kwargs) super().__init__(scope, id, **kwargs)
self.files_bucket = s3.Bucket(self, 'files-bucket', self.files_bucket = s3.Bucket(
bucket_name='once-shared-files', self,
"files-bucket",
bucket_name="once-shared-files",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL, block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED, encryption=s3.BucketEncryption.S3_MANAGED,
removal_policy=core.RemovalPolicy.DESTROY) removal_policy=core.RemovalPolicy.DESTROY,
)
self.files_table = dynamodb.Table(self, 'once-files-table', self.files_table = dynamodb.Table(
table_name='once-files', self,
partition_key=dynamodb.Attribute(name='id', type=dynamodb.AttributeType.STRING), "once-files-table",
table_name="once-files",
partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST,
removal_policy=core.RemovalPolicy.DESTROY) removal_policy=core.RemovalPolicy.DESTROY,
)
self.api = apigw.HttpApi(self, 'once-api', api_name='once-api') self.api = apigw.HttpApi(self, "once-api", api_name="once-api")
api_url = self.api.url api_url = self.api.url
if custom_domain is not None: if custom_domain is not None:
api_url = f'https://{custom_domain}/' api_url = f"https://{custom_domain}/"
core.CfnOutput(self, 'base-url', value=api_url) core.CfnOutput(self, "base-url", value=api_url)
self.get_upload_ticket_function = lambda_.Function(self, 'get-upload-ticket-function', self.get_upload_ticket_function = lambda_.Function(
function_name='once-get-upload-ticket', self,
description='Returns a pre-signed request to share a file', "get-upload-ticket-function",
function_name="once-get-upload-ticket",
description="Returns a pre-signed request to share a file",
runtime=lambda_.Runtime.PYTHON_3_7, runtime=lambda_.Runtime.PYTHON_3_7,
code=make_python_zip_bundle(os.path.join(BASE_PATH, 'get-upload-ticket')), code=make_python_zip_bundle(os.path.join(BASE_PATH, "get-upload-ticket")),
handler='handler.on_event', handler="handler.on_event",
log_retention=LOG_RETENTION, log_retention=LOG_RETENTION,
environment={ environment={
'APP_URL': api_url, "APP_URL": api_url,
'FILES_TABLE_NAME': self.files_table.table_name, "FILES_TABLE_NAME": self.files_table.table_name,
'FILES_BUCKET': self.files_bucket.bucket_name, "FILES_BUCKET": self.files_bucket.bucket_name,
'SECRET_KEY': secret_key "SECRET_KEY": secret_key,
}) },
)
self.files_bucket.grant_put(self.get_upload_ticket_function) self.files_bucket.grant_put(self.get_upload_ticket_function)
self.files_table.grant_read_write_data(self.get_upload_ticket_function) self.files_table.grant_read_write_data(self.get_upload_ticket_function)
self.download_and_delete_function = lambda_.Function(self, 'download-and-delete-function', self.download_and_delete_function = lambda_.Function(
function_name='once-download-and-delete', self,
description='Serves a file from S3 and deletes it as soon as it has been successfully transferred', "download-and-delete-function",
function_name="once-download-and-delete",
description="Serves a file from S3 and deletes it as soon as it has been successfully transferred",
runtime=lambda_.Runtime.PYTHON_3_7, runtime=lambda_.Runtime.PYTHON_3_7,
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, 'download-and-delete')), code=lambda_.Code.from_asset(os.path.join(BASE_PATH, "download-and-delete")),
handler='handler.on_event', handler="handler.on_event",
log_retention=LOG_RETENTION, log_retention=LOG_RETENTION,
environment={ environment={
'FILES_BUCKET': self.files_bucket.bucket_name, "FILES_BUCKET": self.files_bucket.bucket_name,
'FILES_TABLE_NAME': self.files_table.table_name "FILES_TABLE_NAME": self.files_table.table_name,
}) },
)
self.files_bucket.grant_read(self.download_and_delete_function) self.files_bucket.grant_read(self.download_and_delete_function)
self.files_bucket.grant_delete(self.download_and_delete_function) self.files_bucket.grant_delete(self.download_and_delete_function)
self.files_table.grant_read_write_data(self.download_and_delete_function) self.files_table.grant_read_write_data(self.download_and_delete_function)
get_upload_ticket_integration = apigw.LambdaProxyIntegration(handler=self.get_upload_ticket_function) get_upload_ticket_integration = integrations.LambdaProxyIntegration(handler=self.get_upload_ticket_function)
self.api.add_routes( self.api.add_routes(path="/", methods=[apigw.HttpMethod.GET], integration=get_upload_ticket_integration)
path='/',
methods=[apigw.HttpMethod.GET],
integration=get_upload_ticket_integration)
download_and_delete_integration = apigw.LambdaProxyIntegration(handler=self.download_and_delete_function) download_and_delete_integration = integrations.LambdaProxyIntegration(
handler=self.download_and_delete_function
)
self.api.add_routes( self.api.add_routes(
path='/{entry_id}/{filename}', path="/{entry_id}/{filename}", methods=[apigw.HttpMethod.GET], integration=download_and_delete_integration
methods=[apigw.HttpMethod.GET], )
integration=download_and_delete_integration)
self.cleanup_function = lambda_.Function(self, 'delete-served-files-function', self.cleanup_function = lambda_.Function(
function_name='once-delete-served-files', self,
description='Deletes files from S3 once they have been marked as deleted in DynamoDB', "delete-served-files-function",
function_name="once-delete-served-files",
description="Deletes files from S3 once they have been marked as deleted in DynamoDB",
runtime=lambda_.Runtime.PYTHON_3_7, runtime=lambda_.Runtime.PYTHON_3_7,
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, 'delete-served-files')), code=lambda_.Code.from_asset(os.path.join(BASE_PATH, "delete-served-files")),
handler='handler.on_event', handler="handler.on_event",
log_retention=LOG_RETENTION, log_retention=LOG_RETENTION,
environment={ environment={
'FILES_BUCKET': self.files_bucket.bucket_name, "FILES_BUCKET": self.files_bucket.bucket_name,
'FILES_TABLE_NAME': self.files_table.table_name "FILES_TABLE_NAME": self.files_table.table_name,
}) },
)
self.files_bucket.grant_delete(self.cleanup_function) self.files_bucket.grant_delete(self.cleanup_function)
self.files_table.grant_read_write_data(self.cleanup_function) self.files_table.grant_read_write_data(self.cleanup_function)
events.Rule(self, 'once-delete-served-files-rule', events.Rule(
self,
"once-delete-served-files-rule",
schedule=events.Schedule.rate(core.Duration.hours(24)), schedule=events.Schedule.rate(core.Duration.hours(24)),
targets=[targets.LambdaFunction(self.cleanup_function)]) targets=[targets.LambdaFunction(self.cleanup_function)],
)
if custom_domain is not None: if custom_domain is not None:
self.custom_domain_stack = CustomDomainStack(self, 'custom-domain', self.custom_domain_stack = CustomDomainStack(
self,
"custom-domain",
api=self.api, api=self.api,
domain_name=custom_domain, domain_name=custom_domain,
hosted_zone_id=hosted_zone_id, hosted_zone_id=hosted_zone_id,
hosted_zone_name=hosted_zone_name) hosted_zone_name=hosted_zone_name,
)

View File

@@ -12,22 +12,24 @@ from aws_cdk import aws_lambda as _lambda
class MissingPrerequisiteCommand(Exception): class MissingPrerequisiteCommand(Exception):
'''A required system command is missing''' """A required system command is missing"""
def add_folder_to_zip(zip_obj: zipfile.ZipFile, folder: str, ignore_names: List[str] = [], ignore_dotfiles: bool = True): def add_folder_to_zip(
zip_obj: zipfile.ZipFile, folder: str, ignore_names: List[str] = [], ignore_dotfiles: bool = True
):
for root, dirs, files in os.walk(folder): for root, dirs, files in os.walk(folder):
if ignore_dotfiles: if ignore_dotfiles:
dirs[:] = [d for d in dirs if not d.startswith('.')] dirs[:] = [d for d in dirs if not d.startswith(".")]
files[:] = [f for f in files if not f.startswith('.')] files[:] = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if d not in ignore_names] dirs[:] = [d for d in dirs if d not in ignore_names]
files[:] = [f for f in files if f not in ignore_names] files[:] = [f for f in files if f not in ignore_names]
logging.debug(f'FILES: {files}, DIRS: {dirs}') logging.debug(f"FILES: {files}, DIRS: {dirs}")
if root == folder: if root == folder:
archive_folder_name = '' archive_folder_name = ""
else: else:
archive_folder_name = os.path.relpath(root, folder) archive_folder_name = os.path.relpath(root, folder)
zip_obj.write(root, arcname=archive_folder_name) zip_obj.write(root, arcname=archive_folder_name)
@@ -38,86 +40,96 @@ def add_folder_to_zip(zip_obj: zipfile.ZipFile, folder: str, ignore_names: List[
zip_obj.write(f, arcname=d) zip_obj.write(f, arcname=d)
def execute_shell_command(command: Union[str, List[str]], def execute_shell_command(command: Union[str, List[str]], env: Union[Dict, None] = None) -> str:
env: Union[Dict, None] = None) -> str:
if isinstance(command, list): if isinstance(command, list):
command = ' '.join(command) command = " ".join(command)
logging.debug(f'Executing command: {command}') logging.debug(f"Executing command: {command}")
completed_process = subprocess.run(command, completed_process = subprocess.run(
env=env, command, env=env, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
shell=True, )
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
logging.debug(completed_process) logging.debug(completed_process)
return completed_process.stdout.strip().decode('utf-8') return completed_process.stdout.strip().decode("utf-8")
def locate_command(command: str) -> str: def locate_command(command: str) -> str:
path = execute_shell_command(['which', command]) path = execute_shell_command(["which", command])
if path is None: if path is None:
raise MissingPrerequisiteCommand(f'Unable to find "{command}"') raise MissingPrerequisiteCommand(f'Unable to find "{command}"')
return path return path
def make_python_zip_bundle(input_path: str, def make_python_zip_bundle(
python_version: str = '3.7', input_path: str,
build_folder: str = '.build', python_version: str = "3.7",
requirements_file: str = 'requirements.txt', build_folder: str = ".build",
output_bundle_name: str = 'bundle.zip') -> _lambda.AssetCode: requirements_file: str = "requirements.txt",
''' output_bundle_name: str = "bundle.zip",
) -> _lambda.AssetCode:
"""
Builds an lambda AssetCode bundling python dependencies along with the code. Builds an lambda AssetCode bundling python dependencies along with the code.
The bundle is built using docker and the target lambda runtime image. The bundle is built using docker and the target lambda runtime image.
''' """
build_path = os.path.abspath(os.path.join(input_path, build_folder)) build_path = os.path.abspath(os.path.join(input_path, build_folder))
asset_path = os.path.join(build_path, output_bundle_name) asset_path = os.path.join(build_path, output_bundle_name)
# checks if it's required to build a new zip file # checks if it's required to build a new zip file
if not os.path.exists(asset_path) or os.path.getmtime(asset_path) < get_folder_latest_mtime(input_path): if not os.path.exists(asset_path) or os.path.getmtime(asset_path) < get_folder_latest_mtime(input_path):
docker = locate_command('docker') docker = locate_command("docker")
lambda_runtime_docker_image = f'lambci/lambda:build-python{python_version}' lambda_runtime_docker_image = f"lambci/lambda:build-python{python_version}"
# cleans the target folder # cleans the target folder
logging.debug(f'Cleaning folder: {build_path}') logging.debug(f"Cleaning folder: {build_path}")
shutil.rmtree(build_path, ignore_errors=True) shutil.rmtree(build_path, ignore_errors=True)
# builds requirements using target runtime # builds requirements using target runtime
build_log = execute_shell_command(command=[ build_log = execute_shell_command(
'docker', 'run', '--rm', command=[
'-v', f'{input_path}:/app', "docker",
'-w', '/app', "run",
"--rm",
"-v",
f"{input_path}:/app",
"-w",
"/app",
lambda_runtime_docker_image, lambda_runtime_docker_image,
'pip', 'install', "pip",
'-r', requirements_file, "install",
'-t', build_folder]) "-r",
requirements_file,
"-t",
build_folder,
]
)
logging.info(build_log) logging.info(build_log)
# creates the zip archive # creates the zip archive
logging.debug(f'Deleting file: {asset_path}') logging.debug(f"Deleting file: {asset_path}")
shutil.rmtree(asset_path, ignore_errors=True) shutil.rmtree(asset_path, ignore_errors=True)
logging.debug(f'Creating bundle: {asset_path}') logging.debug(f"Creating bundle: {asset_path}")
with zipfile.ZipFile(asset_path, 'w', zipfile.ZIP_DEFLATED) as zip_obj: with zipfile.ZipFile(asset_path, "w", zipfile.ZIP_DEFLATED) as zip_obj:
add_folder_to_zip(zip_obj, input_path, ignore_names=[output_bundle_name, '__pycache__']) add_folder_to_zip(zip_obj, input_path, ignore_names=[output_bundle_name, "__pycache__"])
add_folder_to_zip(zip_obj, build_path, ignore_names=[output_bundle_name, '__pycache__'], ignore_dotfiles=False) add_folder_to_zip(
zip_obj, build_path, ignore_names=[output_bundle_name, "__pycache__"], ignore_dotfiles=False
)
logging.info(f'Lambda bundle created at {asset_path}') logging.info(f"Lambda bundle created at {asset_path}")
source_hash = get_folder_checksum(input_path) source_hash = get_folder_checksum(input_path)
logging.debug(f'Source folder hash {input_path} -> {source_hash}') logging.debug(f"Source folder hash {input_path} -> {source_hash}")
return _lambda.AssetCode.from_asset(asset_path, source_hash=source_hash) return _lambda.AssetCode.from_asset(asset_path, source_hash=source_hash)
def get_folder_checksum(path: str, ignore_dotfiles: bool = True, def get_folder_checksum(
chunk_size: int = 4096, path: str, ignore_dotfiles: bool = True, chunk_size: int = 4096, digest_method: hashlib._hashlib.HASH = hashlib.md5
digest_method: hashlib._hashlib.HASH = hashlib.md5) -> str: ) -> str:
def _hash_file(filename: str) -> bytes: def _hash_file(filename: str) -> bytes:
with open(filename, mode='rb', buffering=0) as fp: with open(filename, mode="rb", buffering=0) as fp:
hash_func = digest_method() hash_func = digest_method()
buffer = fp.read(chunk_size) buffer = fp.read(chunk_size)
while len(buffer) > 0: while len(buffer) > 0:
@@ -125,10 +137,10 @@ def get_folder_checksum(path: str, ignore_dotfiles: bool = True,
buffer = fp.read(chunk_size) buffer = fp.read(chunk_size)
return hash_func.digest() return hash_func.digest()
folder_hash = b'' folder_hash = b""
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
files = [f for f in files if not f.startswith('.')] files = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if not d.startswith('.')] dirs[:] = [d for d in dirs if not d.startswith(".")]
for file_name in sorted(files): for file_name in sorted(files):
file_path = os.path.join(root, file_name) file_path = os.path.join(root, file_name)
@@ -142,8 +154,8 @@ def get_folder_latest_mtime(path: str, ignore_dotfiles: bool = True) -> float:
latest_mtime = None latest_mtime = None
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
if ignore_dotfiles: if ignore_dotfiles:
files = [f for f in files if not f.startswith('.')] files = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if not d.startswith('.')] dirs[:] = [d for d in dirs if not d.startswith(".")]
for file_name in files: for file_name in files:
file_path = os.path.join(root, file_name) file_path = os.path.join(root, file_name)

1374
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

34
pyproject.toml Normal file
View File

@@ -0,0 +1,34 @@
[tool.poetry]
name = "once"
version = "0.2.0"
description = "A one-time file sharing personal service, running serverless on AWS"
authors = ["Domenico Testa"]
license = "MIT"
packages = [
{ include = "client/*.py" },
]
[tool.poetry.dependencies]
python = "~3.8"
"click" = "^7.1"
"requests" = "^2.24"
"Pygments" = "^2.6"
"aws-cdk.aws-apigatewayv2-integrations" = "^1.74.0"
"aws-cdk.core" = "^1.74"
"aws-cdk.aws-apigatewayv2" = "^1.74"
"aws-cdk.aws-dynamodb" = "^1.74"
"aws-cdk.aws-lambda" = "^1.74"
"aws-cdk.aws-s3" = "^1.74"
"aws-cdk.aws-certificatemanager" = "^1.74"
"aws-cdk.aws-cloudformation" = "^1.74"
"aws-cdk.aws-events" = "^1.74"
"aws-cdk.aws-events-targets" = "^1.74"
"aws-cdk.aws-logs" = "^1.74"
"aws-cdk.aws-route53" = "^1.74"
[tool.poetry.scripts]
once = 'client:share'
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

View File

@@ -1,12 +0,0 @@
-e .
aws_cdk.core>=1.45
aws_cdk.aws_apigatewayv2>=1.45
aws_cdk.aws_certificatemanager>=1.45
aws_cdk.aws_cloudformation>=1.45
aws_cdk.aws_dynamodb>=1.45
aws_cdk.aws_events>=1.45
aws_cdk.aws_events_targets>=1.45
aws_cdk.aws_lambda>=1.45
aws_cdk.aws_logs>=1.45
aws_cdk.aws_route53>=1.45
aws_cdk.aws_s3>=1.45

View File

@@ -1,48 +0,0 @@
import setuptools
with open("README.md") as fp:
long_description = fp.read()
setuptools.setup(
name="once",
description="A one-time file sharing personal service, running serverless on AWS",
version="0.1.0",
url="https://github.com/domtes/once",
author="Domenico Testa",
author_email="domenico.testa@gmail.com",
long_description=long_description,
long_description_content_type="text/markdown",
python_requires=">=3.6",
install_requires=[
"click",
"pygments",
"requests"
],
package_dir={'': 'client'},
py_modules=['once'],
entry_points={
'console_scripts': ['once=once:share']
},
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: File Sharing",
"Topic :: Utilities",
"Typing :: Typed",
],
)

View File

@@ -1,13 +0,0 @@
@echo off
rem The sole purpose of this script is to make the command
rem
rem source .env/bin/activate
rem
rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows.
rem On Windows, this command just runs this batch file (the argument is ignored).
rem
rem Now we don't need to document a Windows command for activating a virtualenv.
echo Executing .env\Scripts\activate.bat for you
.env\Scripts\activate.bat