Compare commits
11 Commits
12e0135113
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d68b0b2744 | |||
| 8d2a513945 | |||
| e01cec63e9 | |||
| 8b5fbf3ddf | |||
| 84b4ad5305 | |||
| f5df37d979 | |||
|
|
eaf449c67a | ||
| c518fcaf14 | |||
| ec22897c96 | |||
| 3c95f31f8d | |||
| f71d5d8039 |
27
Pipfile
27
Pipfile
@@ -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
521
Pipfile.lock
generated
@@ -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": {}
|
||||
}
|
||||
@@ -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
51
app.py
@@ -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()
|
||||
|
||||
@@ -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 uploaded in {upload_time}s")
|
||||
print(f"File can be downloaded once at: {once_url}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
share()
|
||||
@@ -1,3 +0,0 @@
|
||||
click
|
||||
pygments
|
||||
requests
|
||||
@@ -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}")
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,45 +53,38 @@ 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
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import os
|
||||
from typing import Optional
|
||||
|
||||
import jsii
|
||||
from aws_cdk import(
|
||||
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,
|
||||
secret_key: str,
|
||||
custom_domain: Optional[str] = None,
|
||||
hosted_zone_id: Optional[str] = None,
|
||||
hosted_zone_name: Optional[str] = None,
|
||||
**kwargs) -> None:
|
||||
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:
|
||||
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,
|
||||
)
|
||||
|
||||
116
once/utils.py
116
once/utils.py
@@ -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',
|
||||
lambda_runtime_docker_image,
|
||||
'pip', 'install',
|
||||
'-r', requirements_file,
|
||||
'-t', build_folder])
|
||||
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,
|
||||
]
|
||||
)
|
||||
|
||||
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
1374
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
pyproject.toml
Normal file
34
pyproject.toml
Normal 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"
|
||||
@@ -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
|
||||
48
setup.py
48
setup.py
@@ -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",
|
||||
],
|
||||
)
|
||||
13
source.bat
13
source.bat
@@ -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
|
||||
Reference in New Issue
Block a user