diff --git a/Cargo.lock b/Cargo.lock index a1a8768..a955c48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ahash" version = "0.7.6" @@ -25,6 +37,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109" +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base64" version = "0.13.0" @@ -40,6 +58,12 @@ dependencies = [ "base64", ] +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + [[package]] name = "bitflags" version = "0.9.1" @@ -80,6 +104,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "bytecheck" version = "0.6.8" @@ -101,6 +131,27 @@ dependencies = [ "syn", ] +[[package]] +name = "bytemuck" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -113,6 +164,66 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "crow" version = "0.1.0" @@ -120,11 +231,15 @@ dependencies = [ "anyhow", "base64-url", "blake2", + "image", "lmdb-zero", + "matchit", "multipart", "rkyv", + "thiserror", "tiny_http", "url", + "webp", ] [[package]] @@ -137,6 +252,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + [[package]] name = "digest" version = "0.10.3" @@ -148,6 +272,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "either" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" + +[[package]] +name = "exr" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" +dependencies = [ + "bit_field", + "deflate", + "flume", + "half", + "inflate", + "lebe", + "smallvec", + "threadpool", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -157,6 +303,29 @@ dependencies = [ "instant", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin", +] + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -167,6 +336,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + [[package]] name = "gcc" version = "0.3.55" @@ -190,10 +371,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.1" @@ -203,6 +402,15 @@ dependencies = [ "ahash", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "httparse" version = "1.7.1" @@ -220,6 +428,35 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + [[package]] name = "instant" version = "0.1.12" @@ -235,6 +472,45 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" + [[package]] name = "libc" version = "0.2.126" @@ -251,6 +527,15 @@ dependencies = [ "libc", ] +[[package]] +name = "libwebp-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439fd1885aa28937e7edcd68d2e793cb4a22f8733460d2519fbafd2b215672bf" +dependencies = [ + "cc", +] + [[package]] name = "lmdb-zero" version = "0.4.4" @@ -263,6 +548,16 @@ dependencies = [ "supercow", ] +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -278,12 +573,27 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfc802da7b1cf80aefffa0c7b2f77247c8b32206cc83c270b61264f5b360a80" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -300,10 +610,19 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "multipart" version = "0.18.0" -source = "git+https://github.com/emily-signet/multipart#8144529477faf92941206320395fc3798c14fdf7" +source = "git+https://github.com/emily-signet/multipart#3ae1cd313be14b94997405fef38e29a2edb60232" dependencies = [ "buf_redux", "httparse", @@ -318,6 +637,66 @@ dependencies = [ "twoway", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -339,6 +718,38 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "deflate", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -419,6 +830,30 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -477,12 +912,39 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "spin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +dependencies = [ + "lock_api", +] + [[package]] name = "subtle" version = "2.4.1" @@ -520,6 +982,46 @@ dependencies = [ "winapi", ] +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.11" @@ -541,8 +1043,7 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tiny_http" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca" +source = "git+https://github.com/emily-signet/tiny-http.git#6228d7b6559ef13f6a1a3f46f6f2cf0b306a386a" dependencies = [ "ascii", "chunked_transfer", @@ -604,9 +1105,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -635,6 +1136,76 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "webp" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf022f821f166079a407d000ab57e84de020e66ffbbf4edde999bc7d6e371cae" +dependencies = [ + "image", + "libwebp-sys", +] + +[[package]] +name = "weezl" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 93fe434..19374be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,12 @@ edition = "2021" anyhow = "1.0.58" base64-url = "1.4.13" blake2 = "0.10.4" +image = "0.24.2" lmdb-zero = "0.4.4" +matchit = "0.6.0" multipart = { git = "https://github.com/emily-signet/multipart", default-features = false, features = ["server", "tiny_http"] } rkyv = { version = "0.7.39", features = ["strict", "archive_le"] } -tiny_http = "0.11.0" +thiserror = "1.0.31" +tiny_http = { git = "https://github.com/emily-signet/tiny-http.git" } url = "2.2.2" +webp = "0.2.2" diff --git a/db/data.mdb b/db/data.mdb new file mode 100644 index 0000000..8abf0c6 Binary files /dev/null and b/db/data.mdb differ diff --git a/db/lock.mdb b/db/lock.mdb new file mode 100644 index 0000000..ca9aba6 Binary files /dev/null and b/db/lock.mdb differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c73edcf --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,237 @@ +use blake2::Digest; +use lmdb::{traits::FromLmdbBytes, Database, Environment}; +use lmdb_zero as lmdb; + +use tiny_http::Request; + +use std::{str::FromStr, sync::Arc}; +use thiserror::Error; + +pub trait Handler: Send + Sync { + fn respond<'u>( + &self, + req: Request, + db: &DatabaseContext, + params: matchit::Params<'u, 'u>, + ) -> CrowResult<()>; +} + +pub mod routes; + +#[derive(Error, Debug)] +pub enum CrowError { + #[error(transparent)] + LmdbError(#[from] lmdb::Error), + #[error(transparent)] + IOError(#[from] std::io::Error), + #[error(transparent)] + ImageError(#[from] image::error::ImageError), + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +pub type CrowResult = Result; + +#[derive(rkyv::Archive, rkyv::Serialize)] +pub struct FileHeader { + file_name: Option, + content_type: Option, +} + +impl FromLmdbBytes for ArchivedFileHeader { + fn from_lmdb_bytes(bytes: &[u8]) -> Result<&Self, String> { + Ok(unsafe { rkyv::archived_root::(bytes) }) + } +} + +#[derive(rkyv::Serialize, rkyv::Archive, Copy, Debug, Clone)] +#[archive(compare(PartialEq))] +#[archive_attr(derive(Copy, Clone))] +#[repr(u8)] +pub enum ImageFormat { + PNG, + WEBP, + JPG, +} + +impl FromStr for ImageFormat { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "png" | "PNG" => ImageFormat::PNG, + "webp" | "WEBP" => ImageFormat::WEBP, + "jpg" | "JPG" | "jpeg" | "JPEG" => ImageFormat::JPG, + _ => return Err(()), + }) + } +} + +impl ImageFormat { + pub const fn to_mime(&self) -> &'static str { + match *self { + ImageFormat::PNG => "image/png", + ImageFormat::JPG => "image/jpeg", + ImageFormat::WEBP => "image/webp", + } + } +} + +impl From for image::ImageFormat { + fn from(val: ImageFormat) -> Self { + match val { + ImageFormat::PNG => image::ImageFormat::Png, + ImageFormat::WEBP => image::ImageFormat::WebP, + ImageFormat::JPG => image::ImageFormat::Jpeg, + } + } +} + +impl From for image::ImageFormat { + fn from(val: ArchivedImageFormat) -> Self { + match val { + ArchivedImageFormat::PNG => image::ImageFormat::Png, + ArchivedImageFormat::WEBP => image::ImageFormat::WebP, + ArchivedImageFormat::JPG => image::ImageFormat::Jpeg, + } + } +} + +#[derive(rkyv::Serialize, rkyv::Archive)] +pub struct ImageHeader { + store_format: ImageFormat, +} + +impl FromLmdbBytes for ArchivedImageHeader { + fn from_lmdb_bytes(bytes: &[u8]) -> Result<&Self, String> { + Ok(unsafe { rkyv::archived_root::(bytes) }) + } +} + +#[derive(Clone)] +pub struct DatabaseContext { + pub env: Arc, + pub metadata_store: Arc>, + pub binary_store: Arc>, + pub image_store: Arc>, + pub image_meta_store: Arc>, +} + +impl DatabaseContext { + pub fn create(path: &str) -> CrowResult { + let env = Arc::new(unsafe { + let mut builder = lmdb::EnvBuilder::new()?; + builder.set_maxdbs(4)?; + builder.open(path, lmdb::open::NOTLS, 0o600)? + }); + + let binary_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("binary"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let image_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("image"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let metadata_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("metadata"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + let image_meta_store = Arc::new(lmdb::Database::open( + env.clone(), + Some("metadata-image"), + &lmdb::DatabaseOptions::new(lmdb::db::CREATE), + )?); + + Ok(DatabaseContext { + env, + binary_store, + metadata_store, + image_meta_store, + image_store, + }) + } + + #[inline] + pub fn write_txn(&self) -> CrowResult> { + lmdb::WriteTransaction::new(self.env.clone()).map_err(CrowError::from) + } + + #[inline] + pub fn read_txn(&self) -> CrowResult> { + lmdb::ReadTransaction::new(self.env.clone()).map_err(CrowError::from) + } +} + +mod macros { + macro_rules! response { + (err $status:literal $msg:literal) => { + Response::new( + tiny_http::StatusCode($status), + vec![], + $msg.as_bytes(), + None, + None, + ) + }; + } + + macro_rules! single_multipart { + ($req:expr) => { + Multipart::from_request($req) + .ok() + .and_then(|v| v.into_entry().into_result().ok()) + .flatten() + }; + } + + macro_rules! fn_to_handler { + ($f:ident : $handler_name:ident) => { + pub struct $handler_name; + + impl Handler for $handler_name { + fn respond<'u>( + &self, + req: Request, + db: &DatabaseContext, + params: matchit::Params<'u, 'u>, + ) -> CrowResult<()> { + $f(req, db, params) + } + } + }; + } + + macro_rules! parse_base64_hash { + ($fr:expr) => { + $fr.and_then(|s| { + let mut out: [u8; 32] = [0; 32]; + base64_url::decode_to_slice(s, &mut out).ok()?; + Some(out) + }) + }; + } + + macro_rules! some_or_response { + ($opt:expr, or respond to $req:ident with $response:expr) => { + match $opt { + Some(v) => v, + None => { + $req.respond($response)?; + return Ok(()); + } + } + }; + } + + pub(crate) use fn_to_handler; + pub(crate) use parse_base64_hash; + pub(crate) use response; + pub(crate) use single_multipart; + pub(crate) use some_or_response; +} + +pub(crate) use macros::*; diff --git a/src/main.rs b/src/main.rs index 961795d..aab5706 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,70 +1,7 @@ -extern crate multipart; -extern crate tiny_http; +use std::{sync::Arc, thread}; -use blake2::{Blake2s256, Digest}; -use lmdb::{Database, Environment, LmdbResultExt}; -use lmdb_zero as lmdb; -use multipart::server::Multipart; -use rkyv::option::ArchivedOption; -use std::{io::Read, sync::Arc, thread}; -use tiny_http::{Header, Request, Response}; - -#[derive(rkyv::Archive, rkyv::Serialize)] -pub struct FileHeader { - file_name: Option, - content_type: Option, -} - -#[derive(Clone)] -pub struct DatabaseContext { - env: Arc, - metadata_store: Arc>, - binary_store: Arc>, - image_store: Arc>, -} - -impl DatabaseContext { - pub fn create(path: &str) -> anyhow::Result { - let env = Arc::new(unsafe { - let mut builder = lmdb::EnvBuilder::new()?; - builder.set_maxdbs(3)?; - builder.open(path, lmdb::open::NOTLS, 0o600)? - }); - - let binary_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("binary"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - let image_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("image"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - let metadata_store = Arc::new(lmdb::Database::open( - env.clone(), - Some("metadata"), - &lmdb::DatabaseOptions::new(lmdb::db::CREATE), - )?); - - Ok(DatabaseContext { - env, - binary_store, - metadata_store, - image_store, - }) - } - - #[inline] - pub fn write_txn(&self) -> anyhow::Result> { - lmdb::WriteTransaction::new(self.env.clone()).map_err(anyhow::Error::from) - } - - #[inline] - pub fn read_txn(&self) -> anyhow::Result> { - lmdb::ReadTransaction::new(self.env.clone()).map_err(anyhow::Error::from) - } -} +use crow::*; +use matchit::Router; fn main() -> anyhow::Result<()> { let database_context = DatabaseContext::create("./db")?; @@ -74,22 +11,25 @@ fn main() -> anyhow::Result<()> { let mut guards = Vec::with_capacity(4); + // router.insert("/upload", &routes::upload::upload_route); + // router.insert("/image", &routes::image::image_route); + for _ in 0..4 { let server = server.clone(); let db = database_context.clone(); + let mut router: Router<&dyn Handler> = Router::new(); + router.insert("/get/:id", &routes::get::GetHandler)?; + router.insert("/upload", &routes::upload::UploadHandler)?; + router.insert("/image/upload", &routes::image::ImageUploadHandler)?; + router.insert("/image/get/:id/:format", &routes::image::ImageGetHandler)?; let guard = thread::spawn(move || loop { let request = server.recv().unwrap(); + // i don't like this alloc + let url = request.url().to_owned(); - match request.url() { - "/upload" => { - process_upload(request, &db).unwrap(); - } - s if s.starts_with("/get/") => { - process_get(request, &db).unwrap(); - } - _ => todo!(), - } + let matched = router.at(&url).unwrap(); + matched.value.respond(request, &db, matched.params).unwrap(); }); guards.push(guard); @@ -101,108 +41,3 @@ fn main() -> anyhow::Result<()> { Ok(()) } - -fn process_upload(mut request: Request, db_context: &DatabaseContext) -> anyhow::Result<()> { - if let Some(mut entry) = Multipart::from_request(&mut request) - .ok() - .and_then(|v| v.into_entry().into_result().ok()) - .flatten() - { - let mut data: Vec = Vec::with_capacity(20000); - entry.data.read_to_end(&mut data)?; - - let data_hash = Blake2s256::digest(&data); - - let txn = db_context.write_txn()?; - - let mut accessor = txn.access(); - - if accessor - .get::<[u8], [u8]>(&db_context.binary_store, data_hash.as_slice()) - .to_opt()? - .is_some() - { - request.respond(Response::from_string(base64_url::encode(&data_hash)))?; - return Ok(()); - } - - accessor.put( - &db_context.binary_store, - data_hash.as_slice(), - &data, - lmdb::put::Flags::empty(), - )?; - - let header = FileHeader { - file_name: entry.headers.filename, - content_type: entry - .headers - .content_type - .map(|v| v.essence_str().to_owned()), - }; - - accessor.put( - &db_context.metadata_store, - data_hash.as_slice(), - rkyv::to_bytes::<_, 256>(&header)?.as_slice(), - lmdb::put::Flags::empty(), - )?; - - request.respond(Response::from_string(base64_url::encode(&data_hash)))?; - - drop(accessor); - - txn.commit()?; - } - - Ok(()) -} - -fn process_get(request: Request, db_context: &DatabaseContext) -> anyhow::Result<()> { - if let Some(get_id) = request.url().strip_prefix("/get/").and_then(|s| { - let mut out: [u8; 32] = [0; 32]; - base64_url::decode_to_slice(s, &mut out).ok()?; - Some(out) - }) { - let read = db_context.read_txn()?; - let access = read.access(); - - if let Some(data) = access - .get::<[u8], [u8]>(&db_context.binary_store, &get_id) - .to_opt() - .unwrap() - { - let header_bytes = access - .get::<[u8], [u8]>(&db_context.metadata_store, &get_id) - .to_opt()? - .ok_or(anyhow::anyhow!("header missing for file"))?; - let header = unsafe { rkyv::archived_root::(header_bytes) }; - - let mut response = Response::new(200.into(), vec![], data, Some(data.len()), None); - - if let ArchivedOption::Some(ref file_name) = header.file_name { - response = response.with_header( - Header::from_bytes( - &b"Content-Disposition"[..], - format!("inline; filename=\"{file_name}\"").into_bytes(), - ) - .unwrap(), - ); - } - - if let ArchivedOption::Some(ref mime_type) = header.content_type { - response = response.with_header( - Header::from_bytes(&b"Content-Type"[..], mime_type.as_bytes()).unwrap(), - ); - } - - request.respond(response)?; - } else { - request.respond(Response::empty(404))?; - } - } else { - request.respond(Response::empty(400))?; - } - - Ok(()) -} diff --git a/src/routes/get.rs b/src/routes/get.rs new file mode 100644 index 0000000..3aea35a --- /dev/null +++ b/src/routes/get.rs @@ -0,0 +1,56 @@ +use crate::*; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; + +use rkyv::option::ArchivedOption; + +use tiny_http::{Header, Request, Response}; + +fn get<'u>( + request: Request, + db_context: &DatabaseContext, + params: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let get_id = some_or_response!( + parse_base64_hash!(params.get("id")), or respond to request with Response::empty(400) + ); + + let read = db_context.read_txn()?; + let access = read.access(); + + let data = some_or_response!( + access + .get::<[u8], [u8]>(&db_context.binary_store, &get_id) + .to_opt()?, + or respond to request with + Response::empty(404) + ); + + let header = access + .get::<[u8], ArchivedFileHeader>(&db_context.metadata_store, &get_id) + .to_opt()? + .ok_or(anyhow::anyhow!("header missing for file"))?; + + let mut response = Response::new(200.into(), vec![], data, Some(data.len()), None); + + if let ArchivedOption::Some(ref file_name) = header.file_name { + response = response.with_header( + Header::from_bytes( + &b"Content-Disposition"[..], + format!("inline; filename=\"{file_name}\"").into_bytes(), + ) + .unwrap(), + ); + } + + if let ArchivedOption::Some(ref mime_type) = header.content_type { + response = response + .with_header(Header::from_bytes(&b"Content-Type"[..], mime_type.as_bytes()).unwrap()); + } + + request.respond(response)?; + + Ok(()) +} + +fn_to_handler!(get: GetHandler); diff --git a/src/routes/image.rs b/src/routes/image.rs new file mode 100644 index 0000000..e6852e3 --- /dev/null +++ b/src/routes/image.rs @@ -0,0 +1,200 @@ +use crate::*; +use blake2::{Blake2s256, Digest}; +use image::{ + codecs::{jpeg::JpegEncoder, png::PngEncoder}, + ColorType, ImageEncoder, +}; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; +use multipart::server::Multipart; + +use std::{ + io::{ErrorKind, Read}, + ops::Deref, +}; +use tiny_http::{Header, Request, Response}; + +fn image_upload<'u>( + mut request: Request, + db_context: &DatabaseContext, + _: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let mut entry = some_or_response!( + single_multipart!(&mut request), or respond to request with Response::empty(400) + ); + + let mut data: Vec = Vec::with_capacity(20000); + entry.data.read_to_end(&mut data)?; + + let upload_format = image::guess_format(&data)?; + let img = image::load_from_memory_with_format(&data, upload_format)?; + + let data_hash = Blake2s256::digest(&img.as_bytes()); + + let txn = db_context.write_txn()?; + + let mut accessor = txn.access(); + + if accessor + .get::<[u8], [u8]>(&db_context.image_store, data_hash.as_slice()) + .to_opt()? + .is_some() + { + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + return Ok(()); + } + + let store_format = match upload_format { + image::ImageFormat::Jpeg => { + accessor.put( + &db_context.image_store, + data_hash.as_slice(), + &data, + lmdb::put::Flags::empty(), + )?; + + ImageFormat::JPG + } + _ => { + // TODO: allow lossy compression via option + let encoded = webp::Encoder::from_image(&img).unwrap().encode_lossless(); + accessor.put( + &db_context.image_store, + data_hash.as_slice(), + encoded.deref(), + lmdb::put::Flags::empty(), + )?; + + ImageFormat::WEBP + } + }; + + accessor.put( + &db_context.image_meta_store, + data_hash.as_slice(), + rkyv::to_bytes::<_, 256>(&ImageHeader { store_format }) + .unwrap() + .as_slice(), + lmdb::put::Flags::empty(), + )?; + + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + + drop(accessor); + + txn.commit()?; + + Ok(()) +} + +fn image_get<'u>( + mut request: Request, + db_context: &DatabaseContext, + params: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let (get_id, requested_format) = some_or_response!( + parse_base64_hash!(params.get("id")).zip( + params + .get("format") + .and_then(|s| ImageFormat::from_str(s).ok()) + ), or respond to request with Response::empty(400) + ); + + let content_type = requested_format.to_mime(); + + let read = db_context.read_txn()?; + let access = read.access(); + + let metadata: &ArchivedImageHeader = some_or_response!( + access + .get::<[u8], ArchivedImageHeader>(&db_context.image_meta_store, &get_id) + .to_opt()?, + or respond to request with + Response::empty(404) + ); + + let data = access.get::<[u8], [u8]>(&db_context.image_store, &get_id)?; + + if metadata.store_format == requested_format { + request.respond(Response::new( + 200.into(), + vec![], + data, + Some(data.len()), + None, + ))?; + + return Ok(()); + } + + let stored_image = image::load_from_memory_with_format(data, metadata.store_format.into())?; + + use ColorType::*; + + // catch unsupported transcoding (because of unsupported color format) + // and return a 500 + match (stored_image.color(), requested_format) { + (Rgb8 | Rgba8, ImageFormat::WEBP) => (), + (Rgb8 | Rgba8 | L8 | La8, ImageFormat::JPG) => (), + (Rgb8 | Rgba8 | L8 | La8 | Rgba16 | Rgb16 | L16 | La16, ImageFormat::PNG) => (), + _ => { + request.respond( + response!(err 500 "unsupported color channels for the requested image format"), + )?; + return Ok(()); + } + } + + let mut req_writer = request.extract_writer_impl(); + + let response = Response::new( + 200.into(), + vec![Header::from_bytes(&b"Content-Type"[..], content_type.as_bytes()).unwrap()], + std::io::empty(), + None, + None, + ); + Request::ignore_client_closing_errors(response.print_and_write( + &mut req_writer, + request.http_version().clone(), + request.headers(), + false, + None, + None, + |w, _| { + match requested_format { + ImageFormat::WEBP => { + // TODO: make compression quality here configurable + let mem = webp::Encoder::from_image(&stored_image) + .unwrap() + .encode(95.0); + w.write_all(mem.deref()) + } + ImageFormat::JPG => JpegEncoder::new(w) + .encode_image(&stored_image) + .map_err(|e| std::io::Error::new(ErrorKind::Other, e)), + ImageFormat::PNG => PngEncoder::new(w) + .write_image( + stored_image.as_bytes(), + stored_image.width(), + stored_image.height(), + stored_image.color(), + ) + .map_err(|e| std::io::Error::new(ErrorKind::Other, e)), + } + }, + ))?; + + Request::ignore_client_closing_errors(req_writer.flush())?; + if let Some(sender) = request.notify_when_responded.take() { + sender.send(()).unwrap(); + } + + drop(req_writer); + drop(request); + + Ok(()) +} + +fn_to_handler!(image_upload: ImageUploadHandler); +fn_to_handler!(image_get: ImageGetHandler); diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..4b224e2 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,3 @@ +pub mod get; +pub mod image; +pub mod upload; diff --git a/src/routes/upload.rs b/src/routes/upload.rs new file mode 100644 index 0000000..084a2e0 --- /dev/null +++ b/src/routes/upload.rs @@ -0,0 +1,68 @@ +use crate::*; +use blake2::{Blake2s256, Digest}; +use lmdb::LmdbResultExt; +use lmdb_zero as lmdb; +use multipart::server::Multipart; + +use std::io::Read; +use tiny_http::{Request, Response}; + +fn upload<'u>( + mut request: Request, + db_context: &DatabaseContext, + _: matchit::Params<'u, 'u>, +) -> CrowResult<()> { + let mut entry = some_or_response!( + single_multipart!(&mut request), or respond to request with Response::empty(400) + ); + + let mut data: Vec = Vec::with_capacity(20000); + entry.data.read_to_end(&mut data)?; + + let data_hash = Blake2s256::digest(&data); + + let txn = db_context.write_txn()?; + + let mut accessor = txn.access(); + + if accessor + .get::<[u8], [u8]>(&db_context.binary_store, data_hash.as_slice()) + .to_opt()? + .is_some() + { + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + return Ok(()); + } + + accessor.put( + &db_context.binary_store, + data_hash.as_slice(), + &data, + lmdb::put::Flags::empty(), + )?; + + let header = FileHeader { + file_name: entry.headers.filename, + content_type: entry + .headers + .content_type + .map(|v| v.essence_str().to_owned()), + }; + + accessor.put( + &db_context.metadata_store, + data_hash.as_slice(), + rkyv::to_bytes::<_, 256>(&header).unwrap().as_slice(), + lmdb::put::Flags::empty(), + )?; + + request.respond(Response::from_string(base64_url::encode(&data_hash)))?; + + drop(accessor); + + txn.commit()?; + + Ok(()) +} + +fn_to_handler!(upload: UploadHandler);