From ceaa43f87dd1511d3bfda2a75c8e0608d24138a9 Mon Sep 17 00:00:00 2001 From: tsuki Date: Fri, 1 Aug 2025 15:25:11 +0800 Subject: [PATCH] update remap --- .dockerignore | 36 ++++ .github/workflows/docker.yml | 61 +++++++ .gitignore | 2 + Cargo.lock | 310 +++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +- Dockerfile | 60 +++++++ README.md | 100 +++++++++++ src/app.rs | 143 ++++++++++++++-- src/config.rs | 2 + src/main.rs | 1 + src/model/mod.rs | 1 + src/model/responses.rs | 19 +++ src/service/mod.rs | 17 +- src/utils.rs | 11 ++ 14 files changed, 748 insertions(+), 21 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 src/model/responses.rs create mode 100644 src/utils.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9c9cc1f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Git相关 +.git +.gitignore + +# Rust构建产物 +target/ +Cargo.lock + +# IDE相关 +.vscode/ +.idea/ +*.swp +*.swo + +# 系统文件 +.DS_Store +Thumbs.db + +# 日志文件 +*.log + +# 临时文件 +*.tmp +*.temp + +# 文档 +README.md +docs/ + +# 测试文件 +tests/ +**/*_test.rs + +# 其他 +.env.local +.env.production \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..c7993d4 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,61 @@ +name: Docker Build and Push + +on: + push: + branches: [ main, master ] + tags: [ 'v*' ] + pull_request: + branches: [ main, master ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Output image digest + if: github.event_name != 'pull_request' + run: echo ${{ steps.build.outputs.digest }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..ec47942 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target + +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fb327b4..49df2c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -204,6 +210,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -337,6 +344,15 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -539,6 +555,25 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "h2" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.4" @@ -653,6 +688,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -661,6 +697,39 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -669,14 +738,24 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", + "tracing", + "windows-registry", ] [[package]] @@ -831,6 +910,22 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.15" @@ -925,11 +1020,13 @@ dependencies = [ "log", "lru", "regex", + "reqwest", "serde", "serde_json", "sqlx", "tokio", "tower-http", + "url", "uuid", ] @@ -1297,6 +1394,60 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.8" @@ -1336,6 +1487,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -1767,6 +1951,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1779,6 +1966,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.20.0" @@ -1868,6 +2076,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -1879,6 +2107,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" @@ -1903,8 +2144,12 @@ checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags", "bytes", + "futures-util", "http", + "http-body", + "iri-string", "pin-project-lite", + "tower", "tower-layer", "tower-service", ] @@ -1953,6 +2198,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.18.0" @@ -1986,6 +2237,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -2011,6 +2268,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.3", "js-sys", + "serde", "wasm-bindgen", ] @@ -2026,6 +2284,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2073,6 +2340,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -2105,6 +2385,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.6.0" @@ -2156,6 +2446,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -2183,6 +2484,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index c2c977f..ef9800c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,16 @@ edition = "2024" [dependencies] axum = "0.8.4" -chrono = "0.4.41" +chrono = { version = "0.4.41", features = ["serde"] } dotenvy = "0.15.7" log = "0.4.27" lru = "0.16.0" regex = "1.11.1" +reqwest = { version = "0.12", features = ["json"] } serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0" sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono"] } tokio = { version = "1.47.0", features = ["full"] } tower-http = {version = "0.6.6" , features = ["cors"]} -uuid = { version = "1.17.0", features = ["v4"] } +url = "2.5.4" +uuid = { version = "1.17.0", features = ["v4", "serde"] } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..97b2e3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,60 @@ +# 使用官方Rust镜像作为构建阶段 +FROM rust:1.75-slim as builder + +# 设置工作目录 +WORKDIR /usr/src/app + +# 复制Cargo.toml和Cargo.lock文件 +COPY Cargo.toml Cargo.lock ./ + +# 创建一个虚拟的main.rs来缓存依赖 +RUN mkdir src && echo "fn main() {}" > src/main.rs + +# 构建依赖(这会缓存依赖层) +RUN cargo build --release + +# 删除虚拟的main.rs +RUN rm src/main.rs + +# 复制源代码 +COPY src ./src + +# 重新构建应用程序(现在会使用缓存的依赖) +RUN cargo build --release + +# 运行时阶段 +FROM debian:bookworm-slim + +# 安装运行时依赖 +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libpq-dev \ + && rm -rf /var/lib/apt/lists/* + +# 创建非root用户 +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /usr/src/app/target/release/mapp-tile /app/mapp-tile + +# 复制配置文件(如果有的话) +COPY .env* ./ + +# 更改文件所有权 +RUN chown -R appuser:appuser /app + +# 切换到非root用户 +USER appuser + +# 暴露端口(根据您的应用程序需要调整) +EXPOSE 3000 + +# 设置健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3000/api/v1/health || exit 1 + +# 运行应用程序 +CMD ["./mapp-tile"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..567a205 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Mapp Tile Service + +一个基于Rust的瓦片地图服务,提供图像缓存和数据库查询功能。 + +## 🐳 Docker 部署 + +### 本地构建 + +```bash +# 构建镜像 +docker build -t mapp-tile:latest . + +# 运行容器 +docker run -p 3000:3000 --env-file .env mapp-tile:latest +``` + +### 使用构建脚本 + +```bash +# 给脚本执行权限 +chmod +x docker-build.sh + +# 运行构建脚本 +./docker-build.sh +``` + +## 🚀 GitHub Actions + +本项目配置了以下GitHub Actions工作流: + +### 1. Docker构建和推送 (`docker.yml`) +- 在推送到main/master分支时自动构建Docker镜像 +- 支持多平台构建(amd64, arm64) +- 自动推送到GitHub Container Registry +- 支持语义化版本标签 + +### 2. 测试 (`test.yml`) +- 运行Rust项目的单元测试 +- 代码格式检查 +- Clippy静态分析 +- 包含PostgreSQL服务用于集成测试 + +### 3. 安全扫描 (`security.yml`) +- 依赖漏洞扫描(cargo audit) +- 许可证检查(cargo-deny) +- 容器镜像安全扫描(Trivy) +- 每周自动运行 + +## 📋 环境变量 + +创建 `.env` 文件并配置以下环境变量: + +```env +DATABASE_URL=postgresql://username:password@localhost:5432/database +LOCAL_OSS=https://your-oss-endpoint.com +``` + +## 🔧 开发 + +### 本地运行 + +```bash +# 安装依赖 +cargo build + +# 运行开发服务器 +cargo run +``` + +### 测试 + +```bash +# 运行测试 +cargo test + +# 运行Clippy检查 +cargo clippy + +# 检查代码格式 +cargo fmt --check +``` + +## 📦 镜像标签说明 + +- `latest`: 最新版本 +- `main`: main分支的最新构建 +- `v1.0.0`: 语义化版本标签 +- `sha-abc123`: 基于提交哈希的标签 + +## 🔍 健康检查 + +服务提供健康检查端点: + +```bash +curl http://localhost:3000/api/v1/health +``` + +## �� 许可证 + +MIT License \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 279fa6b..35f4b1b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,35 +1,48 @@ use axum::{Error, response::Response}; use std::num::NonZero; -use crate::{config::Config, model::inputs::TileInfo, service::Service}; +use crate::{ + config::Config, + model::{inputs::TileInfo, responses::TileResponse}, + service::Service, + utils::build_url, +}; use axum::{ Router, ServiceExt, extract::{FromRef, Query, State}, - http::StatusCode, - response::{Html, IntoResponse}, + http::{HeaderMap, StatusCode, header}, + response::{Html, IntoResponse, Redirect}, routing::{get, post}, }; use chrono::{DateTime, Utc}; use lru::LruCache; +use reqwest; +use serde_json; use sqlx::postgres::PgPool; +use std::collections::HashMap; use tower_http::cors::CorsLayer; #[derive(Clone)] pub struct AppState { pub service: Service, pub pool: PgPool, - pub cache: LruCache, Vec>, + pub cache: LruCache, TileResponse>, + pub image_cache: LruCache, String)>, // URL -> (image_data, content_type) + pub config: Config, } pub async fn create_router(config: &Config) -> Router { let pool = PgPool::connect(&config.database_url).await.unwrap(); let cache = LruCache::new(NonZero::new(10).unwrap()); + let image_cache = LruCache::new(NonZero::new(20).unwrap()); // 缓存更多图像 let service = Service::new(pool.clone()); let app_state = AppState { service, pool, cache, + image_cache, + config: config.clone(), }; Router::new() @@ -50,33 +63,129 @@ async fn data_handler( match chrono::NaiveDateTime::parse_from_str(¶ms.datetime, "%Y%m%d%H%M%S") { Ok(naive_datetime) => { let datetime = naive_datetime.and_utc(); - if let Some(tile_info) = state.cache.get(&datetime) { - return Html(format!("tile_info: {:?}", tile_info)).into_response(); + let area = params.area.unwrap_or_default(); + + // 先检查缓存 + if let Some(tile_response) = state.cache.get(&datetime) { + // 检查图像缓存 + if let Some((image_data, content_type)) = + state.image_cache.get(&tile_response.storage_url) + { + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert(header::CONTENT_DISPOSITION, "inline".parse().unwrap()); + return (StatusCode::OK, headers, image_data.clone()).into_response(); + } + + // 下载图像数据并返回 + match download_image(&tile_response.storage_url).await { + Ok((image_data, content_type)) => { + // 缓存图像数据 + state.image_cache.put( + tile_response.storage_url.clone(), + (image_data.clone(), content_type.clone()), + ); + + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert(header::CONTENT_DISPOSITION, "inline".parse().unwrap()); + (StatusCode::OK, headers, image_data).into_response() + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("下载图像失败: {:?}", e), + ) + .into_response(), + } } else { - match state.service.get_tile_data(datetime, "").await { - Ok(tile_info) => { - // state.cache.put(datetime, tile_info.clone()); - (StatusCode::OK, Html(format!("tile_info: {:?}", tile_info))) - .into_response() + // 从数据库获取 + match state.service.get_tile_data(datetime, &area).await { + Ok(tile_response) => { + // 缓存结果 + state.cache.put(datetime, tile_response.clone()); + + // 检查图像缓存 + if let Some((image_data, content_type)) = + state.image_cache.get(&tile_response.storage_url) + { + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers.insert(header::CONTENT_DISPOSITION, "inline".parse().unwrap()); + return (StatusCode::OK, headers, image_data.clone()).into_response(); + } + + // 下载图像数据并返回 + let url = build_url(&state.config.local_oss, &[&tile_response.storage_url]) + .map_err(|e| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("构建URL失败: {:?}", e), + ) + .into_response() + }) + .unwrap() + .replace(".png", "_processed.png"); + + match download_image(&url).await { + Ok((image_data, content_type)) => { + // 缓存图像数据 + state.image_cache.put( + tile_response.storage_url.clone(), + (image_data.clone(), content_type.clone()), + ); + + let mut headers = HeaderMap::new(); + headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); + headers + .insert(header::CONTENT_DISPOSITION, "inline".parse().unwrap()); + (StatusCode::OK, headers, image_data).into_response() + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("下载图像失败: {:?}", e), + ) + .into_response(), + } } Err(e) => { - // 方式1: 返回错误状态码和错误信息 - return ( + // 返回错误状态码和错误信息 + ( StatusCode::INTERNAL_SERVER_ERROR, format!("数据库错误: {:?}", e), ) - .into_response(); + .into_response() } } } } Err(e) => { - // 方式2: 返回400错误状态码 - return ( + // 返回400错误状态码 + ( StatusCode::BAD_REQUEST, format!("日期格式错误: {}. 期望格式: YYYYMMDDHHMMSS", e), ) - .into_response(); + .into_response() } } } + +async fn download_image(url: &str) -> Result<(Vec, String), Box> { + let client = reqwest::Client::new(); + let response = client.get(url).send().await?; + + if response.status().is_success() { + // 检测图像类型 + let content_type = response + .headers() + .get("content-type") + .and_then(|ct| ct.to_str().ok()) + .unwrap_or("image/png") + .to_string(); + + let bytes = response.bytes().await?; + + Ok((bytes.to_vec(), content_type.to_string())) + } else { + Err(format!("HTTP错误: {}", response.status()).into()) + } +} diff --git a/src/config.rs b/src/config.rs index c615d07..e1c5e91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ pub struct Config { pub database_url: String, pub jwt_secret: String, pub port: u16, + pub local_oss: String, } impl Config { @@ -18,6 +19,7 @@ impl Config { .unwrap_or_else(|_| "3000".to_string()) .parse() .unwrap_or(3000), + local_oss: env::var("LOCAL_OSS")?, }) } } diff --git a/src/main.rs b/src/main.rs index 4c0df24..d862ab3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod config; mod db; mod model; mod service; +mod utils; #[tokio::main] async fn main() -> Result<(), Box> { diff --git a/src/model/mod.rs b/src/model/mod.rs index cefb36d..6a3527a 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -1 +1,2 @@ pub mod inputs; +pub mod responses; diff --git a/src/model/responses.rs b/src/model/responses.rs new file mode 100644 index 0000000..c169e49 --- /dev/null +++ b/src/model/responses.rs @@ -0,0 +1,19 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TileResponse { + pub id: Uuid, + pub ingestion_time: DateTime, + pub data_time: DateTime, + pub source: String, + pub storage_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImageInfo { + pub url: String, + pub timestamp: DateTime, + pub source: String, +} diff --git a/src/service/mod.rs b/src/service/mod.rs index c0b61df..424be83 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,3 +1,4 @@ +use crate::model::responses::TileResponse; use axum::Error; use chrono::{DateTime, Utc}; use sqlx::PgPool; @@ -16,7 +17,7 @@ impl Service { &self, datetime: DateTime, area: &str, - ) -> Result, Error> { + ) -> Result { let primitive_datetime = datetime.format("%Y%m%d%H%M%S").to_string(); let record = sqlx::query!( @@ -37,6 +38,18 @@ impl Service { .await .map_err(|e| Error::new(format!("Database error: {}", e)))?; - Ok(Vec::new()) + match record { + Some(row) => Ok(TileResponse { + id: row.id, + ingestion_time: row.ingestion_time.and_utc(), + data_time: row.data_time.and_utc(), + source: row.source, + storage_url: row.storage_url, + }), + None => Err(Error::new(format!( + "No tile data found for datetime: {} and area: {}", + primitive_datetime, area + ))), + } } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b345455 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,11 @@ +use url::Url; + +pub fn build_url(base: &str, path_segments: &[&str]) -> Result { + let mut url = Url::parse(base)?; + { + let mut segs = url.path_segments_mut().expect("base URL must be a base"); + segs.extend(path_segments); + } + + Ok(url.into()) +}