feat: first working version

This commit is contained in:
Kenneth Jao 2025-05-24 23:30:59 -04:00
parent a0648c7a51
commit 2deabf2dcc
20 changed files with 4927 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
upload
data.db
target/*
logs/*

2214
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

28
Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "pack"
version = "0.0.1"
edition = "2024"
authors = ["Kenneth Jao"]
[dependencies]
anyhow = "1.0.98"
axum = { version = "0.8.1", features = ["multipart"] }
bytes = "1.10.1"
env_filter = "0.1.3"
log = "0.4.27"
mime = "0.3.17"
rand = { version = "0.9.1", features = ["alloc"] }
reqwest = { version = "0.12.15", features = ["json"] }
serde = { version= "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
serde_yaml = "0.9.34"
sqlite = "0.37.0"
thiserror = "2.0.12"
tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread"] }
tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["fs", "trace"] }
trace = "0.1.7"
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] }
url = "2.5.4"

View File

@ -1,3 +1,6 @@
# pack
A web server for hosting built packages, designed to be connected with Gitea actions and repositories.
A web server written in [rust](https://www.rust-lang.org/) with [axum](https://docs.rs/axum/latest/axum/) for hosting built packages as a
destination endpoint for packages in a continuous integration workflow. It is designed around Gitea, using it for its authentication
as well as integrating with Gitea actions.

75
graphics/code128.svg Normal file
View File

@ -0,0 +1,75 @@
<svg data-cached="true" width="1596" height="432" viewBox="0 0 1596 432" version="1.1">
<path d="M 0,432 H 12 V 0 H 0 Z" />
<path d="m 18,432 h 6 V 0 h -6 z" />
<path d="m 36,432 h 6 V 0 h -6 z" />
<path d="m 66,432 h 6 V 0 h -6 z" />
<path d="M 84,432 H 96 V 0 H 84 Z" />
<path d="m 120,432 h 6 V 0 h -6 z" />
<path d="m 132,432 h 6 V 0 h -6 z" />
<path d="m 150,432 h 24 V 0 h -24 z" />
<path d="m 180,432 h 6 V 0 h -6 z" />
<path d="m 198,432 h 6 V 0 h -6 z" />
<path d="m 216,432 h 24 V 0 h -24 z" />
<path d="m 246,432 h 6 V 0 h -6 z" />
<path d="m 264,432 h 6 V 0 h -6 z" />
<path d="m 276,432 h 6 V 0 h -6 z" />
<path d="m 294,432 h 24 V 0 h -24 z" />
<path d="m 330,432 h 6 V 0 h -6 z" />
<path d="m 342,432 h 24 V 0 h -24 z" />
<path d="m 378,432 h 6 V 0 h -6 z" />
<path d="m 396,432 h 18 V 0 h -18 z" />
<path d="m 426,432 h 6 V 0 h -6 z" />
<path d="m 444,432 h 12 V 0 h -12 z" />
<path d="m 462,432 h 6 V 0 h -6 z" />
<path d="m 474,432 h 18 V 0 h -18 z" />
<path d="m 504,432 h 12 V 0 h -12 z" />
<path d="m 528,432 h 6 V 0 h -6 z" />
<path d="m 540,432 h 18 V 0 h -18 z" />
<path d="m 570,432 h 12 V 0 h -12 z" />
<path d="m 594,432 h 6 V 0 h -6 z" />
<path d="m 606,432 h 6 V 0 h -6 z" />
<path d="m 624,432 h 24 V 0 h -24 z" />
<path d="m 660,432 h 6 V 0 h -6 z" />
<path d="m 678,432 h 6 V 0 h -6 z" />
<path d="m 690,432 h 12 V 0 h -12 z" />
<path d="m 726,432 h 6 V 0 h -6 z" />
<path d="m 756,432 h 6 V 0 h -6 z" />
<path d="m 768,432 h 12 V 0 h -12 z" />
<path d="m 792,432 h 12 V 0 h -12 z" />
<path d="m 828,432 h 6 V 0 h -6 z" />
<path d="m 846,432 h 6 V 0 h -6 z" />
<path d="m 858,432 h 6 V 0 h -6 z" />
<path d="m 876,432 h 12 V 0 h -12 z" />
<path d="m 900,432 h 18 V 0 h -18 z" />
<path d="m 924,432 h 12 V 0 h -12 z" />
<path d="m 960,432 h 6 V 0 h -6 z" />
<path d="m 978,432 h 6 V 0 h -6 z" />
<path d="m 990,432 h 6 V 0 h -6 z" />
<path d="m 1020,432 h 12 V 0 h -12 z" />
<path d="m 1044,432 h 6 V 0 h -6 z" />
<path d="m 1056,432 h 6 V 0 h -6 z" />
<path d="m 1074,432 h 6 V 0 h -6 z" />
<path d="m 1086,432 h 12 V 0 h -12 z" />
<path d="m 1122,432 h 6 V 0 h -6 z" />
<path d="m 1146,432 h 24 V 0 h -24 z" />
<path d="m 1176,432 h 6 V 0 h -6 z" />
<path d="m 1188,432 h 6 V 0 h -6 z" />
<path d="m 1206,432 h 12 V 0 h -12 z" />
<path d="m 1230,432 h 18 V 0 h -18 z" />
<path d="m 1254,432 h 24 V 0 h -24 z" />
<path d="m 1284,432 h 18 V 0 h -18 z" />
<path d="m 1308,432 h 6 V 0 h -6 z" />
<path d="m 1320,432 h 6 V 0 h -6 z" />
<path d="m 1332,432 h 12 V 0 h -12 z" />
<path d="m 1356,432 h 6 V 0 h -6 z" />
<path d="m 1386,432 h 6 V 0 h -6 z" />
<path d="m 1398,432 h 18 V 0 h -18 z" />
<path d="m 1428,432 h 12 V 0 h -12 z" />
<path d="m 1452,432 h 6 V 0 h -6 z" />
<path d="m 1470,432 h 6 V 0 h -6 z" />
<path d="m 1488,432 h 24 V 0 h -24 z" />
<path d="m 1518,432 h 12 V 0 h -12 z" />
<path d="m 1548,432 h 18 V 0 h -18 z" />
<path d="m 1572,432 h 6 V 0 h -6 z" />
<path d="m 1584,432 h 12 V 0 h -12 z" />
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

110
graphics/favicon.svg Normal file
View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="48"
viewBox="0 0 12.7 12.7"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="favicon.svg"
inkscape:export-filename="pack.svg"
inkscape:export-xdpi="24.58"
inkscape:export-ydpi="24.58"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="11.313709"
inkscape:cx="23.864853"
inkscape:cy="16.307649"
inkscape:window-width="2560"
inkscape:window-height="1410"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<rect
style="fill:#5b4824;fill-opacity:0.988235;stroke-width:10.0668;stroke-linecap:round;stroke-linejoin:bevel"
id="rect1527-3"
width="6.8788414"
height="6.8788414"
x="4.8726211"
y="-2.0062201"
transform="matrix(0.92312053,0.3845107,-0.92312053,0.3845107,0,0)"
inkscape:label="Inside" />
<path
id="rect1529-5-6"
style="fill:#b6904e;fill-opacity:0.988235;stroke-width:3.48451;stroke-linecap:round;stroke-linejoin:bevel"
inkscape:label="BottomTab"
d="M 12.580184,3.7010601 6.35,6.2969694 6.3499999,6.3921383 12.7,3.7463047 Z"
sodipodi:nodetypes="ccccc" />
<path
id="rect1527-5-9-2"
style="fill:#997b40;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
d="M 12.7,3.7463047 V 3.8754156 L 9.5249998,4.4276929 V 4.298582 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="RightThick" />
<path
id="rect1527"
style="fill:#e7bb69;fill-opacity:0.988235;stroke-width:8.48184;stroke-linecap:round;stroke-linejoin:bevel"
d="M 6.3499998,1.1021618 12.699492,3.7469386 9.5249998,4.2985819 3.1755071,1.653805 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="Right" />
<path
id="rect1527-5-9"
style="fill:#997b40;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
d="M 9.5249998,3.3899535 9.587387,3.5101459 6.35,6.5687057 l 2e-7,-0.17699 z"
sodipodi:nodetypes="ccccc"
inkscape:label="LeftThick" />
<path
id="rect1527-5-9-9"
style="fill:#785f30;fill-opacity:0.988235;stroke-width:8.482;stroke-linecap:round;stroke-linejoin:bevel;stroke-dasharray:none"
d="M 9.5249998,4.298582 V 4.4276929 L 8.8941256,4.1651128 8.9686178,4.0668299 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="RightThick2" />
<rect
style="fill:#e7bb69;fill-opacity:0.988235;stroke-width:3.62679;stroke-linecap:round;stroke-linejoin:bevel"
id="rect1529-5"
width="6.8791666"
height="5.8977885"
x="-13.758333"
y="9.0379715"
transform="matrix(-0.92307692,0.3846154,0,1,0,0)"
inkscape:label="Right Bottom" />
<rect
style="fill:#c29b56;fill-opacity:0.988235;stroke-width:3.62679;stroke-linecap:round;stroke-linejoin:bevel"
id="rect1529"
width="6.8791666"
height="5.8977885"
x="8.1217173e-08"
y="3.7463048"
transform="matrix(0.92307692,0.38461539,0,1,0,0)"
inkscape:label="Left Bottom" />
<path
id="rect1527-5"
style="fill:#eec06a;fill-opacity:0.988235;stroke-width:8.48184;stroke-linecap:round;stroke-linejoin:bevel"
d="M 3.1749996,0.74496517 9.5249998,3.3899535 6.3500002,6.3917157 -1.524423e-7,3.7467274 Z"
sodipodi:nodetypes="ccccc"
inkscape:label="Left" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

42
graphics/lines.py Normal file
View File

@ -0,0 +1,42 @@
import random
def main():
left = 20
width = 1
tape_height = 10
triangles = 8
points = []
for i in range(triangles*2 + 1):
if i % 2:
points.append(f"{left},{i*tape_height/(2*triangles)}")
else:
points.append(f"{left+width},{i*tape_height/(2*triangles)}")
points.append("100,10")
points.append("100,0")
print(" ".join(points))
text = '<textPath href="#packTapeNUM">PACK.PACK.PACK.PACK.</textPath>'
path = '<path id="packTapeNUM" fill="none" stroke="none" d="MOFFX OFFY l6 20">'
text_list = []
path_list = []
#path = (6, 10)
#perp = (-3, 6*10/3)
for i in range(35):
text_list.append(text.replace("NUM", str(i)))
rand = -0.2*(i%3)
path_list.append(path.replace("NUM", str(i)).replace("OFFX", f"{-3 + 3*i + 3*rand:.2f}").replace("OFFY", f"{10*rand:.2f}") )
print("".join(text_list))
print("".join(path_list))
if __name__ == "__main__":
main()

10
graphics/pack.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="48" height="48" viewBox="0 0 12.7 12.7" version="1.1">
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
</svg>

After

Width:  |  Height:  |  Size: 889 B

View File

@ -0,0 +1,3 @@
<svg width="618" height="124" viewBox="0 0 618 124" version="1.1">
<path d="M 2.1999999e-6,0 H 41.200002 V 123.6 H 2.1999999e-6 Z M 46.350002,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 H 113.3 v 15.45 h 5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 30.9 H 103 V 46.35 H 97.850002 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 H 103 v 15.450002 h 5.15 V 61.8 h 5.15 v 61.8 H 87.550002 Z M 118.45,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 25.75 v 15.45 h 5.15 V 30.9 H 154.5 V 15.45 h -15.45 z m 36.05,0 h 15.45 v 30.9 h -5.15 v 30.9 h 5.15 v 15.450002 h -5.15 v 15.45 h -5.15 V 108.15 h 10.3 V 123.6 H 175.1 Z m 20.6,0 h 5.15 V 15.45 H 195.7 Z M 206,0 h 10.3 v 15.45 h -5.15 V 30.9 h 5.15 V 15.45 h 10.3 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 h 5.15 v 15.450002 h -5.15 v 15.45 h 5.15 V 108.15 H 226.6 V 123.6 H 216.3 V 92.700002 H 206 V 123.6 h -5.15 V 108.15 H 195.7 V 92.700002 h 5.15 v -15.45 H 206 V 61.8 h 5.15 v 15.450002 h 10.3 V 61.8 H 216.3 V 46.35 H 206 V 61.8 H 190.55 V 46.35 h 5.15 V 30.9 H 206 Z m 20.6,0 h 20.6 v 46.35 h -5.15 V 15.45 H 226.6 Z m 36.05,0 h 10.3 v 30.9 h -5.15 v 15.45 h 5.15 v 30.900002 h -5.15 v 15.45 h 10.3 V 108.15 h 5.15 v 15.45 h -10.3 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 10.3 V 30.9 H 309 V 0 h 5.15 v 30.9 h 5.15 v 15.45 h -5.15 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -10.3 v -15.45 h -10.3 v 15.45 H 298.7 V 108.15 H 288.4 V 77.250002 h 10.3 V 61.8 h 5.15 V 46.35 H 298.7 V 30.9 h -10.3 v 30.9 h -5.15 V 15.45 h 10.3 z m 25.75,0 h 5.15 v 15.45 h 20.6 V 30.9 H 319.3 Z m 30.9,0 h 5.15 v 15.45 h 5.15 V 0 h 15.45 v 15.45 h -10.3 V 30.9 h -10.3 v 15.45 h 5.15 v 46.350002 h 5.15 V 108.15 h 5.15 v 15.45 h -20.6 z m 46.35,0 H 412 v 15.45 h 5.15 v 30.9 H 412 V 61.8 h 5.15 v 15.450002 h 5.15 V 61.8 h 10.3 v 15.450002 h -5.15 v 15.45 h -20.6 v -15.45 h -10.3 V 61.8 h 10.3 V 46.35 h -10.3 V 30.9 h 5.15 V 15.45 h -5.15 z m 20.6,0 h 5.15 v 15.45 h -5.15 z m 20.6,0 h 25.75 v 15.45 h -5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 61.8 h 5.15 V 46.35 h 5.15 V 77.250002 H 453.2 V 123.6 h -5.15 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 H 515 V 15.45 H 494.4 V 30.9 h 25.75 V 61.8 H 515 v 15.450002 h -10.3 v 15.45 h 15.45 V 108.15 h -10.3 V 123.6 H 494.4 V 92.700002 h 5.15 V 61.8 H 494.4 V 46.35 h -5.15 z m 36.05,0 h 36.05 V 123.6 H 525.3 Z m 41.2,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 15.45,0 H 618 v 123.6 h -5.15 z M 123.6,15.45 h 5.15 V 30.9 h 5.15 V 15.45 h 5.15 V 30.9 h 5.15 v 15.45 h -10.3 v 30.900002 h 15.45 v 15.45 h 5.15 V 61.8 H 144.2 V 46.35 h 25.75 V 61.8 h -5.15 v 30.900002 h 5.15 V 108.15 H 154.5 V 123.6 H 144.2 V 92.700002 h -5.15 V 123.6 H 133.9 V 108.15 H 123.6 V 92.700002 h 5.15 v -15.45 h -5.15 v 15.45 h -5.15 V 61.8 H 113.3 V 46.35 h 10.3 z m 257.5,0 h 5.15 V 30.9 h -5.15 z m 41.2,0 h 10.3 V 30.9 h -10.3 z m 41.2,0 h 5.15 V 30.9 h -5.15 z m 10.3,0 h 5.15 V 30.9 H 473.8 Z M 231.75,30.9 h 5.15 v 15.45 h -5.15 z m 41.2,0 h 5.15 v 15.45 h -5.15 z m 97.85,0 h 5.15 v 15.45 h 5.15 v 30.900002 h -5.15 V 61.8 h -5.15 z m 15.45,0 h 5.15 v 15.45 h -5.15 z m 72.1,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h 10.3 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -5.15 V 123.6 h -30.9 V 77.250002 h 5.15 V 108.15 h 10.3 V 92.700002 h -5.15 v -15.45 h 5.15 V 61.8 h -5.15 V 77.250002 H 463.5 V 46.35 h 5.15 z M 103,46.35 h 5.15 V 61.8 H 103 Z m 149.35,0 h 5.15 v 77.25 h -5.15 V 108.15 H 247.2 V 92.700002 h 5.15 z m 77.25,0 h 15.45 V 61.8 h -10.3 v 15.450002 h 10.3 v 15.45 H 339.9 V 108.15 H 329.6 V 123.6 H 303.85 V 92.700002 h 10.3 V 108.15 h 10.3 V 92.700002 h 5.15 v -15.45 h -5.15 V 61.8 h 5.15 z m 61.8,0 h 5.15 V 61.8 H 391.4 Z M 278.1,61.8 h 5.15 v 15.450002 h -5.15 z m -41.2,15.450002 h 5.15 v 15.45 h -5.15 z m 128.75,0 h 10.3 v 15.45 h 5.15 v -15.45 h 5.15 v 15.45 h 5.15 V 108.15 h -5.15 v 15.45 h -10.3 V 108.15 H 370.8 V 92.700002 h -5.15 z M 103,92.700002 V 108.15 h 5.15 V 92.700002 Z m 293.55,0 h 10.3 V 108.15 h -10.3 z m 30.9,0 h 5.15 V 123.6 h -5.15 z M 118.45,108.15 h 5.15 v 15.45 h -5.15 z m 221.45,0 h 5.15 v 15.45 h -5.15 z m 66.95,0 h 10.3 v 15.45 h -10.3 z" fill-rule="evenodd" style="stroke-width:0.858333" />
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

504
src/api.rs Normal file
View File

@ -0,0 +1,504 @@
use std::{fs, path, slice::Iter};
use bytes::Bytes;
use std::process::Command;
use rand::distr::{Alphanumeric, SampleString};
use thiserror::Error;
use axum::{
body::Body,
extract::{Path, Json, State, Multipart},
http::{StatusCode, header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}},
response::{IntoResponse, Response},
};
use reqwest::{
Client, Method, Request, RequestBuilder, Url
};
use serde::{Serialize, Deserialize};
use serde_json::Value;
use crate::{query, ServerState};
#[derive(Clone, Copy)]
#[repr(u8)]
enum RepoFeature {
Docs = 0,
Builds = 1,
Nightly = 2,
PreProd = 3,
}
type FeatureList = Vec<String>;
impl RepoFeature {
pub fn iter() -> Iter<'static, RepoFeature> {
static REPOFEATURE: [RepoFeature; 4] = [RepoFeature::Docs, RepoFeature::Builds, RepoFeature::Nightly, RepoFeature::PreProd];
REPOFEATURE.iter()
}
}
#[derive(Error, Debug)]
pub enum APIError {
#[error("Request error: {0}")]
RequestError(#[from] reqwest::Error),
#[error("SQL error: {0}")]
SQLError(#[from] sqlite::Error),
#[error("Filesystem error: {0}")]
IOError(#[from] std::io::Error),
#[error("Multipart extract error: {0}")]
MultipartError(#[from] axum::extract::multipart::MultipartError),
#[error("Unexpected input in JSON: {msg}")]
InvalidJson { msg: String },
#[error("No such feature '{feature}' exists")]
InvalidFeature { feature: String },
#[error("The feature '{feature}' is not enabled for this repository")]
DisabledFeature { feature: String },
#[error("The current user is not an owner of '{repo}'")]
Unauthorized { repo: String },
#[error("A token was not provided or is malformed")]
Tokenless,
#[error("{msg}")]
Other { msg: String },
}
#[repr(u16)]
pub enum ErrorCode {
InvalidToken = 0, // Invalid OAuth token.
External = 1, // Error originates from an external location/server.
Filesystem = 2, // Error originates from filesystem operation.
Sql = 3, // Error originates from SQL.
BadRequest = 4, // Error originates from client request.
}
#[derive(Serialize)]
pub struct ErrorResponse {
status: u16,
code: u16,
message: String,
}
type Result<T, E = APIError> = core::result::Result<T, E>;
impl IntoResponse for APIError {
fn into_response(self) -> Response<Body> {
let (status, code) = match self {
APIError::RequestError(ref err) => {
let status = err.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let action = match status {
StatusCode::FORBIDDEN => ErrorCode::InvalidToken,
_ => ErrorCode::External
};
(status, action)
},
APIError::SQLError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Sql),
APIError::IOError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::Filesystem),
APIError::MultipartError(_) => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::BadRequest),
APIError::InvalidJson{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
APIError::InvalidFeature{..} => (StatusCode::BAD_REQUEST, ErrorCode::BadRequest),
APIError::DisabledFeature{..} => (StatusCode::NOT_FOUND, ErrorCode::BadRequest),
APIError::Unauthorized{..} => (StatusCode::FORBIDDEN, ErrorCode::BadRequest),
APIError::Tokenless => (StatusCode::FORBIDDEN, ErrorCode::InvalidToken),
APIError::Other{..} => (StatusCode::INTERNAL_SERVER_ERROR, ErrorCode::External),
};
(status, Json(ErrorResponse{
status: u16::from(status),
code: code as u16,
message: self.to_string(),
})).into_response()
}
}
impl From<RepoFeature> for &str {
fn from(value: RepoFeature) -> Self {
match value {
RepoFeature::Docs => "docs",
RepoFeature::Builds => "builds",
RepoFeature::Nightly => "nightly",
RepoFeature::PreProd => "preprod",
}
}
}
impl TryFrom<&str> for RepoFeature {
type Error = APIError;
fn try_from(value: &str) -> Result<Self> {
match value.to_lowercase().as_str() {
"docs" => Ok(RepoFeature::Docs),
"builds" => Ok(RepoFeature::Builds),
"nightly" => Ok(RepoFeature::Nightly),
"preprod" => Ok(RepoFeature::PreProd),
other => Err(APIError::InvalidFeature { feature: other.to_owned() }),
}
}
}
fn as_feature_list(value: u64) -> FeatureList {
let mut v: Vec<String> = Vec::new();
for feature in RepoFeature::iter() {
let feat_num = 1 << (*feature as u64);
if (value & feat_num) == feat_num {
v.push(Into::<&str>::into(*feature).to_owned());
}
}
v
}
async fn gitea_api<T, U>(url: &str, method: Method, payload: &T, token: &str) -> Result<U> where
T: Serialize + ?Sized,
U: serde::de::DeserializeOwned + Default
{
// Make request to Gitea.
let res = RequestBuilder::from_parts(Client::new(), Request::new(method, Url::parse(url).unwrap()))
.header("User-Agent", "TestBot")
.header("Authorization", format!("token {token}"))
.json(payload)
.send()
.await?;
res.error_for_status_ref()?; // Return with error if Gitea request has error.
let mut content_type = "".to_owned();
if res.headers().contains_key(CONTENT_TYPE) {
content_type = res.headers().get(CONTENT_TYPE)
.unwrap()
.to_str()
.map_err(|_| APIError::Other { msg: "Content-Type in Gitea response invalid".to_owned() })?
.to_owned();
}
if content_type.contains("application/json") {
Ok(res.json::<U>().await?)
} else {
Ok(U::default())
}
}
async fn authorize(host: &str, token: &str, repo: &str) -> Result<Json<Value>> {
// Use repos API call to check admin permission. Return the repo JSON as
// well in case it needs to be used later.
let url = format!("{host}/api/v1/repos/{repo}");
let data = Empty{};
let json: Value = gitea_api(&url, Method::GET, &data, token).await?;
// If permission is not admin level, return error.
json.get("permissions")
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'permissions' key.".to_owned() })?
.get("admin")
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'admin' key.".to_owned() })?
.as_bool()
.ok_or(APIError::InvalidJson{ msg: "Value in 'admin' is not bool.".to_owned() })?
.then_some(0)
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
Ok(Json(json))
}
#[derive(Serialize, Deserialize, Default)]
pub struct Empty();
#[derive(Deserialize)]
pub struct TokenAuth {
code: String /* Could be refresh token. */
}
#[derive(Serialize)]
pub struct TokenRequest {
client_id: String,
client_secret: String,
code: String,
grant_type: String,
redirect_uri: String
}
#[derive(Deserialize, Serialize, Default)]
pub struct TokenResponse {
access_token: String,
token_type: String,
expires_in: i32,
refresh_token: String
}
pub async fn token(State(state): State<ServerState<'_>>,
Json(payload): Json<TokenAuth>) -> Result<Json<TokenResponse>> {
let token_endpoint = format!("{}/login/oauth/access_token", state.config.gitea_host);
let redirect_uri = "http://127.0.0.1:3000";
let data = TokenRequest {
client_id: state.config.client_id,
client_secret: state.config.client_secret,
code: payload.code,
grant_type: "authorization_code".to_owned(),
redirect_uri: redirect_uri.to_owned()
};
Ok(Json(gitea_api(&token_endpoint, Method::POST, &data, "").await?))
}
#[derive(Serialize)]
pub struct RefreshTokenRequest {
client_id: String,
client_secret: String,
refresh_token: String,
grant_type: String,
}
pub async fn refresh_token(State(state): State<ServerState<'_>>,
Json(payload): Json<TokenAuth>) -> Result<Json<TokenResponse>>{
let token_endpoint = format!("{}/login/oauth/access_token", state.config.gitea_host);
let data = RefreshTokenRequest {
client_id: state.config.client_id,
client_secret: state.config.client_secret,
refresh_token: payload.code,
grant_type: "refresh_token".to_owned(),
};
Ok(Json(gitea_api(&token_endpoint, Method::POST, &data, "").await?))
}
#[derive(Serialize)]
pub struct RepoResponse {
description: String,
exists: bool,
features: FeatureList,
}
fn extract_token(headers: HeaderMap) -> Result<String> {
Ok(headers.get(AUTHORIZATION)
.ok_or(APIError::Tokenless)?
.to_str()
.map_err(|_| APIError::Tokenless)?
.to_owned())
}
pub async fn get_repo(State(state): State<ServerState<'_>>,
Path((owner, repo)): Path<(String, String)>,
headers: HeaderMap) -> Result<Json<RepoResponse>> {
let repo = format!("{owner}/{repo}");
let token = extract_token(headers)?;
// Pull repository information from Gitea.
let json = authorize(&state.config.gitea_host, &token, &repo).await?;
let description = json.get("description")
.ok_or(APIError::InvalidJson{ msg: "Couldn't find 'description' key.".to_owned() })?
.as_str()
.ok_or(APIError::InvalidJson{ msg: "Value in 'description' is not String.".to_owned() })?
.to_owned();
// Check if entry exists and return features.
let mut features = state.sql.prepare(query::GET_REPO)?
.iter()
.bind((1, repo.as_str()))?
.map(|row| -> Result<FeatureList> {
Ok(as_feature_list(row?.read::<i64, _>("features") as u64))
})
.collect::<Result<Vec<FeatureList>>>()?;
if features.len() == 1 {
Ok(Json(RepoResponse { description, exists: true, features: features.remove(0) }))
} else {
Ok(Json(RepoResponse { description, exists: false, features: vec!() }))
}
}
#[derive(Serialize)]
pub struct GiteaSetSecret {
data: String,
}
pub async fn create_repo(State(state): State<ServerState<'_>>,
Path((owner, repo)): Path<(String, String)>,
headers: HeaderMap,
) -> Result<()> {
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
let token = extract_token(headers)?;
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
// Create secret and insert new repository into database.
let secret = Alphanumeric.sample_string(&mut rand::rng(), 48);
let _: i32 = state.sql.prepare(crate::query::CREATE_REPO)?
.iter()
.bind_iter::<_, (_, sqlite::Value)>([
(1, repo.clone().into()),
(2, 0.into()),
(3, secret.clone().into()),
])?
.map(|_| 0)
.sum(); // Evalute statement.
// Make associated folder.
fs::create_dir_all(path::Path::new(&state.config.upload_path).join(&repo))?;
// Add secret to Gitea secrets.
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
let data = GiteaSetSecret { data: secret };
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, &token).await?;
Ok(())
}
pub async fn delete_repo(State(state): State<ServerState<'_>>,
Path((owner, repo)): Path<(String, String)>,
headers: HeaderMap,
) -> Result<()> {
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
let token = extract_token(headers)?;
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
let _: i32 = state.sql.prepare(crate::query::DELETE_REPO)?
.iter()
.bind((1, repo.as_str()))?
.map(|_| 0)
.sum(); // Evalute statement.
// Remove entire directory and its parent if its empty.
let dir = path::Path::new(&state.config.upload_path).join(&repo);
let parent = dir.as_path()
.parent()
.ok_or(APIError::Other { msg: "Could not find parent in repo directory. Should be impossible...".to_owned() })?;
fs::remove_dir_all(&dir)?;
if fs::read_dir(parent)?.next().is_none() {
fs::remove_dir(parent)?;
}
// Remove secret from Gitea.
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, &repo);
let data = Empty{};
let _: Empty = gitea_api(secret_url.as_str(), Method::DELETE, &data, &token).await?;
Ok(())
}
#[derive(Deserialize)]
pub struct PatchRepoRequest {
secret: bool,
feature: String,
}
async fn update_secret(state: &ServerState<'_>, repo: &str, token: &str) -> Result<()> {
let secret = Alphanumeric.sample_string(&mut rand::rng(), 48);
let _: i32 = state.sql.prepare(crate::query::UPDATE_REPO_SECRET)?
.iter()
.bind_iter::<_, (_, sqlite::Value)>([
(1, secret.clone().into()),
(2, repo.into()),
])?
.map(|_| 0)
.sum(); // Evalute statement.
let secret_url = format!("{}/api/v1/repos/{}/actions/secrets/PACK_REPO_SECRET", &state.config.gitea_host, repo);
let data = GiteaSetSecret { data: secret };
let _: Empty = gitea_api(secret_url.as_str(), Method::PUT, &data, token).await?;
Ok(())
}
async fn update_feature(state: &ServerState<'_>, repo: &str, feature: &str) -> Result<()> {
let feat_num: u64 = 1 << (RepoFeature::try_from(feature)? as u64);
// Update feature in database. (feature = feature ^ feat_num) and get result.
let features = state.sql.prepare(crate::query::UPDATE_REPO_FEATURES)?
.iter()
.bind_iter::<_, (_, sqlite::Value)>([
(1, (feat_num as i64).into()),
(2, repo.into()),
])?
.map(|row| -> Result<u64> {
Ok(row?.read::<i64, _>("features") as u64)
})
.collect::<Result<Vec<u64>>>()?[0]; // Evalute statement.
// Check added or removed and update folders accordingly.
let added = (features & feat_num) == feat_num;
let dir = path::Path::new(&state.config.upload_path).join(repo).join(feature);
if added {
fs::create_dir(dir)?;
} else {
fs::remove_dir_all(dir)?;
}
Ok(())
}
pub async fn patch_repo(State(state): State<ServerState<'_>>,
Path((owner, repo)): Path<(String, String)>,
headers: HeaderMap,
Json(payload): Json<PatchRepoRequest>) -> Result<()> {
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
let token = extract_token(headers)?;
let _ = authorize(&state.config.gitea_host, &token, &repo).await?;
if payload.secret {
return update_secret(&state, &repo, &token).await;
}
update_feature(&state, &repo, &payload.feature).await
}
#[derive(Default)]
struct UploadFile {
name: String,
data: Bytes,
folder: String,
}
pub async fn upload(State(state): State<ServerState<'_>>,
Path((owner, repo, feature)): Path<(String, String, String)>,
headers: HeaderMap,
mut multipart: Multipart) -> Result<()> {
let repo = format!("{owner}/{repo}").to_lowercase(); // Repos are case-insensitive.
let feature = feature.to_lowercase();
let user_secret = extract_token(headers)?; // Authorization should be the secret this time.
let (secret, features) = &state.sql.prepare(crate::query::UPLOAD_QUERY)?
.iter()
.bind((1, repo.as_str()))?
.map(|row| -> Result<(String, FeatureList)> {
let row = row?;
Ok(
(row.read::<&str, _>("secret").to_owned(),
as_feature_list(row.read::<i64, _>("features") as u64))
)
})
.collect::<Result<Vec<(String, FeatureList)>>>()?[0];
(user_secret == *secret).then_some(0)
.ok_or(APIError::Unauthorized{ repo: repo.to_owned() })?;
features.contains(&feature).then_some(0)
.ok_or(APIError::DisabledFeature{ feature: feature.clone() })?;
// Process multipart.
let mut file = UploadFile{ folder: "".to_owned(), ..Default::default() };
while let Some(field) = multipart.next_field().await? {
let name = match field.name() {
Some(x) => x,
None => continue,
};
match name {
"name" => file.name = field.text().await?,
"file" => file.data = field.bytes().await?,
"folder" => file.folder = field.text().await?,
_ => continue,
}
}
let dir = path::Path::new(&state.config.upload_path).join(&repo).join(feature);
fs::write(dir.join(&file.name), file.data)?;
let tardir = dir.join(&file.folder);
if file.folder != *"" {
if fs::exists(&tardir)? {
fs::remove_dir_all(&tardir)?;
}
fs::create_dir(&tardir)?;
let output = Command::new("tar").args(["-xf", dir.join(&file.name).to_str().unwrap(), "-C", tardir.to_str().unwrap()])
.output()?;
// If there was an error, remove everything because the operation was unsucessful.
if !output.status.success() {
fs::remove_dir_all(&tardir)?;
fs::remove_file(dir.join(file.name))?;
return Err(APIError::Other{ msg: "Failed to complete untar".to_owned() })
}
}
Ok(())
}

145
src/main.rs Normal file
View File

@ -0,0 +1,145 @@
use std::{
fs,
net::SocketAddr,
};
use anyhow::{Context, Result};
use axum::{
body::Body,
extract::{ConnectInfo, Request, DefaultBodyLimit},
routing::{get, post},
Router,
};
use tower_http::{
services::{ServeDir, ServeFile},
trace::TraceLayer,
};
use tracing::Level;
use tracing_subscriber::{
fmt,
prelude::*,
EnvFilter,
filter::LevelFilter,
};
use tracing_appender::rolling;
use serde::Deserialize;
mod api;
mod util;
type SQLConn = sqlite::ConnectionThreadSafe;
// type RepoMap = util::TimedHashMap<String, Vec<String>>;
#[derive(Clone, Deserialize)]
struct ServerConfig {
host: String,
port: String,
db_path: String,
log_path: String,
upload_path: String,
gitea_host: String,
client_id: String,
client_secret: String,
}
#[derive(Clone)]
struct ServerState<'a> {
config: ServerConfig,
sql: &'a SQLConn,
}
mod query {
pub static DB_INIT: &str = "
CREATE TABLE IF NOT EXISTS repositories (
full_name TEXT PRIMARY KEY,
features BIGINT UNSIGNED NOT NULL,
secret TEXT NOT NULL UNIQUE
);
";
pub static GET_REPO: &str = "SELECT features FROM repositories WHERE full_name = ?;";
pub static UPLOAD_QUERY: &str = "SELECT secret, features FROM repositories WHERE full_name = ?;";
pub static CREATE_REPO: &str = "INSERT INTO repositories (full_name, features, secret) VALUES(?, ?, ?);";
pub static DELETE_REPO: &str = "DELETE FROM repositories WHERE full_name = ?;";
pub static UPDATE_REPO_SECRET: &str = "UPDATE repositories SET secret = ? WHERE full_name = ?;";
pub static UPDATE_REPO_FEATURES: &str = "UPDATE repositories SET features = (features | ?1) - (features & ?1) WHERE full_name = ? RETURNING features;";
}
static CONFIG_PATH: &str = "pack.yml";
#[tokio::main]
async fn main() -> Result<()> {
// Server initialization: read config, make address, initialize and setup SQL.
let file = String::from_utf8(fs::read(CONFIG_PATH)?)
.context(format!("Unable to read YAML config '{}'", CONFIG_PATH))?;
let config: ServerConfig = serde_yaml::from_str(&file)
.context(format!("Failed to parse YAML config '{}'.", CONFIG_PATH))?;
let addr: SocketAddr = format!("{}:{}", &config.host, &config.port).parse().
context("Invalid host or port.")?;
let sql = sqlite::Connection::open_thread_safe(&config.db_path)
.context(format!("Cannot open SQLite database at '{}'.", &config.db_path))?;
sql.execute(query::DB_INIT)
.context(format!("Failed to setup SQLite database at '{}'.", &config.db_path))?;
// Make file upload folder.
fs::create_dir_all(&config.upload_path)?;
let log_file = rolling::daily(&config.log_path, "");
let upload_path = config.upload_path.clone();
let sql: &'static sqlite::ConnectionThreadSafe = Box::leak(Box::new(sql));
let server_state = ServerState { config, sql };
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))?;
let std_layer = fmt::layer()
.compact()
.with_ansi(true)
.with_filter(env_filter);
let log_layer = fmt::layer()
.json()
.with_writer(log_file)
.with_filter(LevelFilter::DEBUG);
tracing_subscriber::registry()
.with(std_layer)
.with(log_layer)
.init();
let trace = TraceLayer::new_for_http()
.make_span_with(|request: &Request<Body>| {
let addr = match request.extensions().get::<ConnectInfo<SocketAddr>>() {
Some(x) => x.0.to_string(),
None => "unknown".to_owned(),
};
tracing::span!(Level::DEBUG,
"request",
method = format!("{}", request.method()),
version = format!("{:?}", request.version()),
remote_ip = addr,
)
});
// Build routes
let api_routes = Router::new()
.route("/token", post(api::token).patch(api::refresh_token))
.route("/repo/{owner}/{repo}", get(api::get_repo).post(api::create_repo).patch(api::patch_repo).delete(api::delete_repo))
.route("/upload/{owner}/{repo}/{feature}", post(api::upload).layer(DefaultBodyLimit::max(usize::pow(1024, 3))))
.with_state(server_state);
let app = Router::new()
.nest("/api", api_routes)
.route_service("/", ServeFile::new("static/index.html"))
.nest_service("/assets", ServeDir::new("static"))
.fallback_service(ServeDir::new(upload_path))
.layer(trace);
// Run server.
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("Listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app.into_make_service_with_connect_info::<std::net::SocketAddr>()).await?;
Ok(())
}

65
src/util.rs Normal file
View File

@ -0,0 +1,65 @@
// use std::{
// collections::HashMap,
// hash::Hash,
// marker::Send,
// sync::mpsc::{channel, Sender, TryRecvError},
// sync::{Arc, Mutex},
// thread,
// time::{Duration, SystemTime},
// };
// pub struct TimedHashMap<K, V> {
// map: Arc<Mutex<HashMap<K, (SystemTime, V)>>>,
// duration: Arc<Mutex<Duration>>,
// tx: Sender<i32>,
// }
// // Insert and cleanup send message to a main thread
// // the main looping thread which reads messages to know its next action
// impl<K, V> TimedHashMap<K, V>
// where
// K: std::cmp::Eq + Hash + Send + 'static,
// V: Send + 'static,
// {
// pub fn new() -> Self {
// let (tx, rx) = channel();
// let thp = Self {
// map: Arc::new(Mutex::new(HashMap::<K, (SystemTime, V)>::new())),
// duration: Arc::new(Mutex::new(Duration::new(60, 0))),
// tx,
// };
// let map2 = Arc::clone(&thp.map);
// let dur2 = Arc::clone(&thp.duration);
// // Cleanup thread.
// thread::spawn(move || loop {
// thread::sleep(*dur2.lock().unwrap());
// map2.lock().unwrap().retain(|_, v| v.0 < SystemTime::now());
// match rx.try_recv() {
// // Thread should get killed when tx drops.
// Ok(_) | Err(TryRecvError::Disconnected) => break,
// Err(TryRecvError::Empty) => {}
// }
// });
// thp
// }
// pub fn insert(&mut self, key: K, value: V, time: Duration) -> Option<V> {
// let map2 = Arc::clone(&self.map);
// match map2
// .lock()
// .unwrap()
// .insert(key, (SystemTime::now() + time, value))
// {
// Some(x) => Some(x.1),
// None => None,
// }
// }
// pub fn set_interval(&mut self, duration: Duration) {
// let dur2 = Arc::clone(&self.duration);
// *dur2.lock().unwrap() = duration
// }
// }

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

26
static/fragile.svg Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1852"
width="500"
height="500"
viewBox="0 0 500 500"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1856" />
<path
d="m 237.22651,265.4401 v 148.14986 h -72.40403 v 25.09037 H 335.82315 V 413.58996 H 263.41311 V 265.30632 C 322.38109,254.83555 368.1994,176.30943 368.1994,80.939099 c 0,-6.629314 -0.23449,-13.174288 -0.66756,-19.619425 h -58.77239 l 16.45555,55.312416 -18.107,58.89411 6.79535,58.8815 -37.3216,-55.31726 20.35516,-60.66703 -26.71846,-57.103736 H 132.46816 c -0.43607,6.445137 -0.66756,12.99011 -0.66756,19.619424 0,95.723142 46.1536,174.499352 105.42591,184.500992"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0983483"
id="path10892" />
<rect
style="display:inline;fill:none;fill-opacity:0.988235;stroke:#000000;stroke-width:25.4064;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect7030"
width="474.59354"
height="474.59354"
x="12.459805"
y="12.703235"
ry="0" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

703
static/index.css Normal file
View File

@ -0,0 +1,703 @@
:root {
--font-family: 'Quicksand', sans-serif;
/* Light Colors */
--cl-bg: #e0ffdd;
--cl-text: black;
--cl-dec-text: black;
--cl-box: #e7bb69;
--cl-box-edge: #c19d59;
--cl-box-ridge: #e1b768;
--cl-label: white;
--cl-lines: black;
--cl-login-hover: rgba(255, 255, 255, 0.8);
--cl-null-text: #9b9b9b;
/* Dark Colors */
--cd-bg: #111;
--cd-text: #fff;
--cd-dec-text: #9b9b9b;
--cd-box: #262933;
--cd-box-edge: #1a1a1a;
--cd-box-ridge: #282b35;
--cd-label: #222;
--cd-lines: #4d4d4d;
--cd-login-hover: rgba(0, 0, 0, 0.5);
--cd-null-text: #9b9b9b;
--c-loading: #d6d6d6;
@media (prefers-color-scheme: light) {
--c-bg: var(--cl-bg);
--c-text: var(--cl-text);
--c-dec-text: var(--cl-dec-text);
--c-box: var(--cl-box);
--c-box-edge: var(--cl-box-edge);
--c-box-ridge: var(--cl-box-ridge);
--c-label: var(--cl-label);
--c-lines: var(--cl-lines);
--c-login-hover: var(--cl-login-hover);
--c-null-text: var(--cl-null-text);
--p-theme-switch-0: 0;
--p-theme-switch-1: 1;
}
@media (prefers-color-scheme: dark) {
--c-bg: var(--cd-bg);
--c-text: var(--cd-text);
--c-dec-text: var(--cd-dec-text);
--c-box: var(--cd-box);
--c-box-edge: var(--cd-box-edge);
--c-box-ridge: var(--cd-box-ridge);
--c-label: var(--cd-label);
--c-lines: var(--cd-lines);
--c-login-hover: var(--cd-login-hover);
--c-null-text: var(--cd-null-text);
--p-theme-switch-0: 1;
--p-theme-switch-1: 0;
}
}
:root:has(#themeSwitch:checked) {
/* Swapped light and dark themes. */
@media (prefers-color-scheme: dark) {
--c-bg: var(--cl-bg);
--c-text: var(--cl-text);
--c-dec-text: var(--cl-dec-text);
--c-box: var(--cl-box);
--c-box-edge: var(--cl-box-edge);
--c-box-ridge: var(--cl-box-ridge);
--c-label: var(--cl-label);
--c-lines: var(--cl-lines);
--c-login-hover: var(--cl-login-hover);
--c-null-text: var(--cl-null-text);
}
@media (prefers-color-scheme: light) {
--c-bg: var(--cd-bg);
--c-text: var(--cd-text);
--c-dec-text: var(--cd-dec-text);
--c-box: var(--cd-box);
--c-box-edge: var(--cd-box-edge);
--c-box-ridge: var(--cd-box-ridge);
--c-label: var(--cd-label);
--c-lines: var(--cd-lines);
--c-login-hover: var(--cd-login-hover);
--c-null-text: var(--cd-null-text);
}
}
html {
width: 100%; height: 100%;
font-family: var(--font-family);
color: var(--c-text);
overflow: hidden;
@media (min-aspect-ratio: 100/135) { /* Wider */
font-size: min(calc(100/135 * 1vh), 62.5%);
/* Centered around the main box: packageLabel with net height/width 100/135 */
}
@media (max-aspect-ratio: 100/135) { /* longer */
font-size: min(calc(100/100 * 1vw), 62.5%);
}
}
body {
width: 100vw; height: 100%;
margin: 0 auto 0 auto;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--c-bg);
}
#themeSwitchContainer {
width: 10rem; height: 10rem;
position: fixed;
top: 0; left: 0;
z-index: 5;
user-select: none;
--sun-color: #fffb15;
--moon-color: #ffe598;
@media (prefers-color-scheme: light) {
--day-color: #a3e0ee;
--night-color: #222;
}
@media (prefers-color-scheme: dark) {
--night-color: #a3e0ee;
--day-color: #222;
}
--w: 6deg;
--spokes: 10;
input {
width: 100%; height: 100%;
position: absolute;
opacity: 0;
cursor: pointer;
}
span {
transition: opacity 0.15s ease-in-out;
position: absolute;
pointer-events: none;
}
span:nth-child(2) {
width: 100%; height: 100%;
top: 0; left: 0;
background-color: var(--day-color);
filter: brightness(100%);
transition: background-color 0.15s ease-in-out,
filter 0.15s ease-in-out;
}
span:nth-child(3) {
width: 4rem; height: 4rem;
position: absolute;
top: 3rem; left: 3rem;
background-color: var(--sun-color);
border-radius: 50%;
opacity: var(--p-theme-switch-1);
}
span:nth-child(3)::before {
content: "";
width: 7rem; height: 7rem;
position: absolute;
top: -1.5rem; left: -1.5rem;
border-radius: inherit;
background-image: repeating-conic-gradient(
from calc(-1*var(--w)/2), var(--sun-color) 0 calc(var(--w) - 2deg),
transparent calc(var(--w)) calc(360deg/var(--spokes) - 2deg),
var(--sun-color) calc(360deg/var(--spokes))
);
}
span:nth-child(4) {
width: 6rem; height: 6rem;
top: 1.75rem; left: 1.75rem;
background-color: var(--moon-color);
border-radius: 50%;
opacity: var(--p-theme-switch-0);
mask-image: radial-gradient(circle, rgba(0,0,0,0) 33%, black 34%);
mask-position: -3rem -3rem;
mask-repeat: no-repeat;
mask-size: 166.66% 166.66%;
}
input:hover ~ span:nth-child(2) {
filter: brightness(110%);
}
input:checked ~ span:nth-child(2) {
background-color: var(--night-color);
}
input:checked ~ span:nth-child(3) {
opacity: var(--p-theme-switch-0);
}
input:checked ~ span:nth-child(4) {
opacity: var(--p-theme-switch-1);
}
}
#package {
margin: auto;
width: 298rem;
height: 200rem;
--spacing: 0.8rem;
background: repeating-linear-gradient(90deg,
var(--c-box) 0rem,
var(--c-box) var(--spacing),
var(--c-box-ridge) var(--spacing),
var(--c-box-ridge) calc(2 *var(--spacing))
);
border: 1rem solid var(--c-box-edge);
position: absolute;
z-index: 1;
user-select: none;
img {
display: block;
position: absolute;
}
:nth-child(1) {
width: 30rem;
top: 6rem; left: 263rem;
}
:nth-child(2) {
width: 30rem;
top: 6rem; left: 228rem;
}
:nth-child(3) {
width: 35rem;
top: 162rem; left: 257rem;
}
:nth-child(4) {
width: 25rem;
top: 165rem; left: 10rem;
}
}
#packageTape {
margin: auto;
height: 30rem;
position: absolute;
z-index: 1;
text {
font: 1.5px var(--font-family);
fill: rgba(255, 255, 255, 0.4);
user-select: none;
}
}
#packageLabel {
width: 80rem;
height: 115rem;
outline-width: 5rem;
outline-style: solid;
outline-color: var(--c-label);
border-radius: 12px;
background-color: var(--c-label);
border: 0.5rem solid var(--c-lines);
transition: background-color 0.15s ease-in-out,
border 0.15s ease-in-out,
outline-color 0.15s ease-in-out;
display: grid;
grid-template-rows: 2fr 1fr 7fr 2fr;
z-index: 2;
svg {
fill: var(--c-dec-text);
}
}
#packageLabelTop {
display: grid;
grid-template-columns: 1fr 3fr;
border-bottom: 0.5rem solid var(--c-lines);
> div:nth-child(1) {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border-right: 0.5rem solid var(--c-lines);
svg {
width: 80%; height: 80%;
}
}
> div:nth-child(2) {
padding: 3%;
display: grid;
grid-template-rows: 1fr 1fr;
> div:nth-child(1) {
display: block;
color: var(--c-dec-text);
user-select: none;
p {
margin: 0;
font-size: 150%;
}
p:nth-child(1) {
display: inline-block;
}
p:nth-child(2) {
display: inline-block;
float: right;
}
}
> div:nth-child(2) {
width: 80%;
position: relative;
svg {
position: absolute;
left: 0; top: 0;
width: 35rem; height: 7rem;
pointer-events: none;
}
}
}
}
#login {
display: block;
margin: 0;
width: 24.8rem; height: 7rem;
line-height: 7rem;
position: relative;
left: 5rem;
font-size: 6rem;
font-family: 'Courier';
font-weight: 800;
text-align: center;
transition: background 0.15s ease-in-out,
color 0.15s ease-in-out;
z-index: 4;
cursor: pointer;
user-select: none;
}
#packageLabelTitle {
display: flex;
align-items: center;
justify-content: center;
border-bottom: 0.5rem solid var(--c-lines);
user-select: none;
h1 {
margin: 1%;
font-size: 430%;
}
}
@keyframes cursorWait {
0% { content: "█"; }
50% { content: ""; }
100% { content: "█"; }
}
@keyframes rotate {
to { --angle: 360deg; }
}
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
#packageLabelContent {
border-bottom: 0.5rem solid var(--c-lines);
font-size: 150%;
font-family: 'Courier';
> h1 {
padding: 7%;
margin: 0;
line-height: 100%;
color: var(--c-lines);
}
> h1::after {
animation: 1.5s infinite normal cursorWait;
content: "hello";
}
> div:nth-child(2) {
padding: 7% 7% 0 7%;
color: var(--c-text);
}
h1 {
margin: 0;
line-height: 100%;
}
input {
background: none;
color: var(--c-text);
border: 1px solid var(--c-lines);
padding: 2%;
margin: 2% 0 2% 0;
font-size: 150%;
font-family: 'Courier';
}
input.error {
outline: 2px solid red !important;
}
input[type=submit] {
cursor: pointer;
transition: background-color 0.15s ease-in-out;
}
/* Button enabled/disabled. */
input[type=submit] {
color: var(--c-null-text);
border-style: dashed;
pointer-events: none;
}
input[type=submit].enabled {
color: var(--c-text);
border-style: solid;
pointer-events: auto;
}
input[type=submit]:hover {
background-color: var(--c-login-hover);
}
input:focus {
outline: none;
}
input[type=submit].loading {
border: 1px solid transparent;
background: linear-gradient(var(--c-label), var(--c-label)) padding-box, conic-gradient(
from var(--angle),
var(--c-lines) 0% 30%,
var(--c-loading) 30% 50%,
var(--c-lines) 50% 80%,
var(--c-loading) 80% 100%
) border-box;
animation: 1.5s rotate linear infinite;
cursor: not-allowed;
}
#repoSelect {
border-bottom: 2px solid var(--c-lines);
#repoSelectInput {
margin-bottom: 0;
}
#repoSelectButton {
float: right;
}
#repoSelectError {
line-height: 100%;
height: 3rem;
margin: 2% 0 2% 0;
}
}
#repoInfo {
margin-top: 2%;
transition: opacity 0.15s ease-in-out;
> h1:first-child {
margin-top: 4%;
}
#repoActions {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
#repoLinkInput::selection {
background-color: transparent;
}
#repoLinkButton {
float: right;
}
#repoFeatures {
margin-top: 2%;
transition: opacity 0.15s ease-in-out;
#featureBox {
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-items: center;
.feature {
margin-right: 2%;
font-weight: 800;
pointer-events: auto;
}
/* .feature { */
/* border: 1px solid var(--c-lines); */
/* padding: 2%; */
/* margin-right: 2%; */
/* font-size: 150%; */
/* cursor: pointer; */
/* user-select: none; */
/* transition: background-color 0.15s ease-in-out; */
/* } */
/* .feature:hover { */
/* background-color: var(--c-login-hover); */
/* } */
/* Feature states. */
.feature {
color: var(--c-null-text);
border-style: dashed;
}
.feature.fEnabled {
color: var(--c-text);
border-style: solid;
}
}
}
}
#confirmOverlay {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
z-index: 10;
transition: opacity 0.15s ease-in-out;
> div {
margin: 30% auto 0 auto;
width: 80rem;
height: 30rem;
outline-width: 5rem;
outline-style: solid;
outline-color: var(--c-label);
border-radius: 12px;
background-color: var(--c-label);
border: 0.5rem solid var(--c-lines);
transition: background-color 0.15s ease-in-out,
border 0.15s ease-in-out,
outline-color 0.15s ease-in-out;
> div {
padding: 7%;
input[type=submit] {
float: right;
}
}
}
}
#confirmOverlay {
pointer-events: none;
top: 100%;
opacity: 0;
}
#confirmOverlay.enabled {
pointer-events: auto;
top: 0;
opacity: 1;
}
/* Repo selected and created states. */
#repoInfo, #repoFeatures {
opacity: 0;
}
#repoInfo.repoSelected, #repoFeatures.repoCreated {
opacity: 1
}
}
#packageLabelBot {
display: grid;
grid-template-rows: 6fr 1fr;
svg {
margin: 3% auto 0 auto;
width: 80%; height: 85%;
}
a {
margin: auto;
margin-bottom: 1%;
font-size: 2rem;
color: var(--c-dec-text);
text-decoration: none;
transition: filter 0.15s ease-in-out;
filter: brightness(100%);
}
a:hover {
filter: brightness(120%);
}
}
body { /* Default no authentication */
#packageLabelTitle h1::before {
content: "PRIORITY PACKAGING"
}
#packageLabelContent > div {
display: none;
}
#login {
background: var(--c-label);
color: var(--c-text);
}
#login::before {
content: "LOGIN";
}
#login:hover {
background-color: var(--c-login-hover);
}
}
body.auth {
#packageLabelTitle h1::before {
content: "PACKAGE INFO"
}
#packageLabelContent > div {
display: block;
}
#packageLabelContent > h1 {
display: none;
}
#login {
color: transparent;
background:transparent;
}
#login::before {
content: "LOGOUT";
}
#login:hover {
background: var(--c-label);
color: var(--c-text);
}
}

191
static/index.html Normal file
View File

@ -0,0 +1,191 @@
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Pack</title>
<link rel="icon" href="assets/favicon.ico?v=2">
<link rel="stylesheet" href="assets/index.css">
<link href="https://fonts.googleapis.com/css?family=Quicksand:300,400" rel="stylesheet">
<!-- <link href="https://fonts.googleapis.com/css?family=Roboto+Slab:100,300" rel="stylesheet"> -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> -->
<!-- <script src="https://use.fontawesome.com/c8d5486cd8.js"></script> -->
</head>
<body>
<div id="themeSwitchContainer">
<input type="checkbox" id="themeSwitch">
<span></span><span></span><span></span>
</div>
<div id="package">
<img src="assets/fragile.svg">
<img src="assets/keep_dry.svg">
<img src="assets/recycle.svg">
<img src="assets/qr_code.svg">
</div>
<svg id="packageTape" viewbox="0 0 100 10">
<rect width="100" height="0.3" x="0" y="4.85" style="fill:#483320;fill-opacity:0.8" />
<rect width="100" height="10" x="0" y="0" style="fill:#111;fill-opacity:0.8" />
<polygon points="21,0.0 20,0.625 21,1.25 20,1.875 21,2.5 20,3.125 21,3.75 20,4.375 21,5.0 20,5.625 21,6.25 20,6.875 21,7.5 20,8.125 21,8.75 20,9.375 21,10.0 100,10 100,0" style="fill:#111;fill-opacity:0.5"/>
<text><textPath href="#packTape0">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape1">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape2">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape3">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape4">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape5">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape6">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape7">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape8">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape9">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape10">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape11">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape12">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape13">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape14">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape15">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape16">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape17">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape18">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape19">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape20">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape21">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape22">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape23">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape24">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape25">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape26">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape27">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape28">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape29">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape30">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape31">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape32">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape33">PACK.PACK.PACK.PACK.</textPath><textPath href="#packTape34">PACK.PACK.PACK.PACK.</textPath></text>
<path id="packTape0" fill="none" stroke="none" d="M-3.00 -0.00 l6 20"><path id="packTape1" fill="none" stroke="none" d="M-0.60 -2.00 l6 20"><path id="packTape2" fill="none" stroke="none" d="M1.80 -4.00 l6 20"><path id="packTape3" fill="none" stroke="none" d="M6.00 -0.00 l6 20"><path id="packTape4" fill="none" stroke="none" d="M8.40 -2.00 l6 20"><path id="packTape5" fill="none" stroke="none" d="M10.80 -4.00 l6 20"><path id="packTape6" fill="none" stroke="none" d="M15.00 -0.00 l6 20"><path id="packTape7" fill="none" stroke="none" d="M17.40 -2.00 l6 20"><path id="packTape8" fill="none" stroke="none" d="M19.80 -4.00 l6 20"><path id="packTape9" fill="none" stroke="none" d="M24.00 -0.00 l6 20"><path id="packTape10" fill="none" stroke="none" d="M26.40 -2.00 l6 20"><path id="packTape11" fill="none" stroke="none" d="M28.80 -4.00 l6 20"><path id="packTape12" fill="none" stroke="none" d="M33.00 -0.00 l6 20"><path id="packTape13" fill="none" stroke="none" d="M35.40 -2.00 l6 20"><path id="packTape14" fill="none" stroke="none" d="M37.80 -4.00 l6 20"><path id="packTape15" fill="none" stroke="none" d="M42.00 -0.00 l6 20"><path id="packTape16" fill="none" stroke="none" d="M44.40 -2.00 l6 20"><path id="packTape17" fill="none" stroke="none" d="M46.80 -4.00 l6 20"><path id="packTape18" fill="none" stroke="none" d="M51.00 -0.00 l6 20"><path id="packTape19" fill="none" stroke="none" d="M53.40 -2.00 l6 20"><path id="packTape20" fill="none" stroke="none" d="M55.80 -4.00 l6 20"><path id="packTape21" fill="none" stroke="none" d="M60.00 -0.00 l6 20"><path id="packTape22" fill="none" stroke="none" d="M62.40 -2.00 l6 20"><path id="packTape23" fill="none" stroke="none" d="M64.80 -4.00 l6 20"><path id="packTape24" fill="none" stroke="none" d="M69.00 -0.00 l6 20"><path id="packTape25" fill="none" stroke="none" d="M71.40 -2.00 l6 20"><path id="packTape26" fill="none" stroke="none" d="M73.80 -4.00 l6 20"><path id="packTape27" fill="none" stroke="none" d="M78.00 -0.00 l6 20"><path id="packTape28" fill="none" stroke="none" d="M80.40 -2.00 l6 20"><path id="packTape29" fill="none" stroke="none" d="M82.80 -4.00 l6 20"><path id="packTape30" fill="none" stroke="none" d="M87.00 -0.00 l6 20"><path id="packTape31" fill="none" stroke="none" d="M89.40 -2.00 l6 20"><path id="packTape32" fill="none" stroke="none" d="M91.80 -4.00 l6 20"><path id="packTape33" fill="none" stroke="none" d="M96.00 -0.00 l6 20"><path id="packTape34" fill="none" stroke="none" d="M98.40 -2.00 l6 20">
</svg>
<div id="packageLabel">
<div id="packageLabelTop">
<div>
<svg width="48" height="48" viewBox="0 0 12.7 12.7" version="1.1">
<defs id="defs2" />
<path d="m 12.323869,4.1992062 -6.2301843,2.5959093 -1e-7,0.095169 6.3500004,-2.6458336 z" />
<path d="M 12.443685,4.2444508 V 4.3735617 L 9.2686845,4.925839 V 4.7967281 Z" />
<path d="M 6.0936845,1.6003079 12.443177,4.2450847 9.2686845,4.796728 2.9191918,2.1519511 Z" />
<path d="m 9.2686845,3.8880996 0.062387,0.1201924 -3.237387,3.0585598 2e-7,-0.17699 z" />
<path d="M 9.2686845,4.7967281 V 4.925839 L 8.6378103,4.6632589 8.7123025,4.564976 Z" />
<rect width="6.8791666" height="5.8977885" x="-13.480659" y="9.4293194" transform="matrix(-0.92307692,0.3846154,0,1,0,0)" />
<rect width="6.8791666" height="5.8977885" x="-0.27767482" y="4.3512492" transform="matrix(0.92307692,0.3846154,0,1,0,0)" />
<path d="M 2.9186843,1.2431113 9.2686845,3.8880996 6.0936849,6.8898618 -0.2563155,4.2448735 Z" />
</svg>
</div>
<div>
<div>
<p><b>BYTE TRANSIT FEES PAID</b></p>
<p>64 OF 64</p>
<p>6.28 TB FIBRE OPTIC EXPRESS RATE</p>
<p>ACCEPTS TAR.GZ, ZIP</p>
</div>
<div>
<svg width="618" height="124" viewBox="0 0 618 124" version="1.1">
<path d="M 2.1999999e-6,0 H 41.200002 V 123.6 H 2.1999999e-6 Z M 46.350002,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 H 113.3 v 15.45 h 5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 30.9 H 103 V 46.35 H 97.850002 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 H 103 v 15.450002 h 5.15 V 61.8 h 5.15 v 61.8 H 87.550002 Z M 118.45,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 25.75 v 15.45 h 5.15 V 30.9 H 154.5 V 15.45 h -15.45 z m 36.05,0 h 15.45 v 30.9 h -5.15 v 30.9 h 5.15 v 15.450002 h -5.15 v 15.45 h -5.15 V 108.15 h 10.3 V 123.6 H 175.1 Z m 20.6,0 h 5.15 V 15.45 H 195.7 Z M 206,0 h 10.3 v 15.45 h -5.15 V 30.9 h 5.15 V 15.45 h 10.3 V 30.9 h -5.15 v 15.45 h 5.15 V 61.8 h 5.15 v 15.450002 h -5.15 v 15.45 h 5.15 V 108.15 H 226.6 V 123.6 H 216.3 V 92.700002 H 206 V 123.6 h -5.15 V 108.15 H 195.7 V 92.700002 h 5.15 v -15.45 H 206 V 61.8 h 5.15 v 15.450002 h 10.3 V 61.8 H 216.3 V 46.35 H 206 V 61.8 H 190.55 V 46.35 h 5.15 V 30.9 H 206 Z m 20.6,0 h 20.6 v 46.35 h -5.15 V 15.45 H 226.6 Z m 36.05,0 h 10.3 v 30.9 h -5.15 v 15.45 h 5.15 v 30.900002 h -5.15 v 15.45 h 10.3 V 108.15 h 5.15 v 15.45 h -10.3 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 10.3 V 30.9 H 309 V 0 h 5.15 v 30.9 h 5.15 v 15.45 h -5.15 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -10.3 v -15.45 h -10.3 v 15.45 H 298.7 V 108.15 H 288.4 V 77.250002 h 10.3 V 61.8 h 5.15 V 46.35 H 298.7 V 30.9 h -10.3 v 30.9 h -5.15 V 15.45 h 10.3 z m 25.75,0 h 5.15 v 15.45 h 20.6 V 30.9 H 319.3 Z m 30.9,0 h 5.15 v 15.45 h 5.15 V 0 h 15.45 v 15.45 h -10.3 V 30.9 h -10.3 v 15.45 h 5.15 v 46.350002 h 5.15 V 108.15 h 5.15 v 15.45 h -20.6 z m 46.35,0 H 412 v 15.45 h 5.15 v 30.9 H 412 V 61.8 h 5.15 v 15.450002 h 5.15 V 61.8 h 10.3 v 15.450002 h -5.15 v 15.45 h -20.6 v -15.45 h -10.3 V 61.8 h 10.3 V 46.35 h -10.3 V 30.9 h 5.15 V 15.45 h -5.15 z m 20.6,0 h 5.15 v 15.45 h -5.15 z m 20.6,0 h 25.75 v 15.45 h -5.15 V 30.9 h -5.15 v 15.45 h -5.15 V 61.8 h 5.15 V 46.35 h 5.15 V 77.250002 H 453.2 V 123.6 h -5.15 v -15.45 h -5.15 v 15.45 h -5.15 z m 30.9,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 H 515 V 15.45 H 494.4 V 30.9 h 25.75 V 61.8 H 515 v 15.450002 h -10.3 v 15.45 h 15.45 V 108.15 h -10.3 V 123.6 H 494.4 V 92.700002 h 5.15 V 61.8 H 494.4 V 46.35 h -5.15 z m 36.05,0 h 36.05 V 123.6 H 525.3 Z m 41.2,0 h 5.15 v 123.6 h -5.15 z m 20.6,0 h 5.15 v 123.6 h -5.15 z m 10.3,0 h 5.15 v 123.6 h -5.15 z m 15.45,0 H 618 v 123.6 h -5.15 z M 123.6,15.45 h 5.15 V 30.9 h 5.15 V 15.45 h 5.15 V 30.9 h 5.15 v 15.45 h -10.3 v 30.900002 h 15.45 v 15.45 h 5.15 V 61.8 H 144.2 V 46.35 h 25.75 V 61.8 h -5.15 v 30.900002 h 5.15 V 108.15 H 154.5 V 123.6 H 144.2 V 92.700002 h -5.15 V 123.6 H 133.9 V 108.15 H 123.6 V 92.700002 h 5.15 v -15.45 h -5.15 v 15.45 h -5.15 V 61.8 H 113.3 V 46.35 h 10.3 z m 257.5,0 h 5.15 V 30.9 h -5.15 z m 41.2,0 h 10.3 V 30.9 h -10.3 z m 41.2,0 h 5.15 V 30.9 h -5.15 z m 10.3,0 h 5.15 V 30.9 H 473.8 Z M 231.75,30.9 h 5.15 v 15.45 h -5.15 z m 41.2,0 h 5.15 v 15.45 h -5.15 z m 97.85,0 h 5.15 v 15.45 h 5.15 v 30.900002 h -5.15 V 61.8 h -5.15 z m 15.45,0 h 5.15 v 15.45 h -5.15 z m 72.1,0 h 5.15 v 15.45 h -5.15 z m 10.3,0 h 5.15 v 15.45 h 10.3 V 61.8 h 5.15 v 15.450002 h 5.15 v 15.45 h -5.15 V 123.6 h -30.9 V 77.250002 h 5.15 V 108.15 h 10.3 V 92.700002 h -5.15 v -15.45 h 5.15 V 61.8 h -5.15 V 77.250002 H 463.5 V 46.35 h 5.15 z M 103,46.35 h 5.15 V 61.8 H 103 Z m 149.35,0 h 5.15 v 77.25 h -5.15 V 108.15 H 247.2 V 92.700002 h 5.15 z m 77.25,0 h 15.45 V 61.8 h -10.3 v 15.450002 h 10.3 v 15.45 H 339.9 V 108.15 H 329.6 V 123.6 H 303.85 V 92.700002 h 10.3 V 108.15 h 10.3 V 92.700002 h 5.15 v -15.45 h -5.15 V 61.8 h 5.15 z m 61.8,0 h 5.15 V 61.8 H 391.4 Z M 278.1,61.8 h 5.15 v 15.450002 h -5.15 z m -41.2,15.450002 h 5.15 v 15.45 h -5.15 z m 128.75,0 h 10.3 v 15.45 h 5.15 v -15.45 h 5.15 v 15.45 h 5.15 V 108.15 h -5.15 v 15.45 h -10.3 V 108.15 H 370.8 V 92.700002 h -5.15 z M 103,92.700002 V 108.15 h 5.15 V 92.700002 Z m 293.55,0 h 10.3 V 108.15 h -10.3 z m 30.9,0 h 5.15 V 123.6 h -5.15 z M 118.45,108.15 h 5.15 v 15.45 h -5.15 z m 221.45,0 h 5.15 v 15.45 h -5.15 z m 66.95,0 h 10.3 v 15.45 h -10.3 z" fill-rule="evenodd" style="stroke-width:0.858333" />
</svg>
<h1 id="login"></h1>
</div>
</div>
</div>
<div id="packageLabelTitle">
<h1><b></b></h1>
</div>
<div id="packageLabelContent">
<h1>AWAITING AUTHORIZATION...</h1>
<div>
<div id="repoSelect">
<h1>REPOSITORY:</h1>
<input id="repoSelectInput" type="text" size="32" placeholder="<user>/<repository>">
<input class="enabled" id="repoSelectButton" type="submit" value="Select">
<p id="repoSelectError"></p>
</div>
<div id="repoInfo">
<h1>ACTIONS:</h1>
<div id="repoActions">
<input class="enabled" id="repoCreate" type="submit" value="Create">
<input id="repoDelete" type="submit" value="Delete">
<input id="repoSecret" type="submit" value="Regenerate Secret">
</div>
<div id="repoFeatures">
<h1>LINK:</h1>
<input id="repoLinkInput" type="text" size="32" readonly>
<input class="enabled" id="repoLinkButton" type="submit" value="Copy">
<h1>FEATURES:</h1>
<div id="featureBox">
<input class="feature" type="submit" data-tag="docs" value="Docs">
<input class="feature" type="submit" data-tag="builds" value="Builds">
<input class="feature" type="submit" data-tag="nightly" value="Nightly">
<input class="feature" type="submit" data-tag="preprod" value="PreProd">
</div>
</div>
</div>
</div>
<div id="confirmOverlay">
<div>
<div>
<h1>CONFIRMATION:</h1>
<input id="confirmInput" type="text" size="32" placeholder="<user>/<repository>">
<input class="enabled" id="confirmButtonYes" type="submit" value="Confirm">
<input class="enabled" id="confirmButtonNo" type="submit" value="Cancel">
<p>This will (irreversibly) delete all associated files! Please type in the full repository name to confirm.</p>
</div>
</div>
</div>
</div>
<div id="packageLabelBot">
<svg width="1596" height="432" viewBox="0 0 1596 432" version="1.1">
<path d="M 0,432 H 12 V 0 H 0 Z" />
<path d="m 18,432 h 6 V 0 h -6 z" />
<path d="m 36,432 h 6 V 0 h -6 z" />
<path d="m 66,432 h 6 V 0 h -6 z" />
<path d="M 84,432 H 96 V 0 H 84 Z" />
<path d="m 120,432 h 6 V 0 h -6 z" />
<path d="m 132,432 h 6 V 0 h -6 z" />
<path d="m 150,432 h 24 V 0 h -24 z" />
<path d="m 180,432 h 6 V 0 h -6 z" />
<path d="m 198,432 h 6 V 0 h -6 z" />
<path d="m 216,432 h 24 V 0 h -24 z" />
<path d="m 246,432 h 6 V 0 h -6 z" />
<path d="m 264,432 h 6 V 0 h -6 z" />
<path d="m 276,432 h 6 V 0 h -6 z" />
<path d="m 294,432 h 24 V 0 h -24 z" />
<path d="m 330,432 h 6 V 0 h -6 z" />
<path d="m 342,432 h 24 V 0 h -24 z" />
<path d="m 378,432 h 6 V 0 h -6 z" />
<path d="m 396,432 h 18 V 0 h -18 z" />
<path d="m 426,432 h 6 V 0 h -6 z" />
<path d="m 444,432 h 12 V 0 h -12 z" />
<path d="m 462,432 h 6 V 0 h -6 z" />
<path d="m 474,432 h 18 V 0 h -18 z" />
<path d="m 504,432 h 12 V 0 h -12 z" />
<path d="m 528,432 h 6 V 0 h -6 z" />
<path d="m 540,432 h 18 V 0 h -18 z" />
<path d="m 570,432 h 12 V 0 h -12 z" />
<path d="m 594,432 h 6 V 0 h -6 z" />
<path d="m 606,432 h 6 V 0 h -6 z" />
<path d="m 624,432 h 24 V 0 h -24 z" />
<path d="m 660,432 h 6 V 0 h -6 z" />
<path d="m 678,432 h 6 V 0 h -6 z" />
<path d="m 690,432 h 12 V 0 h -12 z" />
<path d="m 726,432 h 6 V 0 h -6 z" />
<path d="m 756,432 h 6 V 0 h -6 z" />
<path d="m 768,432 h 12 V 0 h -12 z" />
<path d="m 792,432 h 12 V 0 h -12 z" />
<path d="m 828,432 h 6 V 0 h -6 z" />
<path d="m 846,432 h 6 V 0 h -6 z" />
<path d="m 858,432 h 6 V 0 h -6 z" />
<path d="m 876,432 h 12 V 0 h -12 z" />
<path d="m 900,432 h 18 V 0 h -18 z" />
<path d="m 924,432 h 12 V 0 h -12 z" />
<path d="m 960,432 h 6 V 0 h -6 z" />
<path d="m 978,432 h 6 V 0 h -6 z" />
<path d="m 990,432 h 6 V 0 h -6 z" />
<path d="m 1020,432 h 12 V 0 h -12 z" />
<path d="m 1044,432 h 6 V 0 h -6 z" />
<path d="m 1056,432 h 6 V 0 h -6 z" />
<path d="m 1074,432 h 6 V 0 h -6 z" />
<path d="m 1086,432 h 12 V 0 h -12 z" />
<path d="m 1122,432 h 6 V 0 h -6 z" />
<path d="m 1146,432 h 24 V 0 h -24 z" />
<path d="m 1176,432 h 6 V 0 h -6 z" />
<path d="m 1188,432 h 6 V 0 h -6 z" />
<path d="m 1206,432 h 12 V 0 h -12 z" />
<path d="m 1230,432 h 18 V 0 h -18 z" />
<path d="m 1254,432 h 24 V 0 h -24 z" />
<path d="m 1284,432 h 18 V 0 h -18 z" />
<path d="m 1308,432 h 6 V 0 h -6 z" />
<path d="m 1320,432 h 6 V 0 h -6 z" />
<path d="m 1332,432 h 12 V 0 h -12 z" />
<path d="m 1356,432 h 6 V 0 h -6 z" />
<path d="m 1386,432 h 6 V 0 h -6 z" />
<path d="m 1398,432 h 18 V 0 h -18 z" />
<path d="m 1428,432 h 12 V 0 h -12 z" />
<path d="m 1452,432 h 6 V 0 h -6 z" />
<path d="m 1470,432 h 6 V 0 h -6 z" />
<path d="m 1488,432 h 24 V 0 h -24 z" />
<path d="m 1518,432 h 12 V 0 h -12 z" />
<path d="m 1548,432 h 18 V 0 h -18 z" />
<path d="m 1572,432 h 6 V 0 h -6 z" />
<path d="m 1584,432 h 12 V 0 h -12 z" />
</svg>
<a href="/">https://pack.kjao.me/</a>
</div>
</div>
</body>
<script src="assets/index.js"></script>
</html>

368
static/index.js Normal file
View File

@ -0,0 +1,368 @@
const CONFIG = {
client_id: "3d463405-d098-4038-8b25-a44ca5b9ab9c",
redirect_uri: "http://127.0.0.1:3000/",
authorization_endpoint: "https://git.kjao.me/login/oauth/authorize",
token_endpoint: "https://git.kjao.me/login/oauth/access_token",
requested_scopes: "read:repository,write:repository"
};
const STATES = { // Either the non-d
auth: "auth",
error: "error",
loading: "loading",
repoSelected: "repoSelected",
repoCreated: "repoCreated",
buttonOn: "enabled",
featureOn: "fEnabled",
confirmOn: "enabled",
}
var G_REPO_VALUE = "",
G_CONFIRM_VALUE = "",
G_CONFIRM = -1;
// UTILITY //
document.on = document.addEventListener;
function sugar(obj) {
obj.on = obj.addEventListener;
obj.addClass = (x) => obj.classList.add(x);
obj.delClass = (x) => obj.classList.remove(x);
obj.hasClass = (x) => obj.classList.contains(x);
obj.setClass = (x, y) => obj.classList.toggle(x, y);
return obj;
}
function get(s) {
let x = [...document.querySelectorAll(s)].map(sugar);
return (x.length === 1) ? x[0] : x;
}
// Check if object is empty.
function isEmpty(obj) {
for (const prop in obj) {
if (Object.hasOwn(obj, prop)) {
return false;
}
}
return true;
}
// Returns error and resolves promise if necessary.
function getErr(f) {
return (err) => {
if (err instanceof Promise) {
return err.then(f);
} else {
return err;
}
}
}
// Parse a query string into an object
function parseQueryString(string) {
if(string == "") { return {}; }
var segments = string.split("&").map(s => s.split("=") );
var queryString = {};
segments.forEach(s => G_QUERYString[s[0]] = s[1]);
return queryString;
}
// Fetch function for applet use case.
async function jfetch(url, method, obj={}) {
let request = {
headers: {
"Content-Type": "application/json",
"Authorization": window.localStorage.getItem("token"),
},
method: method,
};
if (!isEmpty(obj)) request["body"] = JSON.stringify(obj);
return fetch(url, request)
.then((response) => {
let contentType = response.headers.get("Content-Type");
let json;
if (contentType && contentType.indexOf("application/json" !== -1)) {
json = response.json();
} else { // Reprocess non-json into json.
json = response.text().then((text) => {
return { status: response.status, "message": text };
});
}
if (!response.ok) { throw json; }
return json;
});
}
// OAUTH //
async function getToken() {
let response;
if (G_QUERY.code) {
response = await jfetch("/api/token", "POST", { code: G_QUERY.code });
} else {
response = await jfetch("/api/token", "PATCH", { code: window.localStorage.getItem("refresh_token") });
}
window.localStorage.setItem("token", response.access_token);
window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString());
window.localStorage.setItem("refresh_token", response.refresh_token);
}
async function tryRefresh() {
if (Date.now() < parseInt(window.localStorage.getItem("token_expiry"))) return; // Not expired.
await getToken().catch((err) => {
let json = err.json();
if (json.status == 401 && json.code === 0) { // Could not refresh, so refresh token expired.
window.localStorage.clear();
alert(`Could not get new session, did you remove access from Gitea?`);
window.location = "/";
} else {
throw json;
}
});
}
// BUTTON ONCLICK MIDDLEWARE //
// Refresh token if necessary.
function tryAuth(f) {
return async (e) => {
e.preventDefault();
if (!G_AUTH) return;
await tryRefresh();
await f(e);
}
}
// Adds loading classes for UI while awaiting.
function doLoading(f) {
return async (e) => {
if (e.target.hasClass("loading")) return;
e.target.addClass(STATES["loading"]);
await f(e);
e.target.delClass(STATES["loading"]);
}
}
// BUTTON ONCLICK HELPERS //
function setRepoError(s, err) {
const input = get("#repoSelectInput"),
error = get("#repoSelectError"),
info = get("#repoInfo");
error.innerText = s;
info.setClass(STATES["repoSelected"], !err);
input.setClass(STATES["error"], err);
}
function setRepoActions(s) {
get("#repoCreate").setClass(STATES["buttonOn"], !s);
get("#repoDelete").setClass(STATES["buttonOn"], s);
get("#repoSecret").setClass(STATES["buttonOn"], s);
get("#repoFeatures").setClass(STATES["repoCreated"], s);
}
async function getConfirm() {
get("#confirmOverlay").addClass(STATES["confirmOn"]);
return new Promise((resolve, reject) => {
const check = function() {
if (G_CONFIRM == -1) {
setTimeout(check, 100);
} else {
get("#confirmOverlay").delClass(STATES["confirmOn"]);
resolve(G_CONFIRM);
G_CONFIRM = -1;
}
}
check();
});
}
document.on("keydown", (e) => {
if (e.keyCode == 27) get("#confirmOverlay").delClass(STATES["confirmOn"]);
});
// BUTTON ONCLICK FUNCTIONS //
get("#login").on("click", async (e) => {
e.preventDefault();
if (G_AUTH) {
window.localStorage.clear();
window.location = "/";
} else {
// Build the authorization URL
let url = CONFIG.authorization_endpoint
+ "?response_type=code"
+ "&client_id="+encodeURIComponent(CONFIG.client_id)
+ "&scope="+encodeURIComponent(CONFIG.requested_scopes)
+ "&redirect_uri="+encodeURIComponent(CONFIG.redirect_uri);
// Redirect to the authorization server
window.location = url;
}
});
get("#repoSelectInput").on("keydown", (e) => {
if (e.keyCode === 13) {
get("#repoSelectButton").click();
return;
}
let value = e.target.value;
if (value !== G_REPO_VALUE) {
get("#repoSelectError").innerText = ""; // Delete error text.
get("#repoSelectInput").delClass(STATES["error"]); // Remove error class.
get("#repoInfo").delClass(STATES["repoSelected"]); // Remove repoSelect class.
setRepoActions(false); // Set actions to default.
get(".feature").forEach((feature) => feature.delClass(STATES["featureOn"]));
G_REPO_VALUE = value;
}
});
get("#repoSelectButton").on("click", tryAuth(doLoading(selectRepo)));
async function selectRepo(e) {
const input = get("#repoSelectInput");
let repo = input.value.trim();
input.value = repo;
if (repo === "") {
setRepoError("Field cannot be empty.", true);
return;
}
await jfetch("/api/repo/" + repo, "GET")
.then((json) => {
setRepoError("OK", false);
setRepoActions(json["exists"]);
if (json["exists"]) {
json["features"].forEach((feature) => {
get(`.feature[data-tag=${feature}]`).addClass(STATES["featureOn"]);
});
}
get("#repoLinkInput").value = CONFIG["redirect_uri"] + repo;
get("#repoInfo").addClass(STATES["repoSelected"]);
})
.catch(getErr((err) => {
console.log(err);
setRepoError("Unable to find repository or you have insufficient permissions.", true);
}));
}
get("#repoCreate").on("click", tryAuth(doLoading(createRepo)));
async function createRepo(e) {
const input = get("#repoSelectInput");
await jfetch("/api/repo/" + input.value, "POST")
.then((json) => {
setRepoActions(true);
})
.catch(getErr((err) => {
console.log(err);
alert("An unexpected error occurred! Check console for more details.");
}));
}
get("#repoDelete").on("click", tryAuth(doLoading(deleteRepo)));
async function deleteRepo(e) {
const input = get("#repoSelectInput");
if (!(await getConfirm())) return;
await jfetch("/api/repo/" + input.value, "DELETE")
.then((json) => {
setRepoActions(false);
})
.catch(getErr((err) => {
console.log(err);
alert("An unexpected error occurred! Check console for more details.");
}));
}
get("#repoSecret").on("click", tryAuth(doLoading(regenSecret)));
async function regenSecret(e) {
const input = get("#repoSelectInput");
await jfetch("/api/repo/" + input.value, "PATCH", { secret: true, feature: "" })
.then((json) => {
setRepoActions(true);
})
.catch(getErr((err) => {
console.log(err);
alert("An unexpected error occurred! Check console for more details.");
}));
}
get("#repoLinkButton").on("click", async (e) => {
const input = get("#repoLinkInput");
input.select();
input.setSelectionRange(0, 99999);
navigator.clipboard.writeText(input.value);
});
get(".feature").forEach((el) => el.on("click", tryAuth(doLoading(toggleFeature))));
async function toggleFeature(e) {
const input = get("#repoSelectInput");
let attr = e.target.getAttribute("data-tag");
if (e.target.hasClass(STATES["featureOn"]) && !(await getConfirm())) return;
await jfetch("/api/repo/" + input.value, "PATCH", { secret: false, feature: attr })
.then((json) => {
e.target.setClass(STATES["featureOn"]);
})
.catch(getErr((err) => {
console.log(err);
alert("An unexpected error occurred! Check console for more details.");
}));
}
get("#confirmInput").on("keydown", function(e) {
if (e.keyCode === 13) {
get("#confirmButtonYes").click();
return;
}
if (e.keyCode === 27) {
get("#confirmButtonNo").click();
return;
}
let value = e.target.value;
if (value !== G_CONFIRM_VALUE) {
get("#confirmInput").delClass(STATES["error"]);
G_REPO_VALUE = value;
}
});
get("#confirmButtonNo").on("click", (e) => {
G_CONFIRM = false;
get("#confirmInput").value = "";
});
get("#confirmButtonYes").on("click", (e) => {
let confirmValue = get("#confirmInput").value.trim();
let repoValue = get("#repoSelectInput").value;
if (confirmValue === repoValue) {
G_CONFIRM = true;
get("#confirmInput").value = "";
} else {
get("#confirmInput").addClass(STATES["error"]);
}
});
async function init() {
// Get query and delete URL parameters.
if(G_QUERY.error) alert("Error returned from authorization server: "+q.error); // Check for auth errors.
if(G_QUERY.code) {
await getToken();
G_AUTH = true;
window.history.replaceState({}, document.title, window.location.pathname);
}
get("body").setClass(STATES["auth"], G_AUTH);
}
var G_QUERY = parseQueryString(window.location.search.substring(1));
var G_AUTH = window.localStorage.getItem("token") !== null;
init();

64
static/keep_dry.svg Normal file
View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1852"
width="500"
height="500"
viewBox="0 0 500 500"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1856" />
<g
id="g1862"
transform="matrix(0.09388818,0,0,-0.09388818,21.082497,529.0621)"
style="fill:#000000;fill-opacity:0.988235">
<path
d="m 1999.88,1286.63 c -0.91,-5.74 -1.47,-13.6 -1.28,-23.03 0.41,-36.42 9,-88.28 33.43,-133.6 20.15,-37.26 52.81,-69.26 103.81,-77.77 11.59,-1.91 23.7,-2.9 36.18,-2.73 85.06,1.11 135.06,32.01 163.01,77.39 33.38,54.2 43.55,131.92 43.67,210.19 l 2.6,1507.16 c -126.9,-21.04 -233.24,-103.14 -287.97,-215.21 -63.82,130.62 -197.67,220.48 -352.46,220.48 -149.3,0 -279.07,-83.62 -345.41,-206.77 -66.27,123.15 -196.08,206.77 -345.37,206.77 -157.59,0 -293.481,-93.15 -355.844,-227.67 C 861.051,3314.35 1549.72,3838.21 2383,3863.05 l 0.27,148.88 h 187.77 l -0.27,-151.71 c 783.7,-47.65 1428.25,-537.3 1611.37,-1186.35 -70.31,105.89 -190.37,175.64 -326.75,175.64 -155.04,0 -289.08,-90.24 -352.74,-221.17 -63.72,130.93 -197.78,221.17 -352.83,221.17 -154.44,0 -288.02,-89.58 -352.07,-219.69 -46.12,93.84 -128.46,166.58 -228.77,199.85 l -2.55,-1492.59 c -0.12,-108.37 -16.81,-220.06 -71.65,-309.22 -60.27,-97.981 -159.91,-164.618 -320.61,-166.712 -25.01,-0.316 -48.08,1.403 -69.23,4.934 -119.15,19.93 -193.7,91.43 -238.54,174.538 -40.59,75.2 -54.83,160.79 -55.62,220.78 -0.16,12.23 0.39,25.75 1,33.95 l 188.1,-8.72"
style="fill:#000000;fill-opacity:0.988235;fill-rule:nonzero;stroke:none"
id="path2290" />
<path
d="m 3074.81,4774.53 c -4.53,-29.67 -16.02,-59.39 -36.62,-79.82 -20.86,-20.57 -49.46,-33.3 -80.93,-33.3 -31.86,0 -60.74,12.97 -81.6,33.87 h -0.03 c -21.81,23.81 -33.79,49.02 -33.79,81.85 0,18.13 4.21,35.41 11.7,50.77 7.58,15.5 22.35,32.15 30.37,40.51 l 208.22,211.25 c 4.81,4.99 12.79,5.05 17.77,0.21 3.2,-3.14 4.38,-7.55 3.53,-11.64 l -38.62,-293.7"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2292" />
<path
d="m 2778.9,4254.29 c -4.55,-29.69 -16.02,-59.41 -36.61,-79.82 -20.83,-20.58 -49.45,-33.33 -80.96,-33.33 -31.81,0 -60.72,12.95 -81.55,33.89 h -0.05 c -21.79,23.8 -33.82,48.98 -33.82,81.85 0,18.14 4.26,35.36 11.72,50.76 7.61,15.49 22.37,32.15 30.32,40.52 l 208.28,211.26 c 4.85,4.94 12.8,5.04 17.77,0.19 3.15,-3.13 4.36,-7.55 3.55,-11.64 l -38.65,-293.68"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2294" />
<path
d="m 1987.59,4044.19 c -4.5,-29.7 -16.02,-59.44 -36.6,-79.83 -20.83,-20.58 -49.44,-33.32 -80.98,-33.32 -31.77,0 -60.62,13.01 -81.53,33.87 v 0 c -21.79,23.82 -33.81,49 -33.81,81.87 0,18.11 4.18,35.37 11.73,50.77 7.52,15.49 22.33,32.16 30.29,40.49 l 208.29,211.28 c 4.83,4.99 12.79,5.05 17.73,0.18 3.18,-3.12 4.34,-7.55 3.54,-11.63 l -38.66,-293.68"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2296" />
<path
d="m 3267.59,3987.48 c -4.46,-29.67 -15.95,-59.41 -36.58,-79.82 -20.82,-20.58 -49.46,-33.28 -80.94,-33.28 -31.82,0 -60.68,12.95 -81.56,33.89 h -0.03 c -21.75,23.77 -33.83,48.96 -33.83,81.82 0,18.14 4.22,35.4 11.77,50.77 7.53,15.5 22.28,32.14 30.28,40.5 l 208.29,211.24 c 4.81,4.99 12.81,5.11 17.72,0.25 3.23,-3.16 4.38,-7.6 3.49,-11.66 l -38.61,-293.71"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2298" />
<path
d="m 3596.74,4484.43 c -4.5,-29.7 -15.99,-59.43 -36.62,-79.82 -20.84,-20.63 -49.44,-33.33 -80.97,-33.33 -31.74,0 -60.61,12.98 -81.49,33.9 h -0.04 c -21.79,23.74 -33.82,48.99 -33.82,81.82 0,18.14 4.2,35.39 11.73,50.77 7.51,15.5 22.28,32.16 30.32,40.52 l 208.28,211.25 c 4.83,4.98 12.8,5.04 17.75,0.18 3.18,-3.13 4.34,-7.54 3.46,-11.62 l -38.6,-293.67"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2300" />
<path
d="m 4009.05,3960.81 c -4.5,-29.68 -15.99,-59.39 -36.64,-79.8 -20.81,-20.58 -49.41,-33.33 -80.93,-33.33 -31.85,0 -60.73,12.97 -81.56,33.89 h -0.06 c -21.78,23.8 -33.82,49 -33.82,81.85 0,18.13 4.24,35.36 11.76,50.8 7.57,15.49 22.33,32.11 30.32,40.45 l 208.26,211.29 c 4.84,4.96 12.81,5.06 17.8,0.18 3.11,-3.1 4.31,-7.52 3.44,-11.66 l -38.57,-293.67"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2302" />
<path
d="m 2396.48,4761.2 c -4.44,-29.65 -15.95,-59.39 -36.58,-79.81 -20.79,-20.59 -49.46,-33.32 -80.97,-33.32 -31.75,0 -60.59,12.94 -81.51,33.88 h -0.02 c -21.79,23.81 -33.83,49.01 -33.83,81.85 0,18.1 4.18,35.35 11.73,50.77 7.52,15.51 22.33,32.12 30.3,40.49 l 208.28,211.26 c 4.85,4.97 12.8,5.07 17.82,0.21 3.12,-3.12 4.33,-7.54 3.45,-11.66 l -38.67,-293.67"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2304" />
<path
d="m 1598.56,4601.13 c -4.46,-29.68 -15.96,-59.42 -36.6,-79.81 -20.77,-20.59 -49.43,-33.35 -80.92,-33.35 -31.81,0 -60.62,13 -81.55,33.92 h -0.07 c -21.72,23.77 -33.73,48.99 -33.73,81.83 0,18.13 4.14,35.39 11.71,50.76 7.5,15.52 22.34,32.17 30.26,40.52 L 1616,4906.26 c 4.83,4.98 12.81,5.08 17.73,0.19 3.22,-3.14 4.32,-7.54 3.45,-11.63 l -38.62,-293.69"
style="fill:#000000;fill-opacity:0.988235;fill-rule:evenodd;stroke:none"
id="path2306" />
</g>
<rect
style="fill:none;fill-opacity:0.988235;stroke:#000000;stroke-width:25.4064;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers fill stroke"
id="rect7030"
width="474.5936"
height="474.5936"
x="12.7032"
y="-487.29681"
transform="scale(1,-1)"
ry="0" />
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

341
static/qr_code.svg Normal file
View File

@ -0,0 +1,341 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="264" height="264" viewBox="0 0 264 264"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events">
<rect x="0" y="0" width="264" height="264" fill="#ffffff"/>
<defs>
<rect id="p" width="8" height="8"/>
</defs>
<g fill="#000000">
<use xlink:href="#p" x="32" y="32"/>
<use xlink:href="#p" x="32" y="40"/>
<use xlink:href="#p" x="32" y="48"/>
<use xlink:href="#p" x="32" y="56"/>
<use xlink:href="#p" x="32" y="64"/>
<use xlink:href="#p" x="32" y="72"/>
<use xlink:href="#p" x="32" y="80"/>
<use xlink:href="#p" x="32" y="96"/>
<use xlink:href="#p" x="32" y="112"/>
<use xlink:href="#p" x="32" y="128"/>
<use xlink:href="#p" x="32" y="176"/>
<use xlink:href="#p" x="32" y="184"/>
<use xlink:href="#p" x="32" y="192"/>
<use xlink:href="#p" x="32" y="200"/>
<use xlink:href="#p" x="32" y="208"/>
<use xlink:href="#p" x="32" y="216"/>
<use xlink:href="#p" x="32" y="224"/>
<use xlink:href="#p" x="40" y="32"/>
<use xlink:href="#p" x="40" y="80"/>
<use xlink:href="#p" x="40" y="104"/>
<use xlink:href="#p" x="40" y="112"/>
<use xlink:href="#p" x="40" y="120"/>
<use xlink:href="#p" x="40" y="128"/>
<use xlink:href="#p" x="40" y="136"/>
<use xlink:href="#p" x="40" y="144"/>
<use xlink:href="#p" x="40" y="152"/>
<use xlink:href="#p" x="40" y="160"/>
<use xlink:href="#p" x="40" y="176"/>
<use xlink:href="#p" x="40" y="224"/>
<use xlink:href="#p" x="48" y="32"/>
<use xlink:href="#p" x="48" y="48"/>
<use xlink:href="#p" x="48" y="56"/>
<use xlink:href="#p" x="48" y="64"/>
<use xlink:href="#p" x="48" y="80"/>
<use xlink:href="#p" x="48" y="96"/>
<use xlink:href="#p" x="48" y="104"/>
<use xlink:href="#p" x="48" y="176"/>
<use xlink:href="#p" x="48" y="192"/>
<use xlink:href="#p" x="48" y="200"/>
<use xlink:href="#p" x="48" y="208"/>
<use xlink:href="#p" x="48" y="224"/>
<use xlink:href="#p" x="56" y="32"/>
<use xlink:href="#p" x="56" y="48"/>
<use xlink:href="#p" x="56" y="56"/>
<use xlink:href="#p" x="56" y="64"/>
<use xlink:href="#p" x="56" y="80"/>
<use xlink:href="#p" x="56" y="96"/>
<use xlink:href="#p" x="56" y="104"/>
<use xlink:href="#p" x="56" y="112"/>
<use xlink:href="#p" x="56" y="128"/>
<use xlink:href="#p" x="56" y="160"/>
<use xlink:href="#p" x="56" y="176"/>
<use xlink:href="#p" x="56" y="192"/>
<use xlink:href="#p" x="56" y="200"/>
<use xlink:href="#p" x="56" y="208"/>
<use xlink:href="#p" x="56" y="224"/>
<use xlink:href="#p" x="64" y="32"/>
<use xlink:href="#p" x="64" y="48"/>
<use xlink:href="#p" x="64" y="56"/>
<use xlink:href="#p" x="64" y="64"/>
<use xlink:href="#p" x="64" y="80"/>
<use xlink:href="#p" x="64" y="96"/>
<use xlink:href="#p" x="64" y="104"/>
<use xlink:href="#p" x="64" y="128"/>
<use xlink:href="#p" x="64" y="136"/>
<use xlink:href="#p" x="64" y="144"/>
<use xlink:href="#p" x="64" y="160"/>
<use xlink:href="#p" x="64" y="176"/>
<use xlink:href="#p" x="64" y="192"/>
<use xlink:href="#p" x="64" y="200"/>
<use xlink:href="#p" x="64" y="208"/>
<use xlink:href="#p" x="64" y="224"/>
<use xlink:href="#p" x="72" y="32"/>
<use xlink:href="#p" x="72" y="80"/>
<use xlink:href="#p" x="72" y="96"/>
<use xlink:href="#p" x="72" y="104"/>
<use xlink:href="#p" x="72" y="136"/>
<use xlink:href="#p" x="72" y="144"/>
<use xlink:href="#p" x="72" y="160"/>
<use xlink:href="#p" x="72" y="176"/>
<use xlink:href="#p" x="72" y="224"/>
<use xlink:href="#p" x="80" y="32"/>
<use xlink:href="#p" x="80" y="40"/>
<use xlink:href="#p" x="80" y="48"/>
<use xlink:href="#p" x="80" y="56"/>
<use xlink:href="#p" x="80" y="64"/>
<use xlink:href="#p" x="80" y="72"/>
<use xlink:href="#p" x="80" y="80"/>
<use xlink:href="#p" x="80" y="96"/>
<use xlink:href="#p" x="80" y="112"/>
<use xlink:href="#p" x="80" y="128"/>
<use xlink:href="#p" x="80" y="144"/>
<use xlink:href="#p" x="80" y="160"/>
<use xlink:href="#p" x="80" y="176"/>
<use xlink:href="#p" x="80" y="184"/>
<use xlink:href="#p" x="80" y="192"/>
<use xlink:href="#p" x="80" y="200"/>
<use xlink:href="#p" x="80" y="208"/>
<use xlink:href="#p" x="80" y="216"/>
<use xlink:href="#p" x="80" y="224"/>
<use xlink:href="#p" x="88" y="104"/>
<use xlink:href="#p" x="88" y="112"/>
<use xlink:href="#p" x="88" y="144"/>
<use xlink:href="#p" x="96" y="48"/>
<use xlink:href="#p" x="96" y="56"/>
<use xlink:href="#p" x="96" y="64"/>
<use xlink:href="#p" x="96" y="72"/>
<use xlink:href="#p" x="96" y="80"/>
<use xlink:href="#p" x="96" y="88"/>
<use xlink:href="#p" x="96" y="112"/>
<use xlink:href="#p" x="96" y="120"/>
<use xlink:href="#p" x="96" y="136"/>
<use xlink:href="#p" x="96" y="160"/>
<use xlink:href="#p" x="96" y="168"/>
<use xlink:href="#p" x="96" y="184"/>
<use xlink:href="#p" x="96" y="192"/>
<use xlink:href="#p" x="96" y="200"/>
<use xlink:href="#p" x="96" y="208"/>
<use xlink:href="#p" x="96" y="224"/>
<use xlink:href="#p" x="104" y="32"/>
<use xlink:href="#p" x="104" y="48"/>
<use xlink:href="#p" x="104" y="56"/>
<use xlink:href="#p" x="104" y="64"/>
<use xlink:href="#p" x="104" y="72"/>
<use xlink:href="#p" x="104" y="88"/>
<use xlink:href="#p" x="104" y="96"/>
<use xlink:href="#p" x="104" y="104"/>
<use xlink:href="#p" x="104" y="120"/>
<use xlink:href="#p" x="104" y="136"/>
<use xlink:href="#p" x="104" y="144"/>
<use xlink:href="#p" x="104" y="152"/>
<use xlink:href="#p" x="104" y="168"/>
<use xlink:href="#p" x="104" y="200"/>
<use xlink:href="#p" x="104" y="224"/>
<use xlink:href="#p" x="112" y="40"/>
<use xlink:href="#p" x="112" y="48"/>
<use xlink:href="#p" x="112" y="56"/>
<use xlink:href="#p" x="112" y="64"/>
<use xlink:href="#p" x="112" y="72"/>
<use xlink:href="#p" x="112" y="80"/>
<use xlink:href="#p" x="112" y="120"/>
<use xlink:href="#p" x="112" y="136"/>
<use xlink:href="#p" x="112" y="144"/>
<use xlink:href="#p" x="112" y="160"/>
<use xlink:href="#p" x="112" y="176"/>
<use xlink:href="#p" x="112" y="200"/>
<use xlink:href="#p" x="112" y="216"/>
<use xlink:href="#p" x="112" y="224"/>
<use xlink:href="#p" x="120" y="32"/>
<use xlink:href="#p" x="120" y="40"/>
<use xlink:href="#p" x="120" y="48"/>
<use xlink:href="#p" x="120" y="64"/>
<use xlink:href="#p" x="120" y="104"/>
<use xlink:href="#p" x="120" y="112"/>
<use xlink:href="#p" x="120" y="128"/>
<use xlink:href="#p" x="120" y="176"/>
<use xlink:href="#p" x="120" y="184"/>
<use xlink:href="#p" x="120" y="192"/>
<use xlink:href="#p" x="128" y="40"/>
<use xlink:href="#p" x="128" y="56"/>
<use xlink:href="#p" x="128" y="80"/>
<use xlink:href="#p" x="128" y="96"/>
<use xlink:href="#p" x="128" y="136"/>
<use xlink:href="#p" x="128" y="152"/>
<use xlink:href="#p" x="128" y="160"/>
<use xlink:href="#p" x="128" y="176"/>
<use xlink:href="#p" x="128" y="200"/>
<use xlink:href="#p" x="128" y="216"/>
<use xlink:href="#p" x="128" y="224"/>
<use xlink:href="#p" x="136" y="32"/>
<use xlink:href="#p" x="136" y="40"/>
<use xlink:href="#p" x="136" y="56"/>
<use xlink:href="#p" x="136" y="64"/>
<use xlink:href="#p" x="136" y="96"/>
<use xlink:href="#p" x="136" y="136"/>
<use xlink:href="#p" x="136" y="144"/>
<use xlink:href="#p" x="136" y="152"/>
<use xlink:href="#p" x="136" y="200"/>
<use xlink:href="#p" x="136" y="208"/>
<use xlink:href="#p" x="136" y="216"/>
<use xlink:href="#p" x="144" y="40"/>
<use xlink:href="#p" x="144" y="56"/>
<use xlink:href="#p" x="144" y="72"/>
<use xlink:href="#p" x="144" y="80"/>
<use xlink:href="#p" x="144" y="88"/>
<use xlink:href="#p" x="144" y="96"/>
<use xlink:href="#p" x="144" y="112"/>
<use xlink:href="#p" x="144" y="120"/>
<use xlink:href="#p" x="144" y="144"/>
<use xlink:href="#p" x="144" y="160"/>
<use xlink:href="#p" x="144" y="184"/>
<use xlink:href="#p" x="144" y="200"/>
<use xlink:href="#p" x="144" y="208"/>
<use xlink:href="#p" x="144" y="224"/>
<use xlink:href="#p" x="152" y="40"/>
<use xlink:href="#p" x="152" y="72"/>
<use xlink:href="#p" x="152" y="88"/>
<use xlink:href="#p" x="152" y="96"/>
<use xlink:href="#p" x="152" y="112"/>
<use xlink:href="#p" x="152" y="144"/>
<use xlink:href="#p" x="152" y="168"/>
<use xlink:href="#p" x="152" y="192"/>
<use xlink:href="#p" x="152" y="200"/>
<use xlink:href="#p" x="152" y="216"/>
<use xlink:href="#p" x="152" y="224"/>
<use xlink:href="#p" x="160" y="40"/>
<use xlink:href="#p" x="160" y="48"/>
<use xlink:href="#p" x="160" y="64"/>
<use xlink:href="#p" x="160" y="72"/>
<use xlink:href="#p" x="160" y="80"/>
<use xlink:href="#p" x="160" y="104"/>
<use xlink:href="#p" x="160" y="136"/>
<use xlink:href="#p" x="160" y="144"/>
<use xlink:href="#p" x="160" y="160"/>
<use xlink:href="#p" x="160" y="168"/>
<use xlink:href="#p" x="160" y="176"/>
<use xlink:href="#p" x="160" y="184"/>
<use xlink:href="#p" x="160" y="192"/>
<use xlink:href="#p" x="160" y="200"/>
<use xlink:href="#p" x="160" y="208"/>
<use xlink:href="#p" x="160" y="216"/>
<use xlink:href="#p" x="168" y="120"/>
<use xlink:href="#p" x="168" y="128"/>
<use xlink:href="#p" x="168" y="152"/>
<use xlink:href="#p" x="168" y="160"/>
<use xlink:href="#p" x="168" y="192"/>
<use xlink:href="#p" x="168" y="200"/>
<use xlink:href="#p" x="168" y="224"/>
<use xlink:href="#p" x="176" y="32"/>
<use xlink:href="#p" x="176" y="40"/>
<use xlink:href="#p" x="176" y="48"/>
<use xlink:href="#p" x="176" y="56"/>
<use xlink:href="#p" x="176" y="64"/>
<use xlink:href="#p" x="176" y="72"/>
<use xlink:href="#p" x="176" y="80"/>
<use xlink:href="#p" x="176" y="96"/>
<use xlink:href="#p" x="176" y="112"/>
<use xlink:href="#p" x="176" y="120"/>
<use xlink:href="#p" x="176" y="128"/>
<use xlink:href="#p" x="176" y="144"/>
<use xlink:href="#p" x="176" y="160"/>
<use xlink:href="#p" x="176" y="176"/>
<use xlink:href="#p" x="176" y="192"/>
<use xlink:href="#p" x="176" y="200"/>
<use xlink:href="#p" x="184" y="32"/>
<use xlink:href="#p" x="184" y="80"/>
<use xlink:href="#p" x="184" y="96"/>
<use xlink:href="#p" x="184" y="104"/>
<use xlink:href="#p" x="184" y="128"/>
<use xlink:href="#p" x="184" y="136"/>
<use xlink:href="#p" x="184" y="144"/>
<use xlink:href="#p" x="184" y="152"/>
<use xlink:href="#p" x="184" y="160"/>
<use xlink:href="#p" x="184" y="192"/>
<use xlink:href="#p" x="184" y="216"/>
<use xlink:href="#p" x="184" y="224"/>
<use xlink:href="#p" x="192" y="32"/>
<use xlink:href="#p" x="192" y="48"/>
<use xlink:href="#p" x="192" y="56"/>
<use xlink:href="#p" x="192" y="64"/>
<use xlink:href="#p" x="192" y="80"/>
<use xlink:href="#p" x="192" y="96"/>
<use xlink:href="#p" x="192" y="128"/>
<use xlink:href="#p" x="192" y="144"/>
<use xlink:href="#p" x="192" y="152"/>
<use xlink:href="#p" x="192" y="160"/>
<use xlink:href="#p" x="192" y="168"/>
<use xlink:href="#p" x="192" y="176"/>
<use xlink:href="#p" x="192" y="184"/>
<use xlink:href="#p" x="192" y="192"/>
<use xlink:href="#p" x="192" y="200"/>
<use xlink:href="#p" x="192" y="216"/>
<use xlink:href="#p" x="192" y="224"/>
<use xlink:href="#p" x="200" y="32"/>
<use xlink:href="#p" x="200" y="48"/>
<use xlink:href="#p" x="200" y="56"/>
<use xlink:href="#p" x="200" y="64"/>
<use xlink:href="#p" x="200" y="80"/>
<use xlink:href="#p" x="200" y="96"/>
<use xlink:href="#p" x="200" y="112"/>
<use xlink:href="#p" x="200" y="136"/>
<use xlink:href="#p" x="200" y="144"/>
<use xlink:href="#p" x="200" y="168"/>
<use xlink:href="#p" x="200" y="184"/>
<use xlink:href="#p" x="200" y="200"/>
<use xlink:href="#p" x="200" y="208"/>
<use xlink:href="#p" x="200" y="216"/>
<use xlink:href="#p" x="200" y="224"/>
<use xlink:href="#p" x="208" y="32"/>
<use xlink:href="#p" x="208" y="48"/>
<use xlink:href="#p" x="208" y="56"/>
<use xlink:href="#p" x="208" y="64"/>
<use xlink:href="#p" x="208" y="80"/>
<use xlink:href="#p" x="208" y="96"/>
<use xlink:href="#p" x="208" y="128"/>
<use xlink:href="#p" x="208" y="160"/>
<use xlink:href="#p" x="208" y="176"/>
<use xlink:href="#p" x="208" y="192"/>
<use xlink:href="#p" x="208" y="200"/>
<use xlink:href="#p" x="208" y="208"/>
<use xlink:href="#p" x="208" y="224"/>
<use xlink:href="#p" x="216" y="32"/>
<use xlink:href="#p" x="216" y="80"/>
<use xlink:href="#p" x="216" y="104"/>
<use xlink:href="#p" x="216" y="112"/>
<use xlink:href="#p" x="216" y="128"/>
<use xlink:href="#p" x="216" y="136"/>
<use xlink:href="#p" x="216" y="144"/>
<use xlink:href="#p" x="216" y="176"/>
<use xlink:href="#p" x="216" y="192"/>
<use xlink:href="#p" x="216" y="200"/>
<use xlink:href="#p" x="216" y="224"/>
<use xlink:href="#p" x="224" y="32"/>
<use xlink:href="#p" x="224" y="40"/>
<use xlink:href="#p" x="224" y="48"/>
<use xlink:href="#p" x="224" y="56"/>
<use xlink:href="#p" x="224" y="64"/>
<use xlink:href="#p" x="224" y="72"/>
<use xlink:href="#p" x="224" y="80"/>
<use xlink:href="#p" x="224" y="112"/>
<use xlink:href="#p" x="224" y="120"/>
<use xlink:href="#p" x="224" y="128"/>
<use xlink:href="#p" x="224" y="144"/>
<use xlink:href="#p" x="224" y="152"/>
<use xlink:href="#p" x="224" y="176"/>
<use xlink:href="#p" x="224" y="184"/>
<use xlink:href="#p" x="224" y="192"/>
<use xlink:href="#p" x="224" y="200"/>
<use xlink:href="#p" x="224" y="208"/>
<use xlink:href="#p" x="224" y="216"/>
<use xlink:href="#p" x="224" y="224"/>
</g>
<g></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

30
static/recycle.svg Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1852"
width="500"
height="500"
viewBox="0 0 500 500"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1856" />
<g
id="g9426"
transform="matrix(0.10916853,0,0,-0.10916853,-59.454168,597.04669)">
<path
d="m 4125.96,4640.21 c -60.45,105.33 -126.32,210.05 -194.01,325.81 -59.51,101.77 -115.02,247.69 -233.79,288.49 -88.97,30.57 -218.55,24.87 -335.74,24.87 -238.52,0 -471.48,4.96 -708.8,7.46 115.68,-111.3 194.18,-264.61 286.02,-400.41 31.49,-46.59 56.11,-98.61 84.55,-146.74 114.69,-194.18 232.64,-389.07 343.21,-581.96 -95.1,-52.48 -191.01,-104.12 -281.03,-161.66 296.9,17.35 577.91,27.92 877.91,44.77 137.57,245.43 280.22,499.06 410.38,743.62 -82.35,-48.66 -164.78,-97.22 -248.7,-144.25 z m -1248.51,166.64 c -53.07,84.43 -111.38,177.15 -169.12,268.59 -55.7,88.24 -126.21,174.96 -233.78,203.94 -20.3,5.47 -49.31,7.78 -77.09,12.43 -25.3,4.25 -53.65,10.67 -77.1,12.45 -163.65,12.42 -273.55,-67.71 -348.2,-149.23 -34.1,-37.26 -55.41,-86.3 -82.06,-134.3 -127.21,-229.18 -269.64,-477.23 -385.49,-693.89 285.43,-153.11 565.14,-311.94 850.57,-465.07 m 1803.1,902.79 c 154.09,87.98 305.54,178.61 460.1,266.11 -199.68,-359.9 -396.33,-722.85 -599.39,-1079.37 -433.28,-21.84 -870.82,-42.72 -1308.18,-64.66 -2.27,5.2 13.74,13.73 24.88,19.9 165.99,92.14 340.08,192.34 504.87,286.02 -101.8,173.43 -204.22,346.23 -305.9,519.77 -184.74,-321.8 -361.72,-651.32 -544.66,-974.91 -338.03,183.44 -673.86,369.03 -1009.74,554.62 114.18,211.08 229.87,419.32 353.15,639.16 59.08,105.34 109.28,220.43 189.02,305.9 77.4,82.98 199.37,151.4 345.7,159.17 52.84,2.81 104.95,-13.21 156.69,-17.41 94.81,-7.68 197.91,-2.48 295.94,-2.48 245.41,0 492.06,-7.47 738.66,-7.47 100.19,0 187.99,-2.45 261.14,-24.87 126.97,-38.91 189.11,-153.41 251.19,-258.64 64.97,-110.17 124.97,-214.57 186.53,-320.84"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path9858" />
<path
d="m 4640.77,2469.03 c -184.34,-12.36 -393.76,2.49 -601.88,2.49 -207.08,0 -414.32,2.29 -601.85,7.47 1.22,98.66 9.77,219.19 4.96,323.31 -136.6,-262.16 -280.27,-517.25 -417.81,-778.46 143.98,-242.29 281.8,-490.82 425.27,-733.67 4.53,99.18 0.77,195.09 7.47,288.5 95.04,-1.2 198.28,-4.97 300.92,-4.97 105.41,0 212.31,-10.51 303.43,-2.5 146.62,12.92 202.16,117.08 263.63,213.9 148.23,233.45 303.64,481.76 450.14,708.8 -0.99,4.29 3.69,6.38 0,7.45 -40.63,-16.78 -87.23,-29.16 -134.28,-32.32 z m -932.64,1029.64 c 165.79,-293.25 333.99,-588.18 504.86,-880.41 11.88,-20.34 25.18,-52.99 37.31,-57.2 21.31,-7.42 70.62,0 99.47,0 98.19,0 222.77,-8.87 295.96,0 111.22,13.45 204.78,78.03 263.64,144.25 87.85,98.84 152.41,266.37 104.43,435.23 -13.84,48.76 -35.98,84.65 -59.69,126.83 -135.64,241.53 -272.96,480.57 -407.84,711.29 -275.88,-153.55 -557.25,-323.17 -838.14,-479.99 z m 870.46,606.84 c 2.01,-0.48 2.46,-2.5 4.96,-2.49 123.6,-214.99 242.66,-429.99 368.1,-646.64 78.16,-135.04 186.73,-261.92 171.61,-487.45 -11.1,-165.26 -114.82,-275.77 -186.53,-390.47 -174.63,-279.29 -344.05,-546.98 -519.82,-825.68 -77.96,-123.69 -147.75,-232.92 -310.85,-266.12 -54.08,-11.02 -113.29,-8.09 -181.57,-4.99 -123.95,5.65 -259.75,-1.14 -380.51,2.5 -0.74,-175.84 -6.02,-347.16 -7.46,-522.291 -5.89,-3.617 -13.59,14.101 -19.89,24.871 -199.27,339.78 -396.99,696.46 -596.9,1037.09 206.96,387.43 418.13,770.71 624.25,1158.98 -0.3,-190.38 -9.75,-412.48 -12.45,-616.8 202.92,1.47 398.55,-4.36 601.89,-2.49 -173.7,322.08 -370.65,642.4 -549.64,967.46 331.45,191.65 663.62,382.6 994.81,574.52"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path9860" />
<path
d="m 1447.4,2270.08 c -46.21,-78.85 -95.63,-159.69 -94.49,-266.13 1.81,-172.7 95.5,-317.04 198.96,-387.96 82.01,-56.24 184.38,-79.6 318.33,-79.6 256.45,0.02 521.31,-2.09 793.36,2.49 -2.81,321.34 -2.82,645.47 -2.48,969.94 -357.31,0 -714.62,0 -1071.91,0 -47.61,-73.71 -91.59,-153.18 -141.77,-238.74 z m -325.8,1410.14 c -76.5,-133.74 -157.479,-262.83 -233.784,-392.96 -25.332,-43.2 -57.761,-87.1 -77.086,-131.8 -50.343,-116.47 -0.851,-226.72 42.266,-308.41 137.602,-260.55 278.474,-523.3 415.334,-773.46 13.71,124.29 86.65,215.53 141.77,310.88 167.68,290.08 327.8,575.06 499.9,863.01 93.53,-52.38 186.41,-118.66 278.54,-164.15 -135.06,216.08 -272.93,436.74 -407.88,656.57 -15.15,24.7 -42.29,85.59 -62.17,92.03 -21.62,7 -67.46,0 -104.45,0 -238.34,0 -531.63,-0.02 -738.649,0 77.593,-55.05 167.129,-98.16 246.209,-151.71 z m 1345.48,-872.95 c -173.7,95.71 -348.89,211.51 -524.76,313.37 -102.26,-172.15 -203.97,-344.85 -300.92,-522.28 368.91,0 751.06,0 1111.7,0 0,-384.68 0,-769.32 0,-1153.98 -322.47,0 -602.15,0 -907.77,0 -293.56,0 -455.02,139.48 -544.66,360.61 -40.87,100.82 -101.41,198.5 -154.19,298.46 -130.44,246.98 -258.828,486.89 -392.964,738.64 -37.481,70.34 -61.715,131.77 -54.711,236.27 8.261,123.16 82.304,205.41 136.781,295.95 58.137,96.62 107.676,189.42 161.66,271.09 -145.043,96.19 -312.867,182.88 -452.637,276.05 300.981,0 606.771,0 910.261,0 103.02,0 204.66,-5.91 303.42,0 7.03,0.44 5.98,1.86 9.95,2.5 8.82,1.41 34.47,-41.84 47.24,-62.18 218.06,-347.29 435.03,-714.78 651.6,-1054.5"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
id="path9862" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB