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:
pip install -r requirements.txt
poetry install
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
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 <file_toshare>
poetry run once <file_toshare>
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
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')
CUSTOM_DOMAIN = os.getenv('CUSTOM_DOMAIN')
HOSTED_ZONE_NAME = os.getenv('HOSTED_ZONE_NAME')
HOSTED_ZONE_ID = os.getenv('HOSTED_ZONE_ID')
SECRET_KEY = os.getenv("SECRET_KEY")
CUSTOM_DOMAIN = os.getenv("CUSTOM_DOMAIN")
HOSTED_ZONE_NAME = os.getenv("HOSTED_ZONE_NAME")
HOSTED_ZONE_ID = os.getenv("HOSTED_ZONE_ID")
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,
hosted_zone_name: str = None,
hosted_zone_id: str = None) -> configparser.ConfigParser:
hosted_zone_id: str = None,
) -> configparser.ConfigParser:
config = configparser.ConfigParser()
config['once'] = {
'secret_key': secret_key or generate_random_key(),
config["once"] = {
"secret_key": secret_key or generate_random_key(),
}
config['deployment'] = {}
config["deployment"] = {}
if all([custom_domain, hosted_zone_name, hosted_zone_id]):
config['once']['base_url'] = f'https://{custom_domain}'
config['deployment'] = {
'custom_domain': custom_domain,
'hosted_zone_name': hosted_zone_name,
'hosted_zone_id': hosted_zone_id
config["once"]["base_url"] = f"https://{custom_domain}"
config["deployment"] = {
"custom_domain": custom_domain,
"hosted_zone_name": hosted_zone_name,
"hosted_zone_id": hosted_zone_id,
}
return config
def get_config(config_gile: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser:
if not os.path.exists(ONCE_CONFIG_FILE):
print(f'Generating configuration file at {ONCE_CONFIG_FILE}')
with open(ONCE_CONFIG_FILE, 'w') as config_file:
print(f"Generating configuration file at {ONCE_CONFIG_FILE}")
with open(ONCE_CONFIG_FILE, "w") as config_file:
config = generate_config(
secret_key=SECRET_KEY,
custom_domain=CUSTOM_DOMAIN,
hosted_zone_name=HOSTED_ZONE_NAME,
hosted_zone_id=HOSTED_ZONE_ID)
hosted_zone_id=HOSTED_ZONE_ID,
)
config.write(config_file)
else:
config = configparser.ConfigParser()
@@ -61,14 +64,14 @@ def get_config(config_gile: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser
def main():
config = get_config()
kwargs = {'secret_key': config['once']['secret_key']}
if config.has_section('deployment'):
kwargs.update(config['deployment'])
kwargs = {"secret_key": config["once"]["secret_key"]}
if config.has_section("deployment"):
kwargs.update(config["deployment"])
app = core.App()
once = OnceStack(app, 'once', **kwargs)
once = OnceStack(app, "once", **kwargs)
app.synth()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,6 @@
'''
"""
Simple command to share one-time files
'''
"""
import os
import base64
@@ -17,9 +17,9 @@ import requests
from pygments import highlight, lexers, formatters
ONCE_CONFIG_FILE = os.getenv('ONCE_CONFIG_FILE', os.path.expanduser('~/.once'))
ONCE_SIGNATURE_HEADER = 'x-once-signature'
ONCE_TIMESTAMP_FORMAT = '%Y%m%d%H%M%S%f'
ONCE_CONFIG_FILE = os.getenv("ONCE_CONFIG_FILE", os.path.expanduser("~/.once"))
ONCE_SIGNATURE_HEADER = "x-once-signature"
ONCE_TIMESTAMP_FORMAT = "%Y%m%d%H%M%S%f"
def highlight_json(obj):
@@ -33,7 +33,7 @@ def echo_obj(obj):
def get_config(config_file: str = ONCE_CONFIG_FILE) -> configparser.ConfigParser:
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.read(ONCE_CONFIG_FILE)
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):
config = get_config()
if not config.has_option('once', 'base_url'):
raise ValueError(f'Configuration file at {ONCE_CONFIG_FILE} misses `base_url` option')
if not config.has_option("once", "base_url"):
raise ValueError(f"Configuration file at {ONCE_CONFIG_FILE} misses `base_url` option")
base_url = os.getenv('ONCE_API_URL', config['once']['base_url'])
secret_key = base64.b64decode(os.getenv('ONCE_SECRET_KEY', config['once']['secret_key']))
base_url = os.getenv("ONCE_API_URL", config["once"]["base_url"])
secret_key = base64.b64decode(os.getenv("ONCE_SECRET_KEY", config["once"]["secret_key"]))
method = method.lower()
if method not in ['get', 'post']:
if method not in ["get", "post"]:
raise ValueError(f'Unsupported HTTP method "{method}"')
actual_url = urljoin(base_url, url)
if verbose:
print(f'{method.upper()} {actual_url}')
print(f"{method.upper()} {actual_url}")
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)
req.headers[ONCE_SIGNATURE_HEADER] = base64.b64encode(hmac_obj.digest())
response = requests.Session().send(req)
if verbose:
print(f'Server response status: {response.status_code}')
print(f"Server response status: {response.status_code}")
echo_obj(response.json())
return response
@click.command('share')
@click.argument('file', type=click.File(mode='rb'), required=True)
@click.option('--verbose', '-v', is_flag=True, default=False, help='Enables verbose output.')
@click.command("share")
@click.argument("file", type=click.File(mode="rb"), required=True)
@click.option("--verbose", "-v", is_flag=True, default=False, help="Enables verbose output.")
def share(file: click.File, verbose: bool):
entry = api_req('GET', '/',
params={
'f': quote_plus(os.path.basename(file.name)),
't': datetime.utcnow().strftime(ONCE_TIMESTAMP_FORMAT)
},
verbose=verbose).json()
entry = api_req(
"GET",
"/",
params={"f": quote_plus(os.path.basename(file.name)), "t": datetime.utcnow().strftime(ONCE_TIMESTAMP_FORMAT)},
verbose=verbose,
).json()
once_url = entry['once_url']
upload_data = entry['presigned_post']
files = {'file': file}
once_url = entry["once_url"]
upload_data = entry["presigned_post"]
files = {"file": file}
upload_started = time.time()
response = requests.post(upload_data['url'],
data=upload_data['fields'],
files=files)
response = requests.post(upload_data["url"], data=upload_data["fields"], files=files)
upload_time = time.time() - upload_started
print(f"File uploaded in {upload_time}s")
print(f"File can be downloaded once at: {once_url}")
if __name__ == '__main__':
if __name__ == "__main__":
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:
value = os.getenv('DEBUG', 'false').lower()
if value in ['false', '0']:
value = os.getenv("DEBUG", "false").lower()
if value in ["false", "0"]:
return False
else:
return bool(value)
DEBUG = is_debug_enabled()
FILES_BUCKET = os.getenv('FILES_BUCKET')
FILES_TABLE_NAME = os.getenv('FILES_TABLE_NAME')
FILES_BUCKET = os.getenv("FILES_BUCKET")
FILES_TABLE_NAME = os.getenv("FILES_TABLE_NAME")
log = logging.getLogger()
@@ -29,28 +29,27 @@ else:
def on_event(event, context):
log.debug(f'Event received: {event}')
log.debug(f'Context is: {context}')
log.debug(f'Debug mode is {DEBUG}')
log.debug(f"Event received: {event}")
log.debug(f"Context is: {context}")
log.debug(f"Debug mode is {DEBUG}")
log.debug(f'Files bucket is "{FILES_BUCKET}"')
dynamodb = boto3.client('dynamodb')
dynamodb = boto3.client("dynamodb")
response = dynamodb.scan(
TableName=FILES_TABLE_NAME,
Select='ALL_ATTRIBUTES',
FilterExpression='deleted = :deleted',
ExpressionAttributeValues={
':deleted': {'BOOL': True}
})
Select="ALL_ATTRIBUTES",
FilterExpression="deleted = :deleted",
ExpressionAttributeValues={":deleted": {"BOOL": True}},
)
s3 = boto3.client('s3')
for item in response['Items']:
object_name = item['object_name']['S']
log.info(f'Deleting file {object_name}')
s3 = boto3.client("s3")
for item in response["Items"]:
object_name = item["object_name"]["S"]
log.info(f"Deleting file {object_name}")
try:
s3.delete_object(Bucket=FILES_BUCKET, Key=object_name)
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']})
log.debug(f'dynamodb delete item: {response}')
response = dynamodb.delete_item(TableName=FILES_TABLE_NAME, Key={"id": item["id"]})
log.debug(f"dynamodb delete item: {response}")

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import jsii
from aws_cdk import (
core,
aws_apigatewayv2 as apigw,
aws_apigatewayv2_integrations as integrations,
aws_certificatemanager as certmgr,
aws_cloudformation as cfn,
aws_dynamodb as dynamodb,
@@ -14,13 +15,14 @@ from aws_cdk import(
aws_logs as logs,
aws_route53 as route53,
aws_route53_targets as route53_targets,
aws_s3 as s3)
aws_s3 as s3,
)
from .utils import make_python_zip_bundle
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)
@@ -28,149 +30,184 @@ class ApiGatewayV2Domain(object):
def __init__(self, domain_name: apigw.CfnDomainName):
self.domain_name = domain_name
@jsii.member(jsii_name='bind')
@jsii.member(jsii_name="bind")
def bind(self, _record: route53.IRecordSet) -> route53.AliasRecordTargetConfig:
return {
'dnsName': self.domain_name.get_att('RegionalDomainName').to_string(),
'hostedZoneId': self.domain_name.get_att('RegionalHostedZoneId').to_string()
"dnsName": self.domain_name.get_att("RegionalDomainName").to_string(),
"hostedZoneId": self.domain_name.get_att("RegionalHostedZoneId").to_string(),
}
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_name: str,
domain_name: str,
api: apigw.HttpApi):
api: apigw.HttpApi,
):
super().__init__(scope, id)
hosted_zone = route53.HostedZone.from_hosted_zone_attributes(self, id='dns-hosted-zone',
hosted_zone_id=hosted_zone_id,
zone_name=hosted_zone_name)
hosted_zone = route53.HostedZone.from_hosted_zone_attributes(
self, id="dns-hosted-zone", hosted_zone_id=hosted_zone_id, zone_name=hosted_zone_name
)
certificate = certmgr.DnsValidatedCertificate(self, 'tls-certificate',
certificate = certmgr.DnsValidatedCertificate(
self,
"tls-certificate",
domain_name=domain_name,
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_configurations=[
apigw.CfnDomainName.DomainNameConfigurationProperty(
certificate_arn=certificate.certificate_arn)])
apigw.CfnDomainName.DomainNameConfigurationProperty(certificate_arn=certificate.certificate_arn)
],
)
custom_domain.node.add_dependency(api)
custom_domain.node.add_dependency(certificate)
api_mapping = apigw.CfnApiMapping(self, 'custom-domain-mapping',
api_id=api.http_api_id,
domain_name=domain_name,
stage='$default')
api_mapping = apigw.CfnApiMapping(
self, "custom-domain-mapping", api_id=api.http_api_id, domain_name=domain_name, stage="$default"
)
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)),
zone=hosted_zone,
record_name=domain_name)
record_name=domain_name,
)
class OnceStack(core.Stack):
def __init__(self, scope: core.Construct, id: str,
def __init__(
self,
scope: core.Construct,
id: str,
secret_key: str,
custom_domain: Optional[str] = None,
hosted_zone_id: Optional[str] = None,
hosted_zone_name: Optional[str] = None,
**kwargs) -> None:
**kwargs,
) -> None:
super().__init__(scope, id, **kwargs)
self.files_bucket = s3.Bucket(self, 'files-bucket',
bucket_name='once-shared-files',
self.files_bucket = s3.Bucket(
self,
"files-bucket",
bucket_name="once-shared-files",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED,
removal_policy=core.RemovalPolicy.DESTROY)
removal_policy=core.RemovalPolicy.DESTROY,
)
self.files_table = dynamodb.Table(self, 'once-files-table',
table_name='once-files',
partition_key=dynamodb.Attribute(name='id', type=dynamodb.AttributeType.STRING),
self.files_table = dynamodb.Table(
self,
"once-files-table",
table_name="once-files",
partition_key=dynamodb.Attribute(name="id", type=dynamodb.AttributeType.STRING),
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
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',
function_name='once-get-upload-ticket',
description='Returns a pre-signed request to share a file',
self.get_upload_ticket_function = lambda_.Function(
self,
"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,
code=make_python_zip_bundle(os.path.join(BASE_PATH, 'get-upload-ticket')),
handler='handler.on_event',
code=make_python_zip_bundle(os.path.join(BASE_PATH, "get-upload-ticket")),
handler="handler.on_event",
log_retention=LOG_RETENTION,
environment={
'APP_URL': api_url,
'FILES_TABLE_NAME': self.files_table.table_name,
'FILES_BUCKET': self.files_bucket.bucket_name,
'SECRET_KEY': secret_key
})
"APP_URL": api_url,
"FILES_TABLE_NAME": self.files_table.table_name,
"FILES_BUCKET": self.files_bucket.bucket_name,
"SECRET_KEY": secret_key,
},
)
self.files_bucket.grant_put(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',
function_name='once-download-and-delete',
description='Serves a file from S3 and deletes it as soon as it has been successfully transferred',
self.download_and_delete_function = lambda_.Function(
self,
"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,
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, 'download-and-delete')),
handler='handler.on_event',
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, "download-and-delete")),
handler="handler.on_event",
log_retention=LOG_RETENTION,
environment={
'FILES_BUCKET': self.files_bucket.bucket_name,
'FILES_TABLE_NAME': self.files_table.table_name
})
"FILES_BUCKET": self.files_bucket.bucket_name,
"FILES_TABLE_NAME": self.files_table.table_name,
},
)
self.files_bucket.grant_read(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)
get_upload_ticket_integration = apigw.LambdaProxyIntegration(handler=self.get_upload_ticket_function)
self.api.add_routes(
path='/',
methods=[apigw.HttpMethod.GET],
integration=get_upload_ticket_integration)
get_upload_ticket_integration = integrations.LambdaProxyIntegration(handler=self.get_upload_ticket_function)
self.api.add_routes(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(
path='/{entry_id}/{filename}',
methods=[apigw.HttpMethod.GET],
integration=download_and_delete_integration)
path="/{entry_id}/{filename}", methods=[apigw.HttpMethod.GET], integration=download_and_delete_integration
)
self.cleanup_function = lambda_.Function(self, 'delete-served-files-function',
function_name='once-delete-served-files',
description='Deletes files from S3 once they have been marked as deleted in DynamoDB',
self.cleanup_function = lambda_.Function(
self,
"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,
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, 'delete-served-files')),
handler='handler.on_event',
code=lambda_.Code.from_asset(os.path.join(BASE_PATH, "delete-served-files")),
handler="handler.on_event",
log_retention=LOG_RETENTION,
environment={
'FILES_BUCKET': self.files_bucket.bucket_name,
'FILES_TABLE_NAME': self.files_table.table_name
})
"FILES_BUCKET": self.files_bucket.bucket_name,
"FILES_TABLE_NAME": self.files_table.table_name,
},
)
self.files_bucket.grant_delete(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)),
targets=[targets.LambdaFunction(self.cleanup_function)])
targets=[targets.LambdaFunction(self.cleanup_function)],
)
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,
domain_name=custom_domain,
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):
'''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):
if ignore_dotfiles:
dirs[:] = [d for d in dirs if not d.startswith('.')]
files[:] = [f for f in files if not f.startswith('.')]
dirs[:] = [d for d in dirs if not d.startswith(".")]
files[:] = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if d 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:
archive_folder_name = ''
archive_folder_name = ""
else:
archive_folder_name = os.path.relpath(root, folder)
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)
def execute_shell_command(command: Union[str, List[str]],
env: Union[Dict, None] = None) -> str:
def execute_shell_command(command: Union[str, List[str]], env: Union[Dict, None] = None) -> str:
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,
env=env,
shell=True,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
completed_process = subprocess.run(
command, env=env, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
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:
path = execute_shell_command(['which', command])
path = execute_shell_command(["which", command])
if path is None:
raise MissingPrerequisiteCommand(f'Unable to find "{command}"')
return path
def make_python_zip_bundle(input_path: str,
python_version: str = '3.7',
build_folder: str = '.build',
requirements_file: str = 'requirements.txt',
output_bundle_name: str = 'bundle.zip') -> _lambda.AssetCode:
'''
def make_python_zip_bundle(
input_path: str,
python_version: str = "3.7",
build_folder: str = ".build",
requirements_file: str = "requirements.txt",
output_bundle_name: str = "bundle.zip",
) -> _lambda.AssetCode:
"""
Builds an lambda AssetCode bundling python dependencies along with the code.
The bundle is built using docker and the target lambda runtime image.
'''
"""
build_path = os.path.abspath(os.path.join(input_path, build_folder))
asset_path = os.path.join(build_path, output_bundle_name)
# 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):
docker = locate_command('docker')
lambda_runtime_docker_image = f'lambci/lambda:build-python{python_version}'
docker = locate_command("docker")
lambda_runtime_docker_image = f"lambci/lambda:build-python{python_version}"
# 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)
# builds requirements using target runtime
build_log = execute_shell_command(command=[
'docker', 'run', '--rm',
'-v', f'{input_path}:/app',
'-w', '/app',
build_log = execute_shell_command(
command=[
"docker",
"run",
"--rm",
"-v",
f"{input_path}:/app",
"-w",
"/app",
lambda_runtime_docker_image,
'pip', 'install',
'-r', requirements_file,
'-t', build_folder])
"pip",
"install",
"-r",
requirements_file,
"-t",
build_folder,
]
)
logging.info(build_log)
# 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)
logging.debug(f'Creating bundle: {asset_path}')
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, build_path, ignore_names=[output_bundle_name, '__pycache__'], ignore_dotfiles=False)
logging.debug(f"Creating bundle: {asset_path}")
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, 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)
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)
def get_folder_checksum(path: str, ignore_dotfiles: bool = True,
chunk_size: int = 4096,
digest_method: hashlib._hashlib.HASH = hashlib.md5) -> str:
def get_folder_checksum(
path: str, ignore_dotfiles: bool = True, chunk_size: int = 4096, digest_method: hashlib._hashlib.HASH = hashlib.md5
) -> str:
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()
buffer = fp.read(chunk_size)
while len(buffer) > 0:
@@ -125,10 +137,10 @@ def get_folder_checksum(path: str, ignore_dotfiles: bool = True,
buffer = fp.read(chunk_size)
return hash_func.digest()
folder_hash = b''
folder_hash = b""
for root, dirs, files in os.walk(path):
files = [f for f in files if not f.startswith('.')]
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if not d.startswith(".")]
for file_name in sorted(files):
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
for root, dirs, files in os.walk(path):
if ignore_dotfiles:
files = [f for f in files if not f.startswith('.')]
dirs[:] = [d for d in dirs if not d.startswith('.')]
files = [f for f in files if not f.startswith(".")]
dirs[:] = [d for d in dirs if not d.startswith(".")]
for file_name in files:
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