diff --git a/CHANGELOG.md b/CHANGELOG.md index df2786e9b..0b75bb490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG.md ## v0.47.0 (unreleased) +- Added S3 object uploads and temporary download links, plus an accordion component for collapsible content. - **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements. - Updated sqlx-oldapi to v0.6.57 to fix SQL Server fallback expressions such as `ISNULL($missing, 'default')` truncating defaults or failing for date values when the bound variable is `NULL`. - Fixed MSSQL `JSON_OBJECT('key': value)` expressions being rejected by SQLPage's parser, including when used in `SET` statements or nested in `sqlpage.*` function calls. diff --git a/Cargo.lock b/Cargo.lock index 22edbbf63..e4a1ad243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,9 +37,9 @@ dependencies = [ "derive_more", "encoding_rs", "flate2", - "foldhash", + "foldhash 0.2.0", "futures-core", - "h2", + "h2 0.3.27", "http 0.2.12", "httparse", "httpdate", @@ -128,7 +128,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2", + "socket2 0.6.5", "tokio", "tracing", ] @@ -159,7 +159,7 @@ dependencies = [ "pin-project-lite", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.5", "tokio-util", "tracing", "webpki-roots 0.26.11", @@ -197,7 +197,7 @@ dependencies = [ "cookie", "derive_more", "encoding_rs", - "foldhash", + "foldhash 0.2.0", "futures-core", "futures-util", "impl-more", @@ -213,7 +213,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", + "socket2 0.6.5", "time", "tracing", "url", @@ -289,6 +289,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-activity" version = "0.6.1" @@ -597,7 +603,7 @@ dependencies = [ "derive_more", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "itoa", "log", @@ -605,13 +611,55 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.9.5", - "rustls", + "rustls 0.23.45", "serde", "serde_json", "serde_urlencoded", "tokio", ] +[[package]] +name = "aws-config" +version = "1.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0149602eeaf915158e14029ba0c78dedb8c08d554b024d54c8f239aab46511d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.5.0", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01c9521fa01558f750d183c8c68c81b0155b9d193a4ba7f84c36bd1b6d04a06" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.18.1" @@ -635,6 +683,372 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "aws-runtime" +version = "1.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce527fb7e53ba9626fc47824f25e256250556c40d8f81d27dd92aa38239d632" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.115.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdaa0053cbcbc384443dd24569bd5d1664f86427b9dc04677bd0ab853954baec" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.10.9", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.90.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f18e53542c522459e757f81e274783a78f8c81acdfc8d1522ee8a18b5fb1c66" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.92.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532f4d866012ffa724a4385c82e8dd0e59f0ca0e600f3f22d4c03b6824b34e4a" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.94.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be6fbbfa1a57724788853a623378223fe828fc4c09b146c992f0c95b6256174" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35452ec3f001e1f2f6db107b6373f1f48f05ec63ba2c5c9fa91f07dad32af11" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac 0.12.1", + "http 0.2.12", + "http 1.5.0", + "p256 0.11.1", + "percent-encoding", + "ring", + "sha2 0.10.9", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95bd108f7b3563598e4dc7b62e1388c9982324a2abd622442167012690184591" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5 0.10.6", + "pin-project-lite", + "sha1 0.10.7", + "sha2 0.10.9", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e29a304f8319781a39808847efb39561351b1bb76e933da7aa90232673638658" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445d5d720c99eed0b4aa674ed00d835d9b1427dd73e04adaf2f94c6b2d6f9fca" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "623254723e8dfd535f566ee7b2381645f8981da086b5c4aa26c0c41582bb1d2c" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.19", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.45", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.5", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db31f727935fc63c6eeae8b37b438847639ec330a9161ece694efba257e0c54" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbe9d018d646b96c7be063dd07987849862b0e6d07c778aad7d93d1be6c1ef0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7204f9fd94749a7c53b26da1b961b4ac36bf070ef1e0b94bb09f79d4f6c193" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f535879a207fce0db74b679cfc3e91a3159c8144d717d55f5832aea9eef46e" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab77cdd036b11056d2a30a7af7b775789fb024bf216acc13884c6c97752ae56" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d79fb68e3d7fe5d4833ea34dc87d2e97d26d3086cb3da660bb6b1f76d98680b6" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "aws_lambda_events" version = "1.2.0" @@ -644,13 +1058,19 @@ dependencies = [ "base64 0.22.1", "bytes", "http 1.5.0", - "http-body", + "http-body 1.1.0", "http-serde", "query_map", "serde", "serde_json", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base16ct" version = "0.2.0" @@ -675,6 +1095,16 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -811,6 +1241,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bytestring" version = "1.5.1" @@ -1100,6 +1540,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "crc-fast" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +dependencies = [ + "crc", + "digest 0.10.7", + "rand 0.9.5", + "regex", + "rustversion", +] + [[package]] name = "crc32fast" version = "1.5.2" @@ -1124,6 +1577,18 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -1325,6 +1790,16 @@ dependencies = [ "thiserror 2.0.20", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid 0.9.6", + "zeroize", +] + [[package]] name = "der" version = "0.7.10" @@ -1508,18 +1983,30 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der", + "der 0.7.10", "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -1528,8 +2015,8 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", ] [[package]] @@ -1552,23 +2039,43 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct", - "crypto-bigint", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.1", "generic-array", - "group", + "group 0.13.0", "hkdf 0.12.4", "pem-rfc7468", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -1657,6 +2164,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "ff" version = "0.13.1" @@ -1707,6 +2224,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1845,7 +2368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls", + "rustls 0.23.45", "rustls-pki-types", ] @@ -1926,13 +2449,24 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff", + "ff 0.13.1", "rand_core 0.6.4", "subtle", ] @@ -1956,6 +2490,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "handlebars" version = "6.4.4" @@ -1978,13 +2531,24 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash", + "foldhash 0.2.0", ] [[package]] @@ -2077,6 +2641,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.1.0" @@ -2096,7 +2671,7 @@ dependencies = [ "bytes", "futures-core", "http 1.5.0", - "http-body", + "http-body 1.1.0", "pin-project-lite", ] @@ -2131,6 +2706,30 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.11.1" @@ -2141,8 +2740,9 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.19", "http 1.5.0", - "http-body", + "http-body 1.1.0", "httparse", "itoa", "pin-project-lite", @@ -2151,21 +2751,57 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.5.0", + "hyper 1.11.1", + "hyper-util", + "rustls 0.23.45", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.5", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ + "base64 0.22.1", "bytes", "futures-channel", "futures-util", "http 1.5.0", - "http-body", - "hyper", + "http-body 1.1.0", + "hyper 1.11.1", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.5", "tokio", "tower-service", "tracing", @@ -2353,6 +2989,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -2538,9 +3180,9 @@ dependencies = [ "encoding_rs", "futures-util", "http 1.5.0", - "http-body", + "http-body 1.1.0", "http-body-util", - "hyper", + "hyper 1.11.1", "lambda_runtime", "mime", "percent-encoding", @@ -2565,7 +3207,7 @@ dependencies = [ "http 1.5.0", "http-body-util", "http-serde", - "hyper", + "hyper 1.11.1", "lambda_runtime_api_client", "pin-project", "serde", @@ -2587,9 +3229,9 @@ dependencies = [ "futures-channel", "futures-util", "http 1.5.0", - "http-body", + "http-body 1.1.0", "http-body-util", - "hyper", + "hyper 1.11.1", "hyper-util", "tower", ] @@ -2628,11 +3270,11 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls", - "rustls-native-certs", - "socket2", + "rustls 0.23.45", + "rustls-native-certs 0.8.4", + "socket2 0.6.5", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.5", "url", "webpki-roots 1.0.9", ] @@ -2732,6 +3374,15 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "markdown" version = "1.0.0" @@ -2751,6 +3402,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "md-5" version = "0.11.0" @@ -3299,7 +3960,7 @@ dependencies = [ "itertools 0.10.5", "log", "oauth2", - "p256", + "p256 0.13.2", "p384", "rand 0.8.8", "rsa", @@ -3315,6 +3976,12 @@ dependencies = [ "url", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3421,14 +4088,31 @@ dependencies = [ "num-traits", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.9", +] + [[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2 0.10.9", ] @@ -3439,8 +4123,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" dependencies = [ - "ecdsa", - "elliptic-curve", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", "primeorder", "sha2 0.10.9", ] @@ -3594,6 +4278,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.5" @@ -3611,9 +4301,19 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", ] [[package]] @@ -3622,8 +4322,8 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -3697,7 +4397,7 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" dependencies = [ - "elliptic-curve", + "elliptic-curve 0.13.8", ] [[package]] @@ -3966,6 +4666,17 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac 0.12.1", + "zeroize", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -4002,10 +4713,10 @@ dependencies = [ "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -4054,6 +4765,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.23.45" @@ -4064,7 +4787,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.15", "subtle", "zeroize", ] @@ -4095,16 +4818,37 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -4116,6 +4860,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.15" @@ -4188,20 +4942,57 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.10", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -4440,6 +5231,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -4493,6 +5294,16 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.5" @@ -4512,6 +5323,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -4519,7 +5340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -4536,6 +5357,8 @@ dependencies = [ "async-stream", "async-trait", "awc", + "aws-config", + "aws-sdk-s3", "base64 0.23.1", "bigdecimal", "chrono", @@ -4564,9 +5387,9 @@ dependencies = [ "percent-encoding", "rand 0.10.2", "regex", - "rustls", + "rustls 0.23.45", "rustls-acme", - "rustls-native-certs", + "rustls-native-certs 0.8.4", "serde", "serde_json", "sha2 0.11.0", @@ -4640,7 +5463,7 @@ dependencies = [ "libc", "libsqlite3-sys", "log", - "md-5", + "md-5 0.11.0", "memchr", "num-bigint", "odbc-api", @@ -4649,7 +5472,7 @@ dependencies = [ "rand 0.10.2", "regex", "rsa", - "rustls", + "rustls 0.23.45", "serde", "serde_json", "sha1 0.10.7", @@ -4675,7 +5498,7 @@ checksum = "ef74464ebe407e3e2a81013ede45cb60ebb3e5fe23f771bd68e3d1941558d542" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.5", ] [[package]] @@ -4877,7 +5700,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] @@ -4893,13 +5716,23 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ - "rustls", + "rustls 0.23.45", "tokio", ] @@ -5190,6 +6023,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5231,6 +6070,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -5651,6 +6496,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust2" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 7b29ee23c..fe5070b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,6 +133,9 @@ opentelemetry-http = { version = "0.32", default-features = false } opentelemetry-semantic-conventions = { version = "0.32", features = ["semconv_experimental"] } +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1" + [features] default = [] odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"] diff --git a/configuration.md b/configuration.md index 9b4e504c8..6c4a9956b 100644 --- a/configuration.md +++ b/configuration.md @@ -41,6 +41,11 @@ Here are the available configuration options and their default values: | `environment` | development | The environment in which SQLPage is running. Can be either `development` or `production`. In `production` mode, SQLPage will hide error messages and stack traces from the user, and will cache sql files in memory to avoid reloading them from disk. | | `cache_stale_duration_ms` | 1000 (prod), 0 (dev) | The duration in milliseconds that a file can be cached before its freshness is checked against the filesystem. Defaults to 1000ms (1 second) in production and 0ms in development. | | `content_security_policy` | `script-src 'self' 'nonce-{NONCE}'` | The [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) to set in the HTTP headers. If you get CSP errors in the browser console, you can set this to the empty string to disable CSP. If you want a custom CSP that contains a nonce, include the `'nonce-{NONCE}'` directive in your configuration string and it will be populated with a random value per request. | +| `s3_bucket` | | Default bucket for `sqlpage.upload_to_s3` and `sqlpage.get_from_s3` when their bucket argument is `NULL`. | +| `s3_region` | AWS SDK default region provider | Region used by the S3 client, such as `us-east-1`. | +| `s3_endpoint` | AWS service endpoint | Optional endpoint URL for an S3-compatible object storage service. | +| `s3_access_key` | AWS SDK default credentials provider | Optional access key ID. Set together with `s3_secret_key` to override the SDK credentials provider. | +| `s3_secret_key` | AWS SDK default credentials provider | Optional secret access key paired with `s3_access_key`. Prefer setting secrets through environment variables. | | `smtp_host` | | SMTP server host used by the `sqlpage.send_mail` function. Set with `SMTP_HOST` in the environment. | | `smtp_port` | 25 (`none`), 465 (`tls`), or 587 (`starttls`) | SMTP server port. The default depends on `smtp_tls_mode`. Set this explicitly for relays using a nonstandard port. | | `smtp_username` | | Optional SMTP user name for `sqlpage.send_mail`. When set, SQLPage authenticates to `SMTP_HOST` using this user name and `smtp_password`. Credentials require `smtp_tls_mode` to be `starttls` or `tls`. | diff --git a/docs/s3.md b/docs/s3.md new file mode 100644 index 000000000..2b3ae2481 --- /dev/null +++ b/docs/s3.md @@ -0,0 +1,93 @@ +# S3 Object Storage Integration + +SQLPage allows you to upload and download files from S3-compatible object storage services, such as AWS S3, MinIO, Cloudflare R2, Google Cloud Storage, and others. + +## Configuration + +To enable S3 integration, you need to add the following configuration to your `sqlpage/sqlpage.json` file. + +### AWS S3 Example + +```json +{ + "s3_bucket": "my-app-uploads", + "s3_region": "us-east-1", + "s3_access_key": "AKIAIOSFODNN7EXAMPLE", + "s3_secret_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +} +``` + +### MinIO Example + +If you are using MinIO, you need to specify the `s3_endpoint` URL. + +```json +{ + "s3_bucket": "my-local-bucket", + "s3_region": "us-east-1", + "s3_endpoint": "http://localhost:9000", + "s3_access_key": "minioadmin", + "s3_secret_key": "minioadmin" +} +``` + +> **Note**: For MinIO, `s3_region` is often required by the client SDK but ignored by the server. You can usually set it to `us-east-1`. + +### Configuration Parameters + +| Parameter | Description | Required | +| :--- | :--- | :--- | +| `s3_bucket` | The default bucket name to use for operations. | Yes (unless specified in function calls) | +| `s3_region` | The AWS region (e.g., `us-east-1`, `eu-west-1`). | Yes | +| `s3_endpoint` | The endpoint URL. Required for non-AWS providers (MinIO, R2, etc.). | No | +| `s3_access_key` | Your access key ID. | Yes | +| `s3_secret_key` | Your secret access key. | Yes | + +## Functions + +### `sqlpage.upload_to_s3` + +Uploads data to an S3 bucket. + +**Signature:** `sqlpage.upload_to_s3(bucket, data, key)` + +- **bucket** (string, optional): The name of the bucket. If `NULL`, the `s3_bucket` from configuration is used. +- **data** (string): The data to upload. This can be: + - A file path (e.g., from `sqlpage.uploaded_file_path`). + - A base64-encoded string. +- **key** (string): The path/name of the object in the bucket (e.g., `uploads/image.png`). + +**Returns:** The S3 URI of the uploaded object (e.g., `s3://bucket/key`). + +**Example:** + +```sql +-- Upload a file submitted via a form +SELECT sqlpage.upload_to_s3( + NULL, -- Use default bucket from config + sqlpage.uploaded_file_path('profile_picture'), + 'users/' || $user_id || '/profile.png' +); +``` + +### `sqlpage.get_from_s3` + +Generates a presigned URL to download a file from an S3 bucket. The URL is valid for 1 hour. + +**Signature:** `sqlpage.get_from_s3(bucket, key)` + +- **bucket** (string, optional): The name of the bucket. If `NULL`, the `s3_bucket` from configuration is used. +- **key** (string): The path/name of the object in the bucket. + +**Returns:** A temporary HTTP URL that allows downloading the file. + +**Example:** + +```sql +SELECT 'text' as component, + 'Download Profile Picture' as contents; + +SELECT 'button' as component; +SELECT 'Download' as title, + sqlpage.get_from_s3(NULL, 'users/' || $user_id || '/profile.png') as link; +``` diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ce61b247a..1f203156e 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -1745,3 +1745,15 @@ This property improves code readability by clearly indicating that you are gener Since SQLPage returns HTML by default, there is no need to specify the content type in the HTTP header. ', json('[{"component":"shell-empty", "html": "\n\n\n My page\n\n\n

My page

\n\n"}]')); + +INSERT INTO component(name, icon, description) VALUES + ('accordion', 'sort-ascending', 'An accordion based list of elements which can be shown individually.'); +INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'accordion', * FROM (VALUES + -- top level + ('id', 'id attribute added to the container in HTML. It can be used to target this item through css or for scrolling to this item through links (use "#id" in link url).', 'TEXT', TRUE, TRUE), + -- item level + ('title', 'Name of the list item, displayed prominently.', 'TEXT', FALSE, FALSE), + ('contents_md', 'A description of the list item, displayed as greyed-out text.', 'TEXT', FALSE, FALSE) +) x; +INSERT INTO example(component, description, properties) VALUES + ('accordion', 'A basic accordion with Markdown', json('[{"component":"accordion"},{"title":"Beautiful","contents_md": "Lorem *ipsum* dolor sit amet, **consectetur** adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam."},{"title":"Useful","contents_md": "Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis *ipsum*. Praesent **mauris**. Fusce nec tellus sed augue semper porta."},{"title":"Compact","contents_md": "Mauris **ipsum**. Nulla metus metus, ullamcorper vel, tincidunt sed, euismod in, *nibh*. Quisque volutpat condimentum velit."}]')); \ No newline at end of file diff --git a/sqlpage/templates/accordion.handlebars b/sqlpage/templates/accordion.handlebars new file mode 100644 index 000000000..9a88f80a5 --- /dev/null +++ b/sqlpage/templates/accordion.handlebars @@ -0,0 +1,18 @@ +
+ {{#each_row}} +
+

+ +

+
+
+ {{{markdown contents_md}}} +
+
+
+ {{/each_row}} +
diff --git a/src/app_config.rs b/src/app_config.rs index 6e9166457..6cfb148ca 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -412,6 +412,12 @@ pub struct AppConfig { pub markdown_allow_dangerous_protocol: bool, pub cache_stale_duration_ms: Option, + + pub s3_bucket: Option, + pub s3_region: Option, + pub s3_endpoint: Option, + pub s3_access_key: Option, + pub s3_secret_key: Option, } impl AppConfig { diff --git a/src/webserver/database/sqlpage_functions/functions.rs b/src/webserver/database/sqlpage_functions/functions.rs index 5139fa314..17908a63f 100644 --- a/src/webserver/database/sqlpage_functions/functions.rs +++ b/src/webserver/database/sqlpage_functions/functions.rs @@ -19,6 +19,7 @@ mod environment_variable; mod exec; mod fetch; mod fetch_with_meta; +mod get_from_s3; mod hash_password; mod header; mod headers; @@ -38,6 +39,7 @@ mod request_method; mod run_sql; mod send_mail; mod set_variable; +mod upload_to_s3; mod uploaded_file_mime_type; mod uploaded_file_name; mod uploaded_file_path; @@ -59,6 +61,7 @@ sqlpage_functions! { exec, fetch, fetch_with_meta, + get_from_s3, hash_password, header, headers, @@ -81,6 +84,7 @@ sqlpage_functions! { uploaded_file_mime_type, uploaded_file_name, uploaded_file_path, + upload_to_s3, url_encode, user_info, user_info_token, diff --git a/src/webserver/database/sqlpage_functions/functions/get_from_s3.rs b/src/webserver/database/sqlpage_functions/functions/get_from_s3.rs new file mode 100644 index 000000000..a92019974 --- /dev/null +++ b/src/webserver/database/sqlpage_functions/functions/get_from_s3.rs @@ -0,0 +1,13 @@ +use std::borrow::Cow; + +use super::super::s3; +use crate::webserver::http_request_info::RequestInfo; + +pub(super) async fn get_from_s3<'a>( + request: &'a RequestInfo, + bucket: Option>, + key: Cow<'a, str>, +) -> anyhow::Result { + // Keep the AWS SDK future out of the shared SQL function dispatch future. + Box::pin(s3::get_from_s3(request, bucket, key)).await +} diff --git a/src/webserver/database/sqlpage_functions/functions/upload_to_s3.rs b/src/webserver/database/sqlpage_functions/functions/upload_to_s3.rs new file mode 100644 index 000000000..651e9276a --- /dev/null +++ b/src/webserver/database/sqlpage_functions/functions/upload_to_s3.rs @@ -0,0 +1,14 @@ +use std::borrow::Cow; + +use super::super::s3; +use crate::webserver::http_request_info::RequestInfo; + +pub(super) async fn upload_to_s3<'a>( + request: &'a RequestInfo, + bucket: Option>, + data: Cow<'a, str>, + key: Cow<'a, str>, +) -> anyhow::Result { + // Keep the AWS SDK future out of the shared SQL function dispatch future. + Box::pin(s3::upload_to_s3(request, bucket, data, key)).await +} diff --git a/src/webserver/database/sqlpage_functions/mod.rs b/src/webserver/database/sqlpage_functions/mod.rs index d4b70ec2f..b9305c158 100644 --- a/src/webserver/database/sqlpage_functions/mod.rs +++ b/src/webserver/database/sqlpage_functions/mod.rs @@ -1,4 +1,5 @@ mod function_traits; pub(super) mod functions; mod http_fetch_request; +mod s3; mod url_parameters; diff --git a/src/webserver/database/sqlpage_functions/s3.rs b/src/webserver/database/sqlpage_functions/s3.rs new file mode 100644 index 000000000..ba653e375 --- /dev/null +++ b/src/webserver/database/sqlpage_functions/s3.rs @@ -0,0 +1,191 @@ +use crate::app_config::AppConfig; +use crate::webserver::http_request_info::RequestInfo; +use anyhow::Context; +use aws_config::BehaviorVersion; +use aws_sdk_s3::presigning::PresigningConfig; +use std::borrow::Cow; +use std::time::Duration; + +pub(super) async fn upload_to_s3<'a>( + request: &'a RequestInfo, + bucket: Option>, + data: Cow<'a, str>, + key: Cow<'a, str>, +) -> anyhow::Result { + let config = &request.app_state.config; + let client = get_s3_client(config).await; + upload_to_s3_with_client(config, &client, bucket, data, key).await +} + +async fn upload_to_s3_with_client<'a>( + config: &AppConfig, + client: &aws_sdk_s3::Client, + bucket: Option>, + data: Cow<'a, str>, + key: Cow<'a, str>, +) -> anyhow::Result { + let bucket = bucket + .as_deref() + .or(config.s3_bucket.as_deref()) + .ok_or_else(|| anyhow::anyhow!("S3 bucket not configured"))?; + + let body_bytes = prepare_upload_body(data.as_ref(), config).await?; + + client + .put_object() + .bucket(bucket) + .key(key.as_ref()) + .body(body_bytes.into()) + .send() + .await + .map_err(|e| anyhow::anyhow!("Failed to upload to S3: {e}"))?; + + Ok(format!("s3://{bucket}/{key}")) +} + +async fn prepare_upload_body(data: &str, config: &AppConfig) -> anyhow::Result> { + if let Some(stripped) = data.strip_prefix("file://") { + let file_path = std::path::Path::new(stripped); + // Security check: ensure the file is within the web root or allowed paths + let web_root = &config.web_root; + let full_path = web_root.join(file_path); + if !full_path.starts_with(web_root) { + anyhow::bail!("Security violation: Access denied to file outside web root"); + } + tokio::fs::read(&full_path) + .await + .map_err(|e| { + log::error!("Failed to read file {}: {}", full_path.display(), e); + e + }) + .with_context(|| format!("Unable to read file {}", full_path.display())) + } else { + // Assume base64 + use base64::Engine; + base64::engine::general_purpose::STANDARD + .decode(data.as_bytes()) + .map_err(|e| { + log::error!("Base64 decode failed: {e}"); + e + }) + .context("Invalid base64 data") + } +} + +pub(super) async fn get_from_s3<'a>( + request: &'a RequestInfo, + bucket: Option>, + key: Cow<'a, str>, +) -> anyhow::Result { + let config = &request.app_state.config; + let client = get_s3_client(config).await; + get_from_s3_with_client(config, &client, bucket, key).await +} + +async fn get_from_s3_with_client<'a>( + config: &AppConfig, + client: &aws_sdk_s3::Client, + bucket: Option>, + key: Cow<'a, str>, +) -> anyhow::Result { + let bucket = bucket + .as_deref() + .or(config.s3_bucket.as_deref()) + .ok_or_else(|| anyhow::anyhow!("S3 bucket not configured"))?; + + let presigning_config = PresigningConfig::expires_in(Duration::from_secs(3600))?; + + let presigned_request = client + .get_object() + .bucket(bucket) + .key(key.as_ref()) + .presigned(presigning_config) + .await + .map_err(|e| anyhow::anyhow!("Failed to generate presigned URL: {e}"))?; + + Ok(presigned_request.uri().to_string()) +} + +async fn get_s3_client(config: &AppConfig) -> aws_sdk_s3::Client { + let mut loader = aws_config::defaults(BehaviorVersion::latest()); + + if let Some(endpoint) = &config.s3_endpoint { + loader = loader.endpoint_url(endpoint); + } + if let Some(region) = &config.s3_region { + loader = loader.region(aws_config::Region::new(region.clone())); + } + if let (Some(access_key), Some(secret_key)) = (&config.s3_access_key, &config.s3_secret_key) { + let creds = aws_sdk_s3::config::Credentials::new( + access_key.clone(), + secret_key.clone(), + None, + None, + "sqlpage-config", + ); + loader = loader.credentials_provider(creds); + } + + let sdk_config = loader.load().await; + aws_sdk_s3::Client::new(&sdk_config) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_config::tests::test_config; + + #[tokio::test] + async fn test_prepare_upload_body_base64() { + let config = test_config(); + let data = "SGVsbG8gV29ybGQ="; // "Hello World" + let result = prepare_upload_body(data, &config).await.unwrap(); + assert_eq!(result, b"Hello World"); + } + + #[tokio::test] + async fn test_prepare_upload_body_invalid_base64() { + let config = test_config(); + let data = "InvalidBase64!"; + let result = prepare_upload_body(data, &config).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_prepare_upload_body_file_security() { + let config = test_config(); + // Try to access a file outside the web root (assuming /tmp is outside) + // Note: test_config uses current dir as web_root usually, or a temp dir. + // We need to construct a path that attempts directory traversal or absolute path outside. + let data = "file:///etc/passwd"; + let result = prepare_upload_body(data, &config).await; + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("Security violation") + ); + } + + #[tokio::test] + async fn test_get_from_s3_presigned() { + let mut config = test_config(); + config.s3_bucket = Some("my-bucket".to_string()); + config.s3_region = Some("us-east-1".to_string()); + config.s3_access_key = Some("test".to_string()); + config.s3_secret_key = Some("test".to_string()); + + // Create a client that doesn't actually connect but is valid enough for presigning + // Presigning is a local operation for the most part, but it needs credentials. + let client = get_s3_client(&config).await; + + let url = get_from_s3_with_client(&config, &client, None, Cow::Borrowed("my-file.txt")) + .await + .unwrap(); + + assert!(url.contains("my-bucket")); + assert!(url.contains("my-file.txt")); + assert!(url.contains("X-Amz-Signature")); + } +} diff --git a/tests/end-to-end/fixtures/accordion/index.sql b/tests/end-to-end/fixtures/accordion/index.sql new file mode 100644 index 000000000..7aa7d3bf4 --- /dev/null +++ b/tests/end-to-end/fixtures/accordion/index.sql @@ -0,0 +1,4 @@ +SELECT 'shell' AS component, 'Accordion test' AS title; +SELECT 'accordion' AS component, 'example-accordion' AS id; +SELECT 'First section' AS title, 'First **content**' AS contents_md; +SELECT 'Second section' AS title, 'Second **content**' AS contents_md; diff --git a/tests/end-to-end/fixtures/accordion/test.ts b/tests/end-to-end/fixtures/accordion/test.ts new file mode 100644 index 000000000..ec5a196db --- /dev/null +++ b/tests/end-to-end/fixtures/accordion/test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "../../fixture"; + +test("uses the documented id and switches expanded sections", async ({ + page, +}) => { + const accordion = page.locator("#example-accordion"); + await expect(accordion).toBeVisible(); + await expect(accordion.getByText("First content")).toBeVisible(); + await expect(accordion.getByText("Second content")).toBeHidden(); + await accordion.getByRole("button", { name: "Second section" }).click(); + await expect(accordion.getByText("Second content")).toBeVisible(); + await expect(accordion.getByText("First content")).toBeHidden(); +});