First version

This commit is contained in:
Armin Friedl 2020-10-26 17:17:00 +01:00
parent 03b1d71018
commit c05154cad9
22 changed files with 6784 additions and 0 deletions

19
Pipfile Normal file
View file

@ -0,0 +1,19 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
flake8 = "*"
epc = "*"
importmagic = "*"
ipython = "*"
[packages]
flask = "*"
flask-sqlalchemy = "*"
base58 = "*"
flask-wtf = "*"
[requires]
python_version = "3.8"

312
Pipfile.lock generated Normal file
View file

@ -0,0 +1,312 @@
{
"_meta": {
"hash": {
"sha256": "5dc5b4443b620da8c4ee4052dea107005a6edec09fe78eae470daecc8d2fc570"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.8"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"base58": {
"hashes": [
"sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79",
"sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"
],
"index": "pypi",
"version": "==2.0.1"
},
"click": {
"hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
"version": "==7.1.2"
},
"flask": {
"hashes": [
"sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060",
"sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"
],
"index": "pypi",
"version": "==1.1.2"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e",
"sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5"
],
"index": "pypi",
"version": "==2.4.4"
},
"flask-wtf": {
"hashes": [
"sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
"sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
],
"index": "pypi",
"version": "==0.14.3"
},
"itsdangerous": {
"hashes": [
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
],
"version": "==1.1.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"version": "==2.11.2"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"sqlalchemy": {
"hashes": [
"sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6",
"sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc",
"sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a",
"sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d",
"sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f",
"sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da",
"sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb",
"sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b",
"sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86",
"sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a",
"sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403",
"sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327",
"sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2",
"sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c",
"sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b",
"sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d",
"sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec",
"sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376",
"sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629",
"sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6",
"sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162",
"sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980",
"sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb",
"sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e",
"sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b",
"sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61",
"sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079",
"sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf",
"sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1",
"sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471",
"sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77",
"sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1",
"sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1",
"sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710",
"sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5",
"sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465",
"sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142",
"sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b"
],
"version": "==1.3.20"
},
"werkzeug": {
"hashes": [
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
],
"version": "==1.0.1"
},
"wtforms": {
"hashes": [
"sha256:7b504fc724d0d1d4d5d5c114e778ec88c37ea53144683e084215eed5155ada4c",
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
],
"version": "==2.3.3"
}
},
"develop": {
"backcall": {
"hashes": [
"sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e",
"sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"
],
"version": "==0.2.0"
},
"decorator": {
"hashes": [
"sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
"sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
],
"version": "==4.4.2"
},
"epc": {
"hashes": [
"sha256:a14d2ea74817955a20eb00812e3a4630a132897eb4d976420240f1152c0d7d25"
],
"index": "pypi",
"version": "==0.0.5"
},
"flake8": {
"hashes": [
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
],
"index": "pypi",
"version": "==3.8.4"
},
"importmagic": {
"hashes": [
"sha256:3f7757a5b74c9a291e20e12023bb3bf71bc2fa3adfb15a08570648ab83eaf8d8"
],
"index": "pypi",
"version": "==0.1.7"
},
"ipython": {
"hashes": [
"sha256:2e22c1f74477b5106a6fb301c342ab8c64bb75d702e350f05a649e8cb40a0fb8",
"sha256:a331e78086001931de9424940699691ad49dfb457cea31f5471eae7b78222d5e"
],
"index": "pypi",
"version": "==7.18.1"
},
"ipython-genutils": {
"hashes": [
"sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
"sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
],
"version": "==0.2.0"
},
"jedi": {
"hashes": [
"sha256:86ed7d9b750603e4ba582ea8edc678657fb4007894a12bcf6f4bb97892f31d20",
"sha256:98cc583fa0f2f8304968199b01b6b4b94f469a1f4a74c1560506ca2a211378b5"
],
"version": "==0.17.2"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"parso": {
"hashes": [
"sha256:97218d9159b2520ff45eb78028ba8b50d2bc61dcc062a9682666f2dc4bd331ea",
"sha256:caba44724b994a8a5e086460bb212abc5a8bc46951bf4a9a1210745953622eb9"
],
"version": "==0.7.1"
},
"pexpect": {
"hashes": [
"sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
"sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
],
"markers": "sys_platform != 'win32'",
"version": "==4.8.0"
},
"pickleshare": {
"hashes": [
"sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
"sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
],
"version": "==0.7.5"
},
"prompt-toolkit": {
"hashes": [
"sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
"sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
],
"version": "==3.0.8"
},
"ptyprocess": {
"hashes": [
"sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
"sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
],
"version": "==0.6.0"
},
"pycodestyle": {
"hashes": [
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
],
"version": "==2.6.0"
},
"pyflakes": {
"hashes": [
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
],
"version": "==2.2.0"
},
"pygments": {
"hashes": [
"sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
"sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
],
"version": "==2.7.2"
},
"sexpdata": {
"hashes": [
"sha256:1ac827a616c5e87ebb60fd6686fb86f8a166938c645f4089d92de3ffbdd494e0"
],
"version": "==0.0.3"
},
"traitlets": {
"hashes": [
"sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396",
"sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"
],
"version": "==5.0.5"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
}
}
}

19
TODO.org Normal file
View file

@ -0,0 +1,19 @@
#+TODO: TODO NEXT HOLD | DONE CANCELLED
* URL Shortener [22%]
** DONE Landing Page Endpoint
CLOSED: [2020-10-25 Sun 04:50]
** DONE Snip Endpoint
CLOSED: [2020-10-25 Sun 05:22]
** TODO Shorten URL
** TODO Save Shortened URLs
** TODO Unsnip Shortened URLs
** TODO Landing Page Template
** TODO Rediret
** TODO Stats Endpoint
** TODO Store Stats
** TODO Stats Template
** TODO Externalize config
** TODO Deploy script
** TODO Improvement: Pronouncable short urls
with markov chain?

5859
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "snip",
"version": "1.0.0",
"description": "A tiny url shortener",
"private": true,
"scripts": {
"build": "webpack --config webpack.dev.js",
"watch": "webpack --watch --config webpack.dev.js",
"publish": "webpack --config webpack.prod.js"
},
"repository": {
"type": "git",
"url": "git@git.friedl.net:incubator/snip.git"
},
"author": "Armin Friedl",
"license": "MIT",
"devDependencies": {
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^4.2.2",
"gulp": "^4.0.2",
"sass": "^1.26.10",
"sass-loader": "^10.0.2",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.1.3"
},
"dependencies": {
"loglevel": "^1.7.0",
"reset-css": "^5.0.1"
}
}

88
snip.svg Normal file
View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 122.88 122.88"
style="enable-background:new 0 0 122.88 122.88"
xml:space="preserve"
sodipodi:docname="snip.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"><metadata
id="metadata13"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs11"><filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter1676"><feFlood
flood-opacity="0.498039"
flood-color="rgb(92,153,246)"
result="flood"
id="feFlood1666" /><feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite1668" /><feGaussianBlur
in="composite1"
stdDeviation="2.77556e-17"
result="blur"
id="feGaussianBlur1670" /><feOffset
dx="1"
dy="1"
result="offset"
id="feOffset1672" /><feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite1674" /></filter></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1892"
inkscape:window-height="1052"
id="namedview9"
showgrid="false"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:zoom="4.8279834"
inkscape:cx="11.579107"
inkscape:cy="35.3816"
inkscape:window-x="28"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><style
type="text/css"
id="style2">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"><ellipse
style="fill:#abccff;fill-opacity:1;stroke-width:1.05806"
id="path844"
cx="61.351963"
cy="61.581459"
rx="60.941147"
ry="60.66304" /></g><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Layer 1"
style="display:inline"><path
class="st0"
d="m 74.09,14.2 6.58,6.58 c 8.27,8.27 7.699178,22.304751 -0.628516,30.516652 L 62.321965,68.769785 C 55.801965,75.299785 48.11,75.79 40.2,72.02 L 69.86,41.55 c 3.008641,-3.090806 3.05,-8.03 0,-11.07 l -5.97,-5.97 c -3.05,-3.05 -8.02,-3.05 -11.07,0 L 22.15,55.17 c -3.37,-7.78 -1.9,-17.2 4.43,-23.53 L 44.02,14.2 c 8.27,-8.27 21.8,-8.27 30.07,0 z m -33.89,87.89 6.58,6.58 c 8.27,8.27 21.8,8.27 30.08,0 L 94.29,91.24 C 100.82,84.72 102.18,74.91 98.41,67 L 67.55,97.86 c -3.05,3.05 -8.03,3.05 -11.07,0 l -5.97,-5.97 c -3.05,-3.05 -3.05,-8.03 0,-11.08 L 81.18,50.15 C 73.39,46.78 63.950912,48.00765 57.610912,54.34765 L 40.2,72.02 c -8.27,8.27 -8.27,21.8 0,30.07 z"
id="path4"
sodipodi:nodetypes="sssccscsccssssscccscsccccs"
style="opacity:1;mix-blend-mode:hard-light;fill:#ffac76;fill-opacity:1;stroke:#000000;stroke-width:0.2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0;paint-order:normal;filter:url(#filter1676)" /></g></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

13
snip/__init__.py Normal file
View file

@ -0,0 +1,13 @@
from flask import Flask
app = Flask(__name__)
from . import config
config.configure("local")
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
from . import models
from . import api
from . import views

47
snip/api.py Normal file
View file

@ -0,0 +1,47 @@
from flask import request
from flask import jsonify
from . import app
from . import snipper
class ClientError(Exception):
def __init__(self, message, status_code=400, payload=None):
Exception.__init__(self)
self.message = message
self.status_code = status_code
self.payload = payload
def to_dict(self):
rv = dict(self.payload or ())
rv['message'] = self.message
return rv
@app.errorhandler(ClientError)
def handle_invalid_usage(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
return response
@app.route("/api/snip", methods=['POST'])
def snip():
data = request.get_json(force=True)
url = data.get('url')
reusable = data.get('reusable', False)
if not url:
raise ClientError("Cannot shorten empty URL", 400)
app.logger.info(f"Snipping url {url}")
snip = snipper.snip(url, reusable)
return {"url": url, "snip": snip}
@app.route("/api/unsnip/<snip>", methods=['GET'])
def unsnip(snip):
url = snipper.unsnip(snip)
if not url:
raise ClientError(f"Snip {snip} not found", 404)
return {"url": url, "snip": snip}

17
snip/config.local.py Normal file
View file

@ -0,0 +1,17 @@
# FLASK
FLASK_DEBUG = True
FLASK_ENV = "development"
# elisp:
# (->> (let (res) (dotimes (n 64 res) (setq res (cons (random (math-pow 2 8)) res))))
# (--map (format "\\x%s" it))
# (--reduce (concat acc it))
# (kill-new))
SECRET_KEY = b'\x90\xd6\x07\xa9\x84\x41\x15\x7c\x7b\x96\xd5\xb7\xa2\x52\xc3'\
b'\x33\x3e\x40\xdf\x0a\x64\xdc\x27\x2b\x4a\x48\x7a\x88\xf6\x4c\xce\x9a'\
b'\x5c\x5f\xc5\xc8\xa8\xa3\xbc\x28\xc0\x10\x6d\x54\xbb\x10\x58\x86\x9c'\
b'\x68\x1b\xcc\xad\x68\xd0\xf3\x66\x65\xb5\xf4\x70\x9d\x0e\x3d'
# SQL ALCHEMY
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/test.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False

53
snip/config.py Normal file
View file

@ -0,0 +1,53 @@
import os
import logging
from . import app
logger = logging.getLogger(__name__)
default_env = "local"
config_vars = [
"SQLALCHEMY_DATABASE_URI",
"SQLALCHEMY_TRACK_MODIFICATIONS",
"SECRET_KEY",
"DEBUG",
"ENV"
]
def find_env():
env = (os.environ.get("SNIP_ENV")
or os.environ.get("FLASK_ENV")
or default_env)
if not env:
raise Exception("Could not determine environment")
logger.info(f"Using environment {env}")
def find_config_file(env):
config_file = f"config.{env}.py"
logger.info(f"Config file {config_file}")
return config_file
def validate(config):
key = config.get("SECRET_KEY")
if isinstance(key, str):
key = key.encode("utf-8")
if not isinstance(key, bytes):
raise Exception("Secret key cannot be cast to bytes")
if len(key) < 64: # for blake2b hashing in snipper
raise Exception("Secret key must be 64 bytes long")
logger.info("Configuration is valid")
def configure(env=None):
logger.info("Configuring snip")
if not env:
env = find_env()
app.config.from_pyfile(find_config_file(env))
validate(app.config)

11
snip/forms.py Normal file
View file

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms import validators
class SnipForm(FlaskForm):
# The URL validator from wtforms is rather simple and only used as a first
# line of defense here. The URL is later validated again by urlvalidator.py
# in snipper.py which can lead to a UrlValidationError.
url = StringField('url', validators=[validators.InputRequired(),
validators.URL(require_tld=False, message="Not a valid URL")])

11
snip/models.py Normal file
View file

@ -0,0 +1,11 @@
from . import db
class Snip(db.Model):
id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.String(512), nullable=False)
snip = db.Column(db.String(16), unique=True, nullable=False, index=True)
reusable = db.Column(db.Boolean, nullable=False, default=False)
def __repr__(self):
return f"<Snip .url={self.url}, .snip={self.snip}"

