Did I tell you I was paranoid
This commit is contained in:
		
							parent
							
								
									4de6254f08
								
							
						
					
					
						commit
						0ec4d86221
					
				
					 23 changed files with 388 additions and 299 deletions
				
			
		
							
								
								
									
										15
									
								
								.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.sqlx/query-0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||
| } | ||||
							
								
								
									
										16
									
								
								.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.sqlx/query-25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||
| } | ||||
|  | @ -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" | ||||
| } | ||||
							
								
								
									
										22
									
								
								.sqlx/query-3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.sqlx/query-3d508af27179a4888500fcc2440adc47c12c87a780aa9a95479547d6c4d3972c.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||
| } | ||||
							
								
								
									
										22
									
								
								.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.sqlx/query-44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc.json
									
										
									
										generated
									
									
									
										Normal 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" | ||||
| } | ||||
|  | @ -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" | ||||
| } | ||||
|  | @ -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" | ||||
| } | ||||
|  | @ -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" | ||||
| } | ||||
|  | @ -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
									
									
									
								
							
							
						
						
									
										149
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							|  | @ -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]] | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -1 +0,0 @@ | |||
| DROP TABLE IF EXISTS link; | ||||
|  | @ -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 | ||||
| ); | ||||
|  | @ -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 | ||||
| ); | ||||
|  | @ -1,2 +1,3 @@ | |||
| DROP TABLE IF EXISTS file_key; | ||||
| DROP TABLE IF EXISTS file; | ||||
| DROP TABLE IF EXISTS link; | ||||
							
								
								
									
										16
									
								
								migrations/20240416191149_create-link-file.up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								migrations/20240416191149_create-link-file.up.sql
									
										
									
									
									
										Normal 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 | ||||
| ); | ||||
|  | @ -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<Config>, | ||||
| pub fn resource() -> Resource<SharedState> { | ||||
|     Resource::named("files") | ||||
|         .create(upload_file) | ||||
|         .show(get_file_info) | ||||
| } | ||||
| 
 | ||||
| pub fn router(db: PgPool, config: Arc<Config>) -> 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<Ulid>, | ||||
| } | ||||
| 
 | ||||
| #[instrument(skip(db, body))] | ||||
| async fn upload_file( | ||||
|     State(SharedState { db, config }): State<SharedState>, | ||||
|     TypedHeader(content_type): TypedHeader<ContentType>, | ||||
|     body: Body, | ||||
| ) -> Result<Json<UploadedFile>, AppError> { | ||||
|     let id_temp = Ulid::new(); | ||||
|     let file_path_temp = PathBuf::from("temp").join(id_temp.to_string()); | ||||
| ) -> Result<Json<File>, 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::<Mime>::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<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)), | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<SharedState> { | ||||
|     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<PgPool>, | ||||
|     State(SharedState { db, .. }): State<SharedState>, | ||||
|     Path(id): Path<Ulid>, | ||||
| ) -> Result<Json<Link>, 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<PgPool>, | ||||
|     State(SharedState { db, .. }): State<SharedState>, | ||||
|     Json(CreateLinkRequestBody { slug, destination }): Json<CreateLinkRequestBody>, | ||||
| ) -> Result<Json<Link>, 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<PgPool>, | ||||
|     State(SharedState { db, .. }): State<SharedState>, | ||||
|     Path(id): Path<Ulid>, | ||||
|     Json(UpdateLinkRequestBody { destination }): Json<UpdateLinkRequestBody>, | ||||
| ) -> Result<StatusCode, AppError> { | ||||
|     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<PgPool>, | ||||
|     State(SharedState { db, .. }): State<SharedState>, | ||||
|     Path(id): Path<Ulid>, | ||||
| ) -> 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) | ||||
|         .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)), | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -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<Config>) -> Router { | ||||
| pub fn router() -> Router<SharedState> { | ||||
|     Router::new() | ||||
|         .nest("/files", files::router(db.clone(), config)) | ||||
|         .nest("/links", links::router(db)) | ||||
|         .merge(files::resource()) | ||||
|         .merge(links::resource()) | ||||
| } | ||||
|  |  | |||
|  | @ -11,6 +11,12 @@ use tracing::{field, span, Level}; | |||
| 
 | ||||
| use crate::config::Config; | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| struct SharedState { | ||||
|     db: PgPool, | ||||
|     config: Arc<Config>, | ||||
| } | ||||
| 
 | ||||
| pub async fn build_app(config: Config) -> eyre::Result<Router> { | ||||
|     let db = PgPool::connect_with( | ||||
|         PgConnectOptions::new() | ||||
|  | @ -21,20 +27,22 @@ pub async fn build_app(config: Config) -> eyre::Result<Router> { | |||
|     ) | ||||
|     .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<Body>| { | ||||
|                     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()), | ||||
|         )) | ||||
| } | ||||
|  |  | |||
|  | @ -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<Config>, | ||||
| } | ||||
| 
 | ||||
| pub fn router(db: PgPool, config: Arc<Config>) -> Router { | ||||
| pub fn router() -> Router<SharedState> { | ||||
|     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<SharedState>, | ||||
|     Path(slug): Path<String>, | ||||
| ) -> 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) | ||||
|         .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<SharedState>, | ||||
|     Path(key): Path<Ulid>, | ||||
|     request: Request<Body>, | ||||
| ) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, 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> = 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> = 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())), | ||||
|  |  | |||
							
								
								
									
										18
									
								
								src/error.rs
									
										
									
									
									
								
							
							
						
						
									
										18
									
								
								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<Body> { | ||||
|         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", | ||||
|  |  | |||
							
								
								
									
										26
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										26
									
								
								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(()) | ||||
|         }) | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue