diff --git a/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json b/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json new file mode 100644 index 0000000..7a64f17 --- /dev/null +++ b/.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json @@ -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" +} diff --git a/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json b/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json new file mode 100644 index 0000000..4425075 --- /dev/null +++ b/.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json @@ -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" +} diff --git a/.sqlx/query-e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89.json b/.sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json similarity index 54% rename from .sqlx/query-e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89.json rename to .sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json index 1440bd5..02b3740 100644 --- a/.sqlx/query-e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89.json +++ b/.sqlx/query-3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, destination FROM link WHERE slug = $1", + "query": "SELECT id, hash, mime FROM file WHERE id = $1", "describe": { "columns": [ { @@ -10,19 +10,25 @@ }, { "ordinal": 1, - "name": "destination", + "name": "hash", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "mime", "type_info": "Text" } ], "parameters": { "Left": [ - "Text" + "Uuid" ] }, "nullable": [ + false, false, false ] }, - "hash": "e83004dd947b684af5ea9319fe136910e75d96a24800fd884c6cb3c1b6a03a89" + "hash": "3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551" } diff --git a/.sqlx/query-3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c.json b/.sqlx/query-3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c.json new file mode 100644 index 0000000..311bab8 --- /dev/null +++ b/.sqlx/query-3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c.json @@ -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" +} diff --git a/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json b/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json new file mode 100644 index 0000000..2037b55 --- /dev/null +++ b/.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json @@ -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" +} diff --git a/.sqlx/query-4d94b8d9c3af0a9cbddc706ee82869355500bfe6cb97f5e20e10ddfddd523136.json b/.sqlx/query-4d94b8d9c3af0a9cbddc706ee82869355500bfe6cb97f5e20e10ddfddd523136.json deleted file mode 100644 index 531ef32..0000000 --- a/.sqlx/query-4d94b8d9c3af0a9cbddc706ee82869355500bfe6cb97f5e20e10ddfddd523136.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-d2a03886009405f5abe777c6f3b387df796d340a2119ede3b74bdeccf42c4f51.json b/.sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json similarity index 61% rename from .sqlx/query-d2a03886009405f5abe777c6f3b387df796d340a2119ede3b74bdeccf42c4f51.json rename to .sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json index 3c815e6..89d45bd 100644 --- a/.sqlx/query-d2a03886009405f5abe777c6f3b387df796d340a2119ede3b74bdeccf42c4f51.json +++ b/.sqlx/query-7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3.json @@ -1,11 +1,11 @@ { "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": { "columns": [ { "ordinal": 0, - "name": "file_hash", + "name": "hash", "type_info": "Bytea" }, { @@ -21,8 +21,8 @@ }, "nullable": [ false, - true + false ] }, - "hash": "d2a03886009405f5abe777c6f3b387df796d340a2119ede3b74bdeccf42c4f51" + "hash": "7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3" } diff --git a/.sqlx/query-9019613c29507ab3aacc861edc4acd1ec5b4a60f4cae5599557c9b54b19960ea.json b/.sqlx/query-9019613c29507ab3aacc861edc4acd1ec5b4a60f4cae5599557c9b54b19960ea.json deleted file mode 100644 index 98a8285..0000000 --- a/.sqlx/query-9019613c29507ab3aacc861edc4acd1ec5b4a60f4cae5599557c9b54b19960ea.json +++ /dev/null @@ -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" -} diff --git a/.sqlx/query-e3ba3d043ee6f16689304d82ec02a1444fddb6e43323769ccd0d42ea5d9570c0.json b/.sqlx/query-e3ba3d043ee6f16689304d82ec02a1444fddb6e43323769ccd0d42ea5d9570c0.json deleted file mode 100644 index aac5f02..0000000 --- a/.sqlx/query-e3ba3d043ee6f16689304d82ec02a1444fddb6e43323769ccd0d42ea5d9570c0.json +++ /dev/null @@ -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" -} diff --git a/Cargo.lock b/Cargo.lock index 857abee..e2d9947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,19 +32,19 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -136,6 +136,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", + "headers", "http", "http-body", "http-body-util", @@ -159,7 +160,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -239,9 +240,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" +checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" [[package]] name = "cfg-if" @@ -335,9 +336,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" dependencies = [ "serde", ] @@ -393,9 +394,9 @@ checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "figment" -version = "0.10.15" +version = "0.10.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7270677e7067213e04f323b55084586195f18308cd7546cfac9f873344ccceb6" +checksum = "752eb150770d6f51eb24d60e3ff84a2c24ccc5e5b3b0f550917ce5ec77c13fe4" dependencies = [ "atomic", "pear", @@ -557,6 +558,30 @@ dependencies = [ "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]] name = "heck" version = "0.4.1" @@ -659,9 +684,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", @@ -876,6 +901,7 @@ dependencies = [ "eyre", "figment", "futures-util", + "headers", "hex", "http", "http-body-util", @@ -1041,7 +1067,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1076,7 +1102,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1126,9 +1152,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" dependencies = [ "unicode-ident", ] @@ -1141,16 +1167,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "version_check", "yansi", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1268,14 +1294,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -1631,9 +1657,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" dependencies = [ "proc-macro2", "quote", @@ -1681,7 +1707,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1734,7 +1760,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -1868,7 +1894,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] @@ -2051,7 +2077,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "wasm-bindgen-shared", ] @@ -2073,7 +2099,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2141,7 +2167,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -2161,17 +2187,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -2182,9 +2209,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -2194,9 +2221,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -2206,9 +2233,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" 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]] name = "windows_i686_msvc" @@ -2218,9 +2251,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -2230,9 +2263,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -2242,9 +2275,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -2254,15 +2287,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" dependencies = [ "memchr", ] @@ -2290,7 +2323,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.59", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7c59b52..44b6fb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,12 @@ edition = "2021" [dependencies] 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" eyre = "0.6.12" figment = { version = "0.10.15", features = ["env", "toml"] } futures-util = { version = "0.3.30", default-features = false } +headers = "0.4.0" hex = "0.4.3" http = "1.1.0" http-body-util = "0.1.1" diff --git a/migrations/20240128201312_create-link.down.sql b/migrations/20240128201312_create-link.down.sql deleted file mode 100644 index 76be25e..0000000 --- a/migrations/20240128201312_create-link.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS link; diff --git a/migrations/20240128201312_create-link.up.sql b/migrations/20240128201312_create-link.up.sql deleted file mode 100644 index faad003..0000000 --- a/migrations/20240128201312_create-link.up.sql +++ /dev/null @@ -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 -); diff --git a/migrations/20240414181705_create-file.up.sql b/migrations/20240414181705_create-file.up.sql deleted file mode 100644 index 0305b34..0000000 --- a/migrations/20240414181705_create-file.up.sql +++ /dev/null @@ -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 -); diff --git a/migrations/20240414181705_create-file.down.sql b/migrations/20240416191149_create-link-file.down.sql similarity index 68% rename from migrations/20240414181705_create-file.down.sql rename to migrations/20240416191149_create-link-file.down.sql index 3e94436..ccb05ce 100644 --- a/migrations/20240414181705_create-file.down.sql +++ b/migrations/20240416191149_create-link-file.down.sql @@ -1,2 +1,3 @@ DROP TABLE IF EXISTS file_key; DROP TABLE IF EXISTS file; +DROP TABLE IF EXISTS link; diff --git a/migrations/20240416191149_create-link-file.up.sql b/migrations/20240416191149_create-link-file.up.sql new file mode 100644 index 0000000..1f9dbe5 --- /dev/null +++ b/migrations/20240416191149_create-link-file.up.sql @@ -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 +); diff --git a/src/app/api/files.rs b/src/app/api/files.rs index a09d3e7..f82341e 100644 --- a/src/app/api/files.rs +++ b/src/app/api/files.rs @@ -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 headers::ContentType; +use mime::Mime; use serde::Serialize; use sha2::{Digest, Sha256}; -use sqlx::{query, PgPool}; -use tokio::{ - fs::{self, File}, - io, -}; +use sqlx::query; +use tokio::{fs, io}; use tokio_util::io::StreamReader; use tracing::{error, field, info, instrument}; use ulid::Ulid; use uuid::Uuid; -use crate::{config::Config, error::AppError}; +use crate::{app::SharedState, error::AppError}; -#[derive(Clone)] -struct SharedState { - db: PgPool, - config: Arc, +pub fn resource() -> Resource { + Resource::named("files") + .create(upload_file) + .show(get_file_info) } -pub fn router(db: PgPool, config: Arc) -> Router { - Router::new() - .route("/", post(upload_file)) - .with_state(SharedState { db, config }) -} - -#[derive(Debug, Serialize)] -struct UploadedFile { - key: Ulid, +#[derive(Serialize)] +struct File { + id: Ulid, hash: String, + mime: String, + keys: Vec, } #[instrument(skip(db, body))] async fn upload_file( State(SharedState { db, config }): State, + TypedHeader(content_type): TypedHeader, body: Body, -) -> Result, AppError> { - let id_temp = Ulid::new(); - let file_path_temp = PathBuf::from("temp").join(id_temp.to_string()); +) -> Result, AppError> { + let id = Ulid::new(); + let path_temp = config.file_temp_dir.join(id.to_string()); 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 .into_data_stream() @@ -55,12 +56,12 @@ async fn upload_file( if let Err(err) = io::copy(&mut reader, &mut file_temp).await { error!( err = field::display(&err), - file_path = field::debug(&file_path_temp), + file_path = field::debug(&path_temp), "failed to copy file, removing", ); 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!( err = field::display(err), "failed to remove failed upload file", @@ -73,16 +74,16 @@ async fn upload_file( let hash = hasher.finalize(); 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"); - 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"); } - } 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"); - 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 file after failed move", @@ -91,27 +92,64 @@ async fn upload_file( return Err(err.into()); } - let key = Ulid::new(); - query!( - "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?; + let mime = Into::::into(content_type); + let mime_str = mime.to_string(); - match result.rows_affected() { - 1 => Ok(Json(UploadedFile { - key, + match query!( + "INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", + 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, + mime: mime_str, + keys: vec![key], })), rows => Err(AppError::ImpossibleAffectedRows(rows)), } } + +async fn get_file_info( + State(SharedState { db, .. }): State, + Path(id): Path, +) -> Result, 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)), + } +} diff --git a/src/app/api/links.rs b/src/app/api/links.rs index 6593152..6be97f3 100644 --- a/src/app/api/links.rs +++ b/src/app/api/links.rs @@ -1,25 +1,23 @@ use axum::{ extract::{Path, State}, - Json, Router, + Json, }; use axum_extra::routing::Resource; use http::StatusCode; use serde::{Deserialize, Serialize}; -use sqlx::{query, PgPool}; +use sqlx::query; use ulid::Ulid; use url::Url; use uuid::Uuid; -use crate::error::AppError; +use crate::{app::SharedState, error::AppError}; -pub fn router(db: PgPool) -> Router { - let links = Resource::named("links") +pub fn resource() -> Resource { + Resource::named("links") .create(create_link) .show(get_link_info) .update(update_link) - .destroy(delete_link); - - Router::new().merge(links).with_state(db) + .destroy(delete_link) } #[derive(Serialize)] @@ -30,21 +28,23 @@ struct Link { } async fn get_link_info( - State(db): State, + State(SharedState { db, .. }): State, Path(id): Path, ) -> Result, AppError> { - let link = query!( + match query!( "SELECT id, slug, destination FROM link WHERE id = $1", Uuid::from(id), ) - .fetch_one(&db) - .await?; - - Ok(Json(Link { - id: Ulid::from(link.id), - slug: link.slug, - destination: link.destination, - })) + .fetch_optional(&db) + .await? + { + Some(r) => Ok(Json(Link { + id: Ulid::from(r.id), + slug: r.slug, + destination: r.destination, + })), + None => Err(AppError::LinkNotFoundId(id)), + } } #[derive(Deserialize)] @@ -54,27 +54,27 @@ struct CreateLinkRequestBody { } async fn create_link( - State(db): State, + State(SharedState { db, .. }): State, Json(CreateLinkRequestBody { slug, destination }): Json, ) -> Result, AppError> { let id = Ulid::new(); - let result = query!( + match query!( "INSERT INTO link (id, slug, destination) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING", Uuid::from(id), slug, destination.to_string(), ) .execute(&db) - .await?; - - match result.rows_affected() { + .await? + .rows_affected() + { 1 => Ok(Json(Link { id, slug, destination: destination.to_string(), })), - 0 => Err(AppError::ApiLinkExists(id)), + 0 => Err(AppError::LinkExists(id)), rows => Err(AppError::ImpossibleAffectedRows(rows)), } } @@ -85,36 +85,36 @@ struct UpdateLinkRequestBody { } async fn update_link( - State(db): State, + State(SharedState { db, .. }): State, Path(id): Path, Json(UpdateLinkRequestBody { destination }): Json, ) -> Result { - let result = query!( + match query!( "UPDATE link SET destination = $2 WHERE id = $1", Uuid::from(id), destination.to_string(), ) .execute(&db) - .await?; - - match result.rows_affected() { + .await? + .rows_affected() + { 1 => Ok(StatusCode::NO_CONTENT), - 0 => Err(AppError::ApiLinkNotFound(id)), + 0 => Err(AppError::LinkNotFoundId(id)), rows => Err(AppError::ImpossibleAffectedRows(rows)), } } async fn delete_link( - State(db): State, + State(SharedState { db, .. }): State, Path(id): Path, ) -> Result { - 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) - .await?; - - match result.rows_affected() { + .await? + .rows_affected() + { 1 => Ok(StatusCode::NO_CONTENT), - 0 => Err(AppError::ApiLinkNotFound(id)), + 0 => Err(AppError::LinkNotFoundId(id)), rows => Err(AppError::ImpossibleAffectedRows(rows)), } } diff --git a/src/app/api/mod.rs b/src/app/api/mod.rs index 9e49cae..e38bc37 100644 --- a/src/app/api/mod.rs +++ b/src/app/api/mod.rs @@ -1,15 +1,12 @@ mod files; mod links; -use std::sync::Arc; - use axum::Router; -use sqlx::PgPool; -use crate::config::Config; +use super::SharedState; -pub fn router(db: PgPool, config: Arc) -> Router { +pub fn router() -> Router { Router::new() - .nest("/files", files::router(db.clone(), config)) - .nest("/links", links::router(db)) + .merge(files::resource()) + .merge(links::resource()) } diff --git a/src/app/mod.rs b/src/app/mod.rs index defb304..e8f7b32 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,6 +11,12 @@ use tracing::{field, span, Level}; use crate::config::Config; +#[derive(Clone)] +struct SharedState { + db: PgPool, + config: Arc, +} + pub async fn build_app(config: Config) -> eyre::Result { let db = PgPool::connect_with( PgConnectOptions::new() @@ -21,20 +27,22 @@ pub async fn build_app(config: Config) -> eyre::Result { ) .await?; - let config = Arc::new(config); - - Ok(root::router(db.clone(), config.clone()) - .nest("/api", api::router(db, config)) + Ok(root::router() + .nest("/api", api::router()) + .with_state(SharedState { + db, + config: Arc::new(config), + }) .layer( TraceLayer::new_for_http() .make_span_with(|request: &Request| { span!( - Level::INFO, + Level::DEBUG, "http-request", uri = field::display(request.uri()), ) }) - .on_request(DefaultOnRequest::new().level(Level::DEBUG)) - .on_response(DefaultOnResponse::new().level(Level::INFO)), + .on_request(DefaultOnRequest::new()) + .on_response(DefaultOnResponse::new()), )) } diff --git a/src/app/root.rs b/src/app/root.rs index 8c4afb4..3f1d51a 100644 --- a/src/app/root.rs +++ b/src/app/root.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use axum::{ body::Body, extract::{Path, State}, @@ -11,83 +9,52 @@ use bytes::Bytes; use http::{Request, Response}; use http_body_util::{combinators::UnsyncBoxBody, BodyExt}; use mime::Mime; -use sqlx::{query, PgPool}; +use sqlx::query; use tower_http::services::ServeFile; -use tracing::{error, field, instrument}; use ulid::Ulid; use uuid::Uuid; -use crate::{config::Config, error::AppError}; +use super::SharedState; +use crate::error::AppError; -#[derive(Clone)] -struct SharedState { - db: PgPool, - config: Arc, -} - -pub fn router(db: PgPool, config: Arc) -> Router { +pub fn router() -> Router { Router::new() .route("/:slug", get(redirect_link)) - .route("/f/:key", get(redirect_file)) - .with_state(SharedState { db, config }) + .route("/f/:key", get(download_file)) } async fn redirect_link( State(SharedState { db, .. }): State, Path(slug): Path, ) -> Result { - 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) .await? - .map(|r| (Ulid::from(r.id), r.destination)); - - match result { - Some((id, destination)) => { - tokio::spawn(increase_visit_count(id, db)); - Ok(Redirect::temporary(&destination)) - } - None => Err(AppError::LinkNotFound(slug)), + { + Some(r) => Ok(Redirect::temporary(&r.destination)), + None => Err(AppError::LinkNotFoundSlug(slug)), } } -#[instrument(skip(db))] -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( +async fn download_file( State(SharedState { db, config }): State, Path(key): Path, request: Request, ) -> Result>, AppError> { - let result = query!( - "SELECT file_hash, mime FROM file_key JOIN file ON file_hash = hash WHERE id = $1", - Uuid::from(key) + match query!( + "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1", + Uuid::from(key), ) .fetch_optional(&db) .await? - .map(|r| (r.file_hash, r.mime)); - - match result { - Some((file_hash, mime)) => { - let mime: Option = mime.map_or(None, |m| m.parse().ok()); - let file_path = config.file_store_dir.join(hex::encode(file_hash)); + .map(|r| (r.hash, r.mime)) + { + Some((hash, mime)) => { + let mime: Option = mime.parse().ok(); + let path = config.file_store_dir.join(hex::encode(hash)); let mut sf = match mime { - Some(mime) => ServeFile::new_with_mime(file_path, &mime), - None => ServeFile::new(file_path), + Some(mime) => ServeFile::new_with_mime(path, &mime), + None => ServeFile::new(path), }; match sf.try_call(request).await { Ok(response) => Ok(response.map(|body| body.map_err(Into::into).boxed_unsync())), diff --git a/src/error.rs b/src/error.rs index 20291c5..214203e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use axum::{body::Body, response::IntoResponse}; use http::StatusCode; use tracing::{error, field}; @@ -6,13 +8,17 @@ use ulid::Ulid; #[derive(Debug, thiserror::Error)] pub enum AppError { #[error("link already exists ({0})")] - ApiLinkExists(Ulid), + LinkExists(Ulid), #[error("link not found ({0})")] - ApiLinkNotFound(Ulid), + LinkNotFoundId(Ulid), #[error("link not found ({0})")] - LinkNotFound(String), + LinkNotFoundSlug(String), + #[error("file not found ({0})")] + FileNotFoundId(Ulid), #[error("file key not found ({0})")] FileKeyNotFound(Ulid), + #[error("file is missing ({0})")] + FileMissing(PathBuf), #[error("database returned an impossible number of affected rows ({0})")] ImpossibleAffectedRows(u64), #[error("database error")] @@ -27,11 +33,13 @@ impl IntoResponse for AppError { fn into_response(self) -> axum::http::Response { error!(err = field::display(&self)); match self { - Self::ApiLinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists"), - Self::ApiLinkNotFound(_) | Self::LinkNotFound(_) => { + Self::LinkExists(_) => (StatusCode::BAD_REQUEST, "Link already exists"), + Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => { (StatusCode::NOT_FOUND, "Link not found") } + Self::FileNotFoundId(_) => (StatusCode::NOT_FOUND, "File not found"), Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"), + Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"), Self::ImpossibleAffectedRows(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "Database returned an impossible number of affected rows", diff --git a/src/main.rs b/src/main.rs index 017975d..dad1009 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,20 +36,20 @@ fn main() -> eyre::Result<()> { .extract() .context("failed to parse config")?; - let rt = Runtime::new().context("failed to create tokio runtime")?; + Runtime::new() + .context("failed to create tokio runtime")? + .block_on(async move { + let listen_addr = config.listen_addr; - rt.block_on(async move { - let listen_addr = config.listen_addr; + let app = build_app(config).await.context("failed to build app")?; + let listener = TcpListener::bind(&listen_addr) + .await + .context("failed to bind listener")?; - let app = build_app(config).await.context("failed to build app")?; - let listener = TcpListener::bind(&listen_addr) - .await - .context("failed to bind listener")?; + axum::serve(listener, app) + .await + .context("server encountered a runtime error")?; - axum::serve(listener, app) - .await - .context("server encountered a runtime error")?; - - Ok(()) - }) + Ok(()) + }) }