1
0
Fork 0

Did I tell you I was paranoid

This commit is contained in:
Honbra 2024-04-16 22:07:00 +02:00
parent 4de6254f08
commit 0ec4d86221
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
23 changed files with 388 additions and 299 deletions

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Bytea",
"Text"
]
},
"nullable": []
},
"hash": "25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b"
}

View file

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT id, destination FROM link WHERE slug = $1", "query": "SELECT id, hash, mime FROM file WHERE id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -10,19 +10,25 @@
}, },
{ {
"ordinal": 1, "ordinal": 1,
"name": "destination", "name": "hash",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "mime",
"type_info": "Text" "type_info": "Text"
} }
], ],
"parameters": { "parameters": {
"Left": [ "Left": [
"Text" "Uuid"
] ]
}, },
"nullable": [ "nullable": [
false,
false, false,
false false
] ]
}, },
"hash": "e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89" "hash": "3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551"
} }

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT destination FROM link WHERE slug = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "destination",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false
]
},
"hash": "3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c"
}

View file

@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM file_key WHERE file_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc"
}

View file

@ -1,14 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE link SET visit_count = visit_count + 1 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4d94b8d9c3af0a9cbddc706ee82869355500bfe6cb97f5e20e10ddfddd523136"
}

View file

@ -1,11 +1,11 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT file_hash, mime FROM file_key JOIN file ON file_hash = hash WHERE id = $1", "query": "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
"describe": { "describe": {
"columns": [ "columns": [
{ {
"ordinal": 0, "ordinal": 0,
"name": "file_hash", "name": "hash",
"type_info": "Bytea" "type_info": "Bytea"
}, },
{ {
@ -21,8 +21,8 @@
}, },
"nullable": [ "nullable": [
false, false,
true false
] ]
}, },
"hash": "d2a03886009405f5abe777c6f3b387df796d340a2119ede3b74bdeccf42c4f51" "hash": "7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3"
} }

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file (hash, mime) VALUES ($1, $2) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Bytea",
"Text"
]
},
"nullable": []
},
"hash": "9019613c29507ab3aacc861edc4acd1ec5b4a60f4cae5599557c9b54b19960ea"
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file_key (id, file_hash) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Bytea"
]
},
"nullable": []
},
"hash": "e3ba3d043ee6f16689304d82ec02a1444fddb6e43323769ccd0d42ea5d9570c0"
}

149
Cargo.lock generated
View file

@ -32,19 +32,19 @@ dependencies = [
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.16" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.79" version = "0.1.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -136,6 +136,7 @@ dependencies = [
"axum-core", "axum-core",
"bytes", "bytes",
"futures-util", "futures-util",
"headers",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -159,7 +160,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -239,9 +240,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.92" version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -335,9 +336,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -393,9 +394,9 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
[[package]] [[package]]
name = "figment" name = "figment"
version = "0.10.15" version = "0.10.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7270677e7067213e04f323b55084586195f18308cd7546cfac9f873344ccceb6" checksum = "752eb150770d6f51eb24d60e3ff84a2c24ccc5e5b3b0f550917ce5ec77c13fe4"
dependencies = [ dependencies = [
"atomic", "atomic",
"pear", "pear",
@ -557,6 +558,30 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@ -659,9 +684,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.2.0" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -876,6 +901,7 @@ dependencies = [
"eyre", "eyre",
"figment", "figment",
"futures-util", "futures-util",
"headers",
"hex", "hex",
"http", "http",
"http-body-util", "http-body-util",
@ -1041,7 +1067,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -1076,7 +1102,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -1126,9 +1152,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.79" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1141,16 +1167,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
"version_check", "version_check",
"yansi", "yansi",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.35" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1268,14 +1294,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.115" version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1631,9 +1657,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.58" version = "2.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1681,7 +1707,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -1734,7 +1760,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -1868,7 +1894,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]
@ -2051,7 +2077,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2073,7 +2099,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2141,7 +2167,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [ dependencies = [
"windows-targets 0.52.4", "windows-targets 0.52.5",
] ]
[[package]] [[package]]
@ -2161,17 +2187,18 @@ dependencies = [
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm 0.52.4", "windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.4", "windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.4", "windows_i686_gnu 0.52.5",
"windows_i686_msvc 0.52.4", "windows_i686_gnullvm",
"windows_x86_64_gnu 0.52.4", "windows_i686_msvc 0.52.5",
"windows_x86_64_gnullvm 0.52.4", "windows_x86_64_gnu 0.52.5",
"windows_x86_64_msvc 0.52.4", "windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@ -2182,9 +2209,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
@ -2194,9 +2221,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
@ -2206,9 +2233,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
@ -2218,9 +2251,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
@ -2230,9 +2263,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
@ -2242,9 +2275,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
@ -2254,15 +2287,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.4" version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.5" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -2290,7 +2323,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.58", "syn 2.0.59",
] ]
[[package]] [[package]]

View file

@ -5,11 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] } axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing"] }
axum-extra = { version = "0.9.3", features = ["async-read-body"] } axum-extra = { version = "0.9.3", features = ["async-read-body", "typed-header"] }
bytes = "1.6.0" bytes = "1.6.0"
eyre = "0.6.12" eyre = "0.6.12"
figment = { version = "0.10.15", features = ["env", "toml"] } figment = { version = "0.10.15", features = ["env", "toml"] }
futures-util = { version = "0.3.30", default-features = false } futures-util = { version = "0.3.30", default-features = false }
headers = "0.4.0"
hex = "0.4.3" hex = "0.4.3"
http = "1.1.0" http = "1.1.0"
http-body-util = "0.1.1" http-body-util = "0.1.1"

View file

@ -1 +0,0 @@
DROP TABLE IF EXISTS link;

View file

@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS link (
id UUID PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
destination TEXT NOT NULL,
visit_count INT NOT NULL DEFAULT 0
);

View file

@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS file (
hash BYTEA PRIMARY KEY,
mime TEXT
);
CREATE TABLE IF NOT EXISTS file_key (
id UUID PRIMARY KEY,
file_hash BYTEA REFERENCES file (hash) NOT NULL,
expires_at TIMESTAMP
);

View file

@ -1,2 +1,3 @@
DROP TABLE IF EXISTS file_key; DROP TABLE IF EXISTS file_key;
DROP TABLE IF EXISTS file; DROP TABLE IF EXISTS file;
DROP TABLE IF EXISTS link;

View file

@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS link (
id UUID PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
destination TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file (
id UUID PRIMARY KEY,
hash BYTEA UNIQUE NOT NULL,
mime TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_key (
id UUID PRIMARY KEY,
file_id UUID REFERENCES file (id) NOT NULL
);

View file

@ -1,50 +1,51 @@
use std::{path::PathBuf, sync::Arc}; use std::path::PathBuf;
use axum::{body::Body, extract::State, routing::post, Json, Router}; use axum::{
body::Body,
extract::{Path, State},
Json,
};
use axum_extra::{routing::Resource, TypedHeader};
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use headers::ContentType;
use mime::Mime;
use serde::Serialize; use serde::Serialize;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use sqlx::{query, PgPool}; use sqlx::query;
use tokio::{ use tokio::{fs, io};
fs::{self, File},
io,
};
use tokio_util::io::StreamReader; use tokio_util::io::StreamReader;
use tracing::{error, field, info, instrument}; use tracing::{error, field, info, instrument};
use ulid::Ulid; use ulid::Ulid;
use uuid::Uuid; use uuid::Uuid;
use crate::{config::Config, error::AppError}; use crate::{app::SharedState, error::AppError};
#[derive(Clone)] pub fn resource() -> Resource<SharedState> {
struct SharedState { Resource::named("files")
db: PgPool, .create(upload_file)
config: Arc<Config>, .show(get_file_info)
} }
pub fn router(db: PgPool, config: Arc<Config>) -> Router { #[derive(Serialize)]
Router::new() struct File {
.route("/", post(upload_file)) id: Ulid,
.with_state(SharedState { db, config })
}
#[derive(Debug, Serialize)]
struct UploadedFile {
key: Ulid,
hash: String, hash: String,
mime: String,
keys: Vec<Ulid>,
} }
#[instrument(skip(db, body))] #[instrument(skip(db, body))]
async fn upload_file( async fn upload_file(
State(SharedState { db, config }): State<SharedState>, State(SharedState { db, config }): State<SharedState>,
TypedHeader(content_type): TypedHeader<ContentType>,
body: Body, body: Body,
) -> Result<Json<UploadedFile>, AppError> { ) -> Result<Json<File>, AppError> {
let id_temp = Ulid::new(); let id = Ulid::new();
let file_path_temp = PathBuf::from("temp").join(id_temp.to_string()); let path_temp = config.file_temp_dir.join(id.to_string());
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
{ {
let mut file_temp = File::create(&file_path_temp).await?; let mut file_temp = fs::File::create(&path_temp).await?;
let better_body = body let better_body = body
.into_data_stream() .into_data_stream()
@ -55,12 +56,12 @@ async fn upload_file(
if let Err(err) = io::copy(&mut reader, &mut file_temp).await { if let Err(err) = io::copy(&mut reader, &mut file_temp).await {
error!( error!(
err = field::display(&err), err = field::display(&err),
file_path = field::debug(&file_path_temp), file_path = field::debug(&path_temp),
"failed to copy file, removing", "failed to copy file, removing",
); );
drop(file_temp); drop(file_temp);
if let Err(err) = fs::remove_file(file_path_temp).await { if let Err(err) = fs::remove_file(path_temp).await {
error!( error!(
err = field::display(err), err = field::display(err),
"failed to remove failed upload file", "failed to remove failed upload file",
@ -73,16 +74,16 @@ async fn upload_file(
let hash = hasher.finalize(); let hash = hasher.finalize();
let hash_hex = hex::encode(hash); let hash_hex = hex::encode(hash);
let file_path_hash = PathBuf::from("files").join(&hash_hex); let path_hash = PathBuf::from("files").join(&hash_hex);
if fs::try_exists(&file_path_hash).await? { if fs::try_exists(&path_hash).await? {
info!(hash = hash_hex, "file already exists"); info!(hash = hash_hex, "file already exists");
if let Err(err) = fs::remove_file(&file_path_temp).await { if let Err(err) = fs::remove_file(&path_temp).await {
error!(err = field::display(&err), "failed to remove temp file"); error!(err = field::display(&err), "failed to remove temp file");
} }
} else if let Err(err) = fs::rename(&file_path_temp, &file_path_hash).await { } else if let Err(err) = fs::rename(&path_temp, &path_hash).await {
error!(err = field::display(&err), "failed to move finished file"); error!(err = field::display(&err), "failed to move finished file");
if let Err(err) = fs::remove_file(&file_path_temp).await { if let Err(err) = fs::remove_file(&path_temp).await {
error!( error!(
err = field::display(&err), err = field::display(&err),
"failed to remove file after failed move", "failed to remove file after failed move",
@ -91,27 +92,64 @@ async fn upload_file(
return Err(err.into()); return Err(err.into());
} }
let key = Ulid::new(); let mime = Into::<Mime>::into(content_type);
query!( let mime_str = mime.to_string();
"INSERT INTO file (hash, mime) VALUES ($1, $2) ON CONFLICT DO NOTHING",
&hash[..],
"video/mp4", // I was testing with a video lol
)
.execute(&db)
.await?;
let result = query!(
"INSERT INTO file_key (id, file_hash) VALUES ($1, $2)",
Uuid::from(key),
&hash[..],
)
.execute(&db)
.await?;
match result.rows_affected() { match query!(
1 => Ok(Json(UploadedFile { "INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
key, Uuid::from(id),
&hash[..],
mime_str,
)
.execute(&db)
.await?
.rows_affected()
{
0 | 1 => {}
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
let key = Ulid::new();
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key),
Uuid::from(id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(Json(File {
id,
hash: hash_hex, hash: hash_hex,
mime: mime_str,
keys: vec![key],
})), })),
rows => Err(AppError::ImpossibleAffectedRows(rows)), rows => Err(AppError::ImpossibleAffectedRows(rows)),
} }
} }
async fn get_file_info(
State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>,
) -> Result<Json<File>, AppError> {
let (file, keys) = tokio::try_join!(
query!(
"SELECT id, hash, mime FROM file WHERE id = $1",
Uuid::from(id),
)
.fetch_optional(&db),
query!("SELECT id FROM file_key WHERE file_id = $1", Uuid::from(id)).fetch_all(&db),
)?;
match file {
Some(r) => Ok(Json(File {
id,
hash: hex::encode(r.hash),
mime: r.mime,
keys: keys.into_iter().map(|r| r.id.into()).collect(),
})),
None => Err(AppError::FileNotFoundId(id)),
}
}

View file

@ -1,25 +1,23 @@
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
Json, Router, Json,
}; };
use axum_extra::routing::Resource; use axum_extra::routing::Resource;
use http::StatusCode; use http::StatusCode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{query, PgPool}; use sqlx::query;
use ulid::Ulid; use ulid::Ulid;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::error::AppError; use crate::{app::SharedState, error::AppError};
pub fn router(db: PgPool) -> Router { pub fn resource() -> Resource<SharedState> {
let links = Resource::named("links") Resource::named("links")
.create(create_link) .create(create_link)
.show(get_link_info) .show(get_link_info)
.update(update_link) .update(update_link)
.destroy(delete_link); .destroy(delete_link)
Router::new().merge(links).with_state(db)
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -30,21 +28,23 @@ struct Link {
} }
async fn get_link_info( async fn get_link_info(
State(db): State<PgPool>, State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
) -> Result<Json<Link>, AppError> { ) -> Result<Json<Link>, AppError> {
let link = query!( match query!(
"SELECT id, slug, destination FROM link WHERE id = $1", "SELECT id, slug, destination FROM link WHERE id = $1",
Uuid::from(id), Uuid::from(id),
) )
.fetch_one(&db) .fetch_optional(&db)
.await?; .await?
{
Ok(Json(Link { Some(r) => Ok(Json(Link {
id: Ulid::from(link.id), id: Ulid::from(r.id),
slug: link.slug, slug: r.slug,
destination: link.destination, destination: r.destination,
})) })),
None => Err(AppError::LinkNotFoundId(id)),
}
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -54,27 +54,27 @@ struct CreateLinkRequestBody {
} }
async fn create_link( async fn create_link(
State(db): State<PgPool>, State(SharedState { db, .. }): State<SharedState>,
Json(CreateLinkRequestBody { slug, destination }): Json<CreateLinkRequestBody>, Json(CreateLinkRequestBody { slug, destination }): Json<CreateLinkRequestBody>,
) -> Result<Json<Link>, AppError> { ) -> Result<Json<Link>, AppError> {
let id = Ulid::new(); let id = Ulid::new();
let result = query!( match query!(
"INSERT INTO link (id, slug, destination) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", "INSERT INTO link (id, slug, destination) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
Uuid::from(id), Uuid::from(id),
slug, slug,
destination.to_string(), destination.to_string(),
) )
.execute(&db) .execute(&db)
.await?; .await?
.rows_affected()
match result.rows_affected() { {
1 => Ok(Json(Link { 1 => Ok(Json(Link {
id, id,
slug, slug,
destination: destination.to_string(), destination: destination.to_string(),
})), })),
0 => Err(AppError::ApiLinkExists(id)), 0 => Err(AppError::LinkExists(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)), rows => Err(AppError::ImpossibleAffectedRows(rows)),
} }
} }
@ -85,36 +85,36 @@ struct UpdateLinkRequestBody {
} }
async fn update_link( async fn update_link(
State(db): State<PgPool>, State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
Json(UpdateLinkRequestBody { destination }): Json<UpdateLinkRequestBody>, Json(UpdateLinkRequestBody { destination }): Json<UpdateLinkRequestBody>,
) -> Result<StatusCode, AppError> { ) -> Result<StatusCode, AppError> {
let result = query!( match query!(
"UPDATE link SET destination = $2 WHERE id = $1", "UPDATE link SET destination = $2 WHERE id = $1",
Uuid::from(id), Uuid::from(id),
destination.to_string(), destination.to_string(),
) )
.execute(&db) .execute(&db)
.await?; .await?
.rows_affected()
match result.rows_affected() { {
1 => Ok(StatusCode::NO_CONTENT), 1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::ApiLinkNotFound(id)), 0 => Err(AppError::LinkNotFoundId(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)), rows => Err(AppError::ImpossibleAffectedRows(rows)),
} }
} }
async fn delete_link( async fn delete_link(
State(db): State<PgPool>, State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>, Path(id): Path<Ulid>,
) -> Result<StatusCode, AppError> { ) -> Result<StatusCode, AppError> {
let result = query!("DELETE FROM link WHERE id = $1", Uuid::from(id)) match query!("DELETE FROM link WHERE id = $1", Uuid::from(id))
.execute(&db) .execute(&db)
.await?; .await?
.rows_affected()
match result.rows_affected() { {
1 => Ok(StatusCode::NO_CONTENT), 1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::ApiLinkNotFound(id)), 0 => Err(AppError::LinkNotFoundId(id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)), rows => Err(AppError::ImpossibleAffectedRows(rows)),
} }
} }

View file

@ -1,15 +1,12 @@
mod files; mod files;
mod links; mod links;
use std::sync::Arc;
use axum::Router; use axum::Router;
use sqlx::PgPool;
use crate::config::Config; use super::SharedState;
pub fn router(db: PgPool, config: Arc<Config>) -> Router { pub fn router() -> Router<SharedState> {
Router::new() Router::new()
.nest("/files", files::router(db.clone(), config)) .merge(files::resource())
.nest("/links", links::router(db)) .merge(links::resource())
} }

View file

@ -11,6 +11,12 @@ use tracing::{field, span, Level};
use crate::config::Config; use crate::config::Config;
#[derive(Clone)]
struct SharedState {
db: PgPool,
config: Arc<Config>,
}
pub async fn build_app(config: Config) -> eyre::Result<Router> { pub async fn build_app(config: Config) -> eyre::Result<Router> {
let db = PgPool::connect_with( let db = PgPool::connect_with(
PgConnectOptions::new() PgConnectOptions::new()
@ -21,20 +27,22 @@ pub async fn build_app(config: Config) -> eyre::Result<Router> {
) )
.await?; .await?;
let config = Arc::new(config); Ok(root::router()
.nest("/api", api::router())
Ok(root::router(db.clone(), config.clone()) .with_state(SharedState {
.nest("/api", api::router(db, config)) db,
config: Arc::new(config),
})
.layer( .layer(
TraceLayer::new_for_http() TraceLayer::new_for_http()
.make_span_with(|request: &Request<Body>| { .make_span_with(|request: &Request<Body>| {
span!( span!(
Level::INFO, Level::DEBUG,
"http-request", "http-request",
uri = field::display(request.uri()), uri = field::display(request.uri()),
) )
}) })
.on_request(DefaultOnRequest::new().level(Level::DEBUG)) .on_request(DefaultOnRequest::new())
.on_response(DefaultOnResponse::new().level(Level::INFO)), .on_response(DefaultOnResponse::new()),
)) ))
} }

View file

@ -1,5 +1,3 @@
use std::sync::Arc;
use axum::{ use axum::{
body::Body, body::Body,
extract::{Path, State}, extract::{Path, State},
@ -11,83 +9,52 @@ use bytes::Bytes;
use http::{Request, Response}; use http::{Request, Response};
use http_body_util::{combinators::UnsyncBoxBody, BodyExt}; use http_body_util::{combinators::UnsyncBoxBody, BodyExt};
use mime::Mime; use mime::Mime;
use sqlx::{query, PgPool}; use sqlx::query;
use tower_http::services::ServeFile; use tower_http::services::ServeFile;
use tracing::{error, field, instrument};
use ulid::Ulid; use ulid::Ulid;
use uuid::Uuid; use uuid::Uuid;
use crate::{config::Config, error::AppError}; use super::SharedState;
use crate::error::AppError;
#[derive(Clone)] pub fn router() -> Router<SharedState> {
struct SharedState {
db: PgPool,
config: Arc<Config>,
}
pub fn router(db: PgPool, config: Arc<Config>) -> Router {
Router::new() Router::new()
.route("/:slug", get(redirect_link)) .route("/:slug", get(redirect_link))
.route("/f/:key", get(redirect_file)) .route("/f/:key", get(download_file))
.with_state(SharedState { db, config })
} }
async fn redirect_link( async fn redirect_link(
State(SharedState { db, .. }): State<SharedState>, State(SharedState { db, .. }): State<SharedState>,
Path(slug): Path<String>, Path(slug): Path<String>,
) -> Result<Redirect, AppError> { ) -> Result<Redirect, AppError> {
let result = query!("SELECT id, destination FROM link WHERE slug = $1", slug) match query!("SELECT destination FROM link WHERE slug = $1", slug)
.fetch_optional(&db) .fetch_optional(&db)
.await? .await?
.map(|r| (Ulid::from(r.id), r.destination)); {
Some(r) => Ok(Redirect::temporary(&r.destination)),
match result { None => Err(AppError::LinkNotFoundSlug(slug)),
Some((id, destination)) => {
tokio::spawn(increase_visit_count(id, db));
Ok(Redirect::temporary(&destination))
}
None => Err(AppError::LinkNotFound(slug)),
} }
} }
#[instrument(skip(db))] async fn download_file(
async fn increase_visit_count(id: Ulid, db: PgPool) {
let result = query!(
"UPDATE link SET visit_count = visit_count + 1 WHERE id = $1",
Uuid::from(id),
)
.execute(&db)
.await;
match result {
Ok(result) if result.rows_affected() != 1 => {
error!(err = field::display(AppError::ImpossibleAffectedRows(result.rows_affected())));
}
Err(err) => error!(err = field::display(err)),
_ => {}
}
}
async fn redirect_file(
State(SharedState { db, config }): State<SharedState>, State(SharedState { db, config }): State<SharedState>,
Path(key): Path<Ulid>, Path(key): Path<Ulid>,
request: Request<Body>, request: Request<Body>,
) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, AppError> { ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, AppError> {
let result = query!( match query!(
"SELECT file_hash, mime FROM file_key JOIN file ON file_hash = hash WHERE id = $1", "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
Uuid::from(key) Uuid::from(key),
) )
.fetch_optional(&db) .fetch_optional(&db)
.await? .await?
.map(|r| (r.file_hash, r.mime)); .map(|r| (r.hash, r.mime))
{
match result { Some((hash, mime)) => {
Some((file_hash, mime)) => { let mime: Option<Mime> = mime.parse().ok();
let mime: Option<Mime> = mime.map_or(None, |m| m.parse().ok()); let path = config.file_store_dir.join(hex::encode(hash));
let file_path = config.file_store_dir.join(hex::encode(file_hash));
let mut sf = match mime { let mut sf = match mime {
Some(mime) => ServeFile::new_with_mime(file_path, &mime), Some(mime) => ServeFile::new_with_mime(path, &mime),
None => ServeFile::new(file_path), None => ServeFile::new(path),
}; };
match sf.try_call(request).await { match sf.try_call(request).await {
Ok(response) => Ok(response.map(|body| body.map_err(Into::into).boxed_unsync())), Ok(response) => Ok(response.map(|body| body.map_err(Into::into).boxed_unsync())),

View file

@ -1,3 +1,5 @@
use std::path::PathBuf;
use axum::{body::Body, response::IntoResponse}; use axum::{body::Body, response::IntoResponse};
use http::StatusCode; use http::StatusCode;
use tracing::{error, field}; use tracing::{error, field};
@ -6,13 +8,17 @@ use ulid::Ulid;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AppError { pub enum AppError {
#[error("link already exists ({0})")] #[error("link already exists ({0})")]
ApiLinkExists(Ulid), LinkExists(Ulid),
#[error("link not found ({0})")] #[error("link not found ({0})")]
ApiLinkNotFound(Ulid), LinkNotFoundId(Ulid),
#[error("link not found ({0})")] #[error("link not found ({0})")]
LinkNotFound(String), LinkNotFoundSlug(String),
#[error("file not found ({0})")]
FileNotFoundId(Ulid),
#[error("file key not found ({0})")] #[error("file key not found ({0})")]
FileKeyNotFound(Ulid), FileKeyNotFound(Ulid),
#[error("file is missing ({0})")]
FileMissing(PathBuf),
#[error("database returned an impossible number of affected rows ({0})")] #[error("database returned an impossible number of affected rows ({0})")]
ImpossibleAffectedRows(u64), ImpossibleAffectedRows(u64),
#[error("database error")] #[error("database error")]
@ -27,11 +33,13 @@ impl IntoResponse for AppError {
fn into_response(self) -> axum::http::Response<Body> { fn into_response(self) -> axum::http::Response<Body> {
error!(err = field::display(&self)); error!(err = field::display(&self));
match self { match self {
Self::ApiLinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists"), Self::LinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists"),
Self::ApiLinkNotFound(_) | Self::LinkNotFound(_) => { Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => {
(StatusCode::NOT_FOUND, "Link not found") (StatusCode::NOT_FOUND, "Link not found")
} }
Self::FileNotFoundId(_) => (StatusCode::NOT_FOUND, "File not found"),
Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"), Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"),
Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"),
Self::ImpossibleAffectedRows(_) => ( Self::ImpossibleAffectedRows(_) => (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"Database returned an impossible number of affected rows", "Database returned an impossible number of affected rows",

View file

@ -36,9 +36,9 @@ fn main() -> eyre::Result<()> {
.extract() .extract()
.context("failed to parse config")?; .context("failed to parse config")?;
let rt = Runtime::new().context("failed to create tokio runtime")?; Runtime::new()
.context("failed to create tokio runtime")?
rt.block_on(async move { .block_on(async move {
let listen_addr = config.listen_addr; let listen_addr = config.listen_addr;
let app = build_app(config).await.context("failed to build app")?; let app = build_app(config).await.context("failed to build app")?;