49
snip/snipper.py Normal file
View file

@ -0,0 +1,49 @@
from . import db
from .urlvalidator import URLValidator
from .models import Snip
import logging
import secrets
import base58
log = logging.getLogger(__name__)
def snip(url: str, reusable=False) -> str:
url_validator = URLValidator()
url_validator(url)
if reusable:
log.debug("Snipping is marked reusable. Looking for existing reusable snips.")
reusable_snip = Snip.query.filter(Snip.url == url,
Snip.reusable == True).first()
if reusable_snip:
log.debug(f"Found reusable snip {reusable_snip} for URL {url}")
return reusable_snip.snip
else:
log.debug(f"No reusable snips exist. Generating new snip.")
snip = gen_snip()
log.debug(f"Generated snip: {snip}")
while Snip.query.filter(Snip.snip == snip).first():
log.info(f"Snip {snip} for {url} already exists. Generating new snip.")
snip = gen_snip()
log.debug(f"Generated new snip: {snip}")
log.info(f"Using snip {snip} for URL {url}")
db.session.add(Snip(url=url, snip=snip, reusable=reusable))
db.session.commit()
return snip
def gen_snip():
""" Generate a random snip """
rand = secrets.token_bytes(5)
snip = str(base58.b58encode(rand), 'ascii')
return snip
def unsnip(snip: str):
snip_dao = Snip.query.filter(Snip.snip == snip).first()
if snip_dao:
return snip_dao.url
return None

BIN
snip/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

27
snip/templates/index.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Snip!</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>
<body>
<form method="post" action="{{ url_for('submit_snip') }}">
{{ form.csrf_token }}
{{ form.url }}
<button type="submit">Snip!</button>
</form>
{% if form.url.errors %}
<ul class=errors>
{% for error in form.url.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</body>
<script src="{{ url_for('static', filename='dist/snip.bundle.js') }}"></script>
</html>

1
snip/templates/index.js Normal file
View file

@ -0,0 +1 @@
console.log("Hello World");

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Snip!</title>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
</head>
<body>
<a href=" {{ url_for('snip_to', snip=snip) }}">test</a>
</body>
<script src="{{ url_for('static', filename='dist/snip.bundle.js') }}"></script>
</html>

136
snip/urlvalidator.py Normal file
View file

@ -0,0 +1,136 @@
"""
Url validator broken out from Django
https://github.com/django/django/blob/83fbaa92311dd96e330496a0e443ea71b9c183e2/django/core/validators.py
"""
import re
import ipaddress
from urllib.parse import urlsplit, urlunsplit
class UrlValidationError(Exception):
def __init__(self, message, code, params):
self.message = message
self.code = code
self.params = params
class RegexValidator:
regex = ''
message = 'Enter a valid value.'
code = 'invalid'
inverse_match = False
flags = 0
def __init__(self, regex=None, message=None, code=None, inverse_match=None, flags=None):
if regex is not None:
self.regex = regex
if message is not None:
self.message = message
if code is not None:
self.code = code
if inverse_match is not None:
self.inverse_match = inverse_match
if flags is not None:
self.flags = flags
if self.flags and not isinstance(self.regex, str):
raise TypeError("If the flags are set, regex must be a regular expression string.")
self.regex = re.compile(self.regex, self.flags)
def __call__(self, value):
"""
Validate that the input contains (or does *not* contain, if
inverse_match is True) a match for the regular expression.
"""
regex_matches = self.regex.search(str(value))
invalid_input = regex_matches if self.inverse_match else not regex_matches
if invalid_input:
raise UrlValidationError(self.message, code=self.code, params={'value': value})
def __eq__(self, other):
return (
isinstance(other, RegexValidator) and
self.regex.pattern == other.regex.pattern and
self.regex.flags == other.regex.flags and
(self.message == other.message) and
(self.code == other.code) and
(self.inverse_match == other.inverse_match)
)
class URLValidator(RegexValidator):
ul = '\u00a1-\uffff' # Unicode letters range (must not be a raw string).
# IP patterns
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
ipv6_re = r'\[[0-9a-f:.]+\]' # (simple regex, validated later)
# Host patterns
hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
tld_re = (
r'\.' # dot
r'(?!-)' # can't start with a dash
r'(?:[a-z' + ul + '-]{2,63}' # domain label
r'|xn--[a-z0-9]{1,59})' # or punycode label
r'(?<!-)' # can't end with a dash
r'\.?' # may have a trailing dot
)
host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
regex = re.compile(
r'^(?:[a-z0-9.+-]*)://' # scheme is validated separately
r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # user:pass authentication
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
r'(?::\d{2,5})?' # port
r'(?:[/?#][^\s]*)?' # resource path
r'\Z', re.IGNORECASE)
message = 'Enter a valid URL.'
schemes = ['http', 'https', 'ftp', 'ftps']
def __init__(self, schemes=None, **kwargs):
super().__init__(**kwargs)
if schemes is not None:
self.schemes = schemes
def __call__(self, value):
if not isinstance(value, str):
raise UrlValidationError(self.message, code=self.code, params={'value': value})
# Check if the scheme is valid.
scheme = value.split('://')[0].lower()
if scheme not in self.schemes:
raise UrlValidationError(self.message, code=self.code, params={'value': value})
# Then check full URL
try:
super().__call__(value)
except UrlValidationError as e:
# Trivial case failed. Try for possible IDN domain
if value:
try:
scheme, netloc, path, query, fragment = urlsplit(value)
except ValueError: # for example, "Invalid IPv6 URL"
raise UrlValidationError(self.message, code=self.code, params={'value': value})
try:
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
except UnicodeError: # invalid domain part
raise e
url = urlunsplit((scheme, netloc, path, query, fragment))
super().__call__(url)
else:
raise
else:
# Now verify IPv6 in the netloc part
host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
if host_match:
potential_ip = host_match[1]
try:
ipaddress.IPv6Address(potential_ip)
except ValueError:
raise UrlValidationError(self.message, code=self.code, params={'value': value})
# The maximum length of a full host name is 253 characters per RFC 1034
# section 3.1. It's defined to be 255 bytes or less, but this includes
# one byte for the length of the name and one byte for the trailing dot
# that's used to indicate absolute names in DNS.
if len(urlsplit(value).netloc) > 253:
raise UrlValidationError(self.message, code=self.code, params={'value': value})

25
snip/views.py Normal file
View file

@ -0,0 +1,25 @@
from flask import render_template, redirect, url_for
from . import app
from .forms import SnipForm
from . import snipper
@app.route('/')
def index():
form = SnipForm()
return render_template('index.html', form=form)
@app.route('/snip', methods=['POST'])
def submit_snip():
form = SnipForm()
if form.validate_on_submit():
snip = snipper.snip(form.url.data, reusable=False)
return render_template('success.html', snip=snip)
return render_template('index.html', form=form)
@app.route('/<snip>')
def snip_to(snip):
url = snipper.unsnip(snip)
return redirect(url)

28
webpack.common.js Normal file
View file

@ -0,0 +1,28 @@
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
module.exports = {
entry: {
snip: './snip/templates/index.js',
},
plugins: [
new CleanWebpackPlugin()
],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'snip', 'static', 'dist')
},
module: {
rules: [{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
]
}]
}
};

14
webpack.dev.js Normal file
View file

@ -0,0 +1,14 @@
const common = require('./webpack.common.js');
const webpack = require('webpack');
const { merge } = require('webpack-merge');
const path = require('path');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
plugins: [
new webpack.EnvironmentPlugin({
LOG_LEVEL: 'trace'
})
]
});

7
webpack.prod.js Normal file
View file

@ -0,0 +1,7 @@
const common = require('./webpack.common.js');
const { merge } = require('webpack-merge');
const path = require('path');
module.exports = merge(common, {
mode: 'production'
});