├── .prettierignore ├── .gitignore ├── .dockerignore ├── .rustfmt.toml ├── .prettierrc ├── .vscode └── settings.json ├── Makefile ├── Dockerfile ├── src ├── types.rs ├── routes │ ├── api.rs │ ├── mod.rs │ └── html.rs ├── utils.rs ├── state.rs ├── main.rs ├── gh_client.rs ├── helpers.rs └── db_client.rs ├── Cargo.toml ├── assets ├── favicon.svg ├── app.css └── app.js ├── LICENSE ├── .github └── workflows │ └── build.yml ├── cliff.toml ├── readme.md └── Cargo.lock /.prettierignore: -------------------------------------------------------------------------------- 1 | app.css -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /target 3 | /data 4 | .env 5 | *.db -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Cargo.toml 3 | !Cargo.lock 4 | !/assets/ 5 | !/src/ 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://rust-lang.github.io/rustfmt/?version=v1.6.0&search= 2 | edition = "2021" 3 | max_width = 100 4 | use_small_heuristics = "Max" 5 | tab_spaces = 2 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[rust]": { 3 | "editor.defaultFormatter": "rust-lang.rust-analyzer" 4 | }, 5 | "[toml]": { 6 | "editor.defaultFormatter": "tamasfe.even-better-toml" 7 | }, 8 | "[markdown]": { 9 | "editor.wordWrap": "wordWrapColumn", 10 | "editor.wordWrapColumn": 100 11 | }, 12 | "editor.formatOnSave": true, 13 | "files.exclude": { "Cargo.lock": true }, 14 | "terminal.integrated.tabStopWidth": 2, 15 | "evenBetterToml.formatter.columnWidth": 100, 16 | "code-runner.executorMap": { 17 | "rust": "cargo run -r #$fileName" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dev: 2 | cargo watch -q -x 'run' 3 | 4 | lint: 5 | cargo fmt --check 6 | cargo check --release --locked 7 | 8 | update: 9 | @# cargo install cargo-edit 10 | cargo upgrade -i 11 | 12 | docker-build: 13 | docker build -t ghstats . 14 | docker images -q ghstats | xargs docker inspect -f '{{.Size}}' | xargs numfmt --to=iec 15 | 16 | docker-run: docker-build 17 | docker rm --force ghstats || true 18 | docker run -p 8080:8080 -v ./data:/app/data --env-file .env --name ghstats ghstats 19 | 20 | docker-log: 21 | docker logs ghstats --follow 22 | 23 | gh-cache-clear: 24 | gh cache delete --all 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM ghcr.io/vladkens/baseimage/rust:latest AS chef 2 | 3 | FROM chef AS planner 4 | COPY Cargo.toml Cargo.lock . 5 | RUN /scripts/build prepare 6 | 7 | FROM chef AS builder 8 | COPY --from=planner /app/recipe.json recipe.json 9 | RUN /scripts/build cook 10 | COPY . . 11 | RUN /scripts/build final ghstats 12 | 13 | FROM alpine:latest 14 | LABEL org.opencontainers.image.source="https://github.com/vladkens/ghstats" 15 | 16 | ARG TARGETPLATFORM 17 | WORKDIR /app 18 | COPY --from=builder /out/ghstats/${TARGETPLATFORM} /app/ghstats 19 | 20 | ENV HOST=0.0.0.0 PORT=8080 21 | HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:${PORT}/health || exit 1 22 | EXPOSE ${PORT} 23 | CMD ["/app/ghstats"] 24 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | // https://github.com/tokio-rs/axum/blob/main/examples/anyhow-error-response/src/main.rs 2 | 3 | pub type Res = anyhow::Result; 4 | pub type JsonRes = Result, AppError>; 5 | pub type HtmlRes = Result; 6 | 7 | pub struct AppError(anyhow::Error); 8 | 9 | impl AppError { 10 | pub fn not_found() -> HtmlRes { 11 | Err(Self(anyhow::anyhow!(axum::http::StatusCode::NOT_FOUND))) 12 | } 13 | } 14 | 15 | impl axum::response::IntoResponse for AppError { 16 | fn into_response(self) -> axum::response::Response { 17 | match self.0.downcast_ref::() { 18 | Some(code) => (*code, self.0.to_string()).into_response(), 19 | None => { 20 | let code = axum::http::StatusCode::INTERNAL_SERVER_ERROR; 21 | (code, format!("Something went wrong: {}", self.0)).into_response() 22 | } 23 | } 24 | } 25 | } 26 | 27 | impl> From for AppError { 28 | fn from(err: E) -> Self { 29 | Self(err.into()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ghstats" 3 | version = "0.7.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.94" 8 | axum = "0.7.9" 9 | chrono = { version = "0.4.39", features = ["serde"] } 10 | dotenvy = "0.15.7" 11 | maud = { version = "0.26.0", features = ["axum"] } 12 | reqwest = { version = "0.12.9", features = ["json", "rustls-tls"], default-features = false } 13 | serde = { version = "1.0.216", features = ["serde_derive"] } 14 | serde_json = "1.0.133" 15 | serde_variant = "0.1.3" 16 | sqlx = { version = "0.8.2", features = ["runtime-tokio", "sqlite"] } 17 | thousands = "0.2.0" 18 | tokio = { version = "1.42.0", features = ["full"] } 19 | tokio-cron-scheduler = "0.13.0" 20 | tower-http = { version = "0.6.2", features = ["trace", "cors"] } 21 | tracing = "0.1.41" 22 | tracing-logfmt = { version = "0.3.5", features = ["ansi_logs"] } 23 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } 24 | 25 | [lints.rust] 26 | dead_code = "allow" 27 | 28 | [profile.dev] 29 | debug = 0 30 | 31 | [profile.release] 32 | strip = true 33 | -------------------------------------------------------------------------------- /src/routes/api.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::extract::{Query, Request, State}; 4 | use axum::Json; 5 | 6 | use crate::db_client::{RepoFilter, RepoTotals}; 7 | use crate::types::JsonRes; 8 | use crate::AppState; 9 | 10 | #[derive(Debug, serde::Serialize)] 11 | pub struct ReposList { 12 | total_count: i32, 13 | total_stars: i32, 14 | total_forks: i32, 15 | total_views: i32, 16 | total_clones: i32, 17 | items: Vec, 18 | } 19 | 20 | pub async fn api_get_repos(State(state): State>, req: Request) -> JsonRes { 21 | let qs: Query = Query::try_from_uri(req.uri())?; 22 | let repos = state.get_repos_filtered(&qs).await?; 23 | 24 | let repos_list = ReposList { 25 | total_count: repos.len() as i32, 26 | total_stars: repos.iter().map(|r| r.stars).sum(), 27 | total_forks: repos.iter().map(|r| r.forks).sum(), 28 | total_views: repos.iter().map(|r| r.views_count).sum(), 29 | total_clones: repos.iter().map(|r| r.clones_count).sum(), 30 | items: repos, 31 | }; 32 | 33 | Ok(Json(repos_list)) 34 | } 35 | -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 vladkens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use tokio::signal; 2 | use tracing::{dispatcher, Dispatch, Level}; 3 | use tracing_subscriber::layer::SubscriberExt; 4 | use tracing_subscriber::{EnvFilter, Registry}; 5 | 6 | pub fn init_logger() { 7 | let logfmt = tracing_logfmt::builder() 8 | .with_target(false) 9 | .with_span_name(false) 10 | .with_span_path(false) 11 | .with_ansi_color(false); 12 | 13 | let subscriber = Registry::default() 14 | .with(EnvFilter::builder().with_default_directive(Level::INFO.into()).from_env_lossy()) 15 | .with(logfmt.layer()); 16 | 17 | dispatcher::set_global_default(Dispatch::new(subscriber)).expect("failed to set global logger"); 18 | } 19 | 20 | // https://github.com/tokio-rs/axum/discussions/1894 21 | pub async fn shutdown_signal() { 22 | let ctrl_c = async { 23 | signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); 24 | }; 25 | 26 | let terminate = async { 27 | signal::unix::signal(signal::unix::SignalKind::terminate()) 28 | .expect("failed to install signal handler") 29 | .recv() 30 | .await; 31 | }; 32 | 33 | tokio::select! { 34 | _ = ctrl_c => {}, 35 | _ = terminate => {}, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod html; 3 | 4 | use std::sync::Arc; 5 | 6 | use axum::http::StatusCode; 7 | use axum::{extract::Request, middleware::Next, response::IntoResponse, routing::get, Router}; 8 | use reqwest::Method; 9 | use tower_http::cors::{Any, CorsLayer}; 10 | 11 | use crate::AppState; 12 | 13 | async fn check_api_token( 14 | req: Request, 15 | next: Next, 16 | ) -> Result { 17 | let ghs_token = std::env::var("GHS_API_TOKEN").unwrap_or_default(); 18 | let req_token = crate::helpers::get_header(&req, "x-api-token").unwrap_or_default(); 19 | if ghs_token.is_empty() || req_token != ghs_token { 20 | return Err((StatusCode::UNAUTHORIZED, "unauthorized".to_string())); 21 | } 22 | 23 | let res = next.run(req).await; 24 | Ok(res) 25 | } 26 | 27 | pub fn api_routes() -> Router> { 28 | let cors = CorsLayer::new().allow_methods([Method::GET]).allow_origin(Any); 29 | 30 | let router = Router::new() 31 | .route("/repos", get(api::api_get_repos)) 32 | .layer(axum::middleware::from_fn(check_api_token)) 33 | .layer(cors); 34 | 35 | router 36 | } 37 | 38 | pub fn html_routes() -> Router> { 39 | Router::new().route("/", get(html::index)).route("/:owner/:repo", get(html::repo_page)) 40 | } 41 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Mutex; 2 | 3 | use crate::{ 4 | db_client::{DbClient, RepoFilter, RepoTotals}, 5 | gh_client::GhClient, 6 | helpers::GhsFilter, 7 | types::Res, 8 | }; 9 | 10 | fn env_bool(key: &str) -> bool { 11 | let val = std::env::var(key).unwrap_or_else(|_| "false".to_string()).to_lowercase(); 12 | return val == "true" || val == "1"; 13 | } 14 | 15 | pub struct AppState { 16 | pub db: DbClient, 17 | pub gh: GhClient, 18 | pub filter: GhsFilter, 19 | pub include_private: bool, 20 | pub last_release: Mutex, 21 | } 22 | 23 | impl AppState { 24 | pub async fn new() -> Res { 25 | let gh_token = std::env::var("GITHUB_TOKEN").unwrap_or_default(); 26 | if gh_token.is_empty() { 27 | tracing::error!("missing GITHUB_TOKEN"); 28 | std::process::exit(1); 29 | } 30 | 31 | let db_path = std::env::var("DB_PATH").unwrap_or("./data/ghstats.db".to_string()); 32 | tracing::info!("db_path: {}", db_path); 33 | 34 | let db = DbClient::new(&db_path).await?; 35 | let gh = GhClient::new(gh_token)?; 36 | 37 | let filter = std::env::var("GHS_FILTER").unwrap_or_default(); 38 | let filter = GhsFilter::new(&filter); 39 | tracing::info!("{:?}", filter); 40 | 41 | let include_private = env_bool("GHS_INCLUDE_PRIVATE"); 42 | 43 | let last_release = Mutex::new(env!("CARGO_PKG_VERSION").to_string()); 44 | Ok(Self { db, gh, filter, include_private, last_release }) 45 | } 46 | 47 | pub async fn get_repos_filtered(&self, qs: &RepoFilter) -> Res> { 48 | let repos = self.db.get_repos(&qs).await?; 49 | let repos = repos.into_iter().filter(|x| self.filter.is_included(&x.name, x.fork, x.archived)); 50 | let repos = repos.collect::>(); 51 | Ok(repos) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /assets/app.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1280px) { .main-box { max-width: 1240px; } } 2 | @media (min-width: 1536px) { .main-box { max-width: 1480px; } } 3 | 4 | .table-popular { 5 | border: 2px solid var(--pico-card-background-color); 6 | } 7 | 8 | .table-popular th, .table-popular td { 9 | background-color: var(--pico-card-background-color); 10 | } 11 | 12 | .flex-row { display: flex; flex-direction: row; } 13 | .flex-col { display: flex; flex-direction: column; } 14 | .justify-center { justify-content: center; } 15 | .justify-end { justify-content: flex-end; } 16 | .justify-between { justify-content: space-between; } 17 | .items-center { align-items: center; } 18 | .grow { flex-grow: 1; } 19 | .grow-0 { flex-grow: 0; } 20 | .block { display: block; } 21 | .w-1\/2 { width: 50%; } 22 | .w-1\/3 { width: 33.333333%; } 23 | .w-2\/3 { width: 66.666667%; } 24 | .w-full { width: 100%; } 25 | .h-1\/2 { height: 50%; } 26 | .h-1\/3 { height: 33.333333%; } 27 | .h-2\/3 { height: 66.666667%; } 28 | .h-full { height: 100%; } 29 | .max-h-96 { max-height: 24rem; } 30 | .max-h-fit { max-height: fit-content; } 31 | .gap-2 { gap: 0.5rem; } 32 | .gap-4 { gap: 1rem; } 33 | .p-0 { padding: 0; } 34 | .pt-0 { padding-top: 0; } 35 | .pr-0 { padding-right: 0; } 36 | .pr-2 { padding-right: 0.5rem; } 37 | .pr-4 { padding-right: 1rem; } 38 | .pb-0 { padding-bottom: 0; } 39 | .pl-0 { padding-left: 0; } 40 | .m-0 { margin: 0; } 41 | .mt-0 { margin-top: 0; } 42 | .mr-0 { margin-right: 0; } 43 | .mb-0 { margin-bottom: 0; } 44 | .ml-0 { margin-left: 0; } 45 | .ml-1 { margin-left: 0.25rem; } 46 | .ml-0\.5 { margin-left: 0.125rem; } 47 | .no-underline { text-decoration: none; } 48 | .text-center { text-align: center; } 49 | .text-left { text-align: left; } 50 | .text-right { text-align: right; } 51 | .font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } 52 | .cursor-pointer { cursor: pointer; } 53 | .select-none { user-select: none; } 54 | -------------------------------------------------------------------------------- /assets/app.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/68140000/3664464 2 | const mouseLinePlugin = { 3 | afterDraw: chart => { 4 | if (!chart.tooltip?._active?.length) return; 5 | let x = chart.tooltip._active[0].element.x; 6 | let yAxis = chart.scales.y; 7 | let ctx = chart.ctx; 8 | ctx.save(); 9 | ctx.beginPath(); 10 | ctx.moveTo(x, yAxis.top); 11 | ctx.lineTo(x, yAxis.bottom); 12 | ctx.lineWidth = 1; 13 | ctx.strokeStyle = 'rgba(0, 0, 255, 0.4)'; 14 | ctx.stroke(); 15 | ctx.restore(); 16 | }, 17 | }; 18 | 19 | const renderMetrics = (canvasId, metrics, uniqueCol, countCol) => { 20 | const ctx = document.getElementById(canvasId); 21 | new Chart(ctx, { 22 | type: 'bar', 23 | data: { 24 | labels: metrics.map(x => x.date.split('T')[0]), 25 | datasets: [ 26 | { label: 'Unique', data: metrics.map(x => x[uniqueCol]), borderWidth: 0, borderRadius: 4 }, 27 | { label: 'Count', data: metrics.map(x => x[countCol]), borderWidth: 0, borderRadius: 4 }, 28 | ], 29 | }, 30 | options: { 31 | responsive: true, 32 | interaction: { mode: 'index' }, 33 | scales: { 34 | x: { stacked: true, type: 'time', time: { tooltipFormat: 'yyyy-MM-dd' } }, 35 | y: { beginAtZero: true }, 36 | }, 37 | plugins: { 38 | legend: { display: false }, 39 | // title: { display: true, text: uniqueCol.split('_')[0].toUpperCase() } 40 | tooltip: { intersect: false }, 41 | }, 42 | }, 43 | plugins: [mouseLinePlugin], 44 | }); 45 | }; 46 | 47 | const renderStars = (canvasId, stars) => { 48 | const ctx = document.getElementById(canvasId); 49 | new Chart(ctx, { 50 | type: 'line', 51 | data: { 52 | labels: stars.map(x => x.date.split('T')[0]), 53 | datasets: [{ label: '', data: stars.map(x => x.stars), pointStyle: false, tension: 0.0 }], 54 | }, 55 | options: { 56 | responsive: true, 57 | maintainAspectRatio: false, 58 | interaction: { mode: 'index' }, 59 | scales: { 60 | x: { stacked: true, type: 'time', time: { tooltipFormat: 'yyyy-MM-dd' } }, 61 | y: { beginAtZero: true }, 62 | }, 63 | plugins: { 64 | legend: { display: false }, 65 | title: { display: false, text: 'Stars', font: { size: 20 }, align: 'start' }, 66 | tooltip: { intersect: false }, 67 | }, 68 | }, 69 | plugins: [mouseLinePlugin], 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | packages: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/cache@v4 16 | with: 17 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 18 | restore-keys: ${{ runner.os }}-cargo- 19 | save-always: true 20 | path: | 21 | ~/.cargo/bin/ 22 | ~/.cargo/registry/index/ 23 | ~/.cargo/registry/cache/ 24 | ~/.cargo/git/db/ 25 | target/ 26 | 27 | - run: rustup update --no-self-update stable && rustup default stable 28 | - run: cargo fmt --check 29 | - run: cargo check --release --locked 30 | - run: cargo test 31 | 32 | - uses: docker/setup-qemu-action@v3 33 | - uses: docker/setup-buildx-action@v3 34 | 35 | - name: Login to ghcr.io 36 | if: ${{ github.ref == 'refs/heads/main' }} || ${{ startsWith(github.ref, 'refs/tags/v') }} 37 | uses: docker/login-action@v3 38 | with: 39 | registry: ghcr.io 40 | username: ${{ github.actor }} 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: build & push (main) 44 | if: ${{ github.ref == 'refs/heads/main' }} 45 | uses: docker/build-push-action@v6 46 | with: 47 | push: true 48 | platforms: linux/amd64,linux/arm64 49 | cache-from: type=gha 50 | cache-to: type=gha,mode=max 51 | tags: | 52 | ghcr.io/${{ github.repository }}:main 53 | 54 | - name: build & push (tag) 55 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 56 | uses: docker/build-push-action@v6 57 | with: 58 | push: true 59 | platforms: linux/amd64,linux/arm64 60 | cache-from: type=gha 61 | cache-to: type=gha,mode=max 62 | tags: | 63 | ghcr.io/${{ github.repository }}:${{ github.ref_name }} 64 | ghcr.io/${{ github.repository }}:latest 65 | 66 | - name: Generate a changelog 67 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 68 | id: git-cliff 69 | uses: orhun/git-cliff-action@v4 70 | with: 71 | args: --latest 72 | 73 | - name: Create Github Release 74 | if: ${{ startsWith(github.ref, 'refs/tags/v') }} 75 | uses: softprops/action-gh-release@v2 76 | with: 77 | body: ${{ steps.git-cliff.outputs.content }} 78 | env: 79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # from: https://github.com/orhun/git-cliff/blob/main/cliff.toml 2 | 3 | [changelog] 4 | trim = true 5 | postprocessors = [{ pattern = '', replace = "https://github.com/vladkens/ghstats" }] 6 | body = """ 7 | {%- macro remote_url() -%} 8 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }} 9 | {%- endmacro -%} 10 | 11 | {% macro print_commit(commit) -%} 12 | - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ 13 | {% if commit.breaking %}[**breaking**] {% endif %}\ 14 | {{ commit.message | upper_first }} - \ 15 | ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\ 16 | {% endmacro -%} 17 | 18 | {% if version %}\ 19 | {% if previous.version %}\ 20 | ## [{{ version | trim_start_matches(pat="v") }}]\ 21 | ({{ self::remote_url() }}/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }} 22 | {% else %}\ 23 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 24 | {% endif %}\ 25 | {% else %}\ 26 | ## [unreleased] 27 | {% endif %}\ 28 | 29 | {% for group, commits in commits | group_by(attribute="group") %} 30 | ### {{ group | striptags | trim | upper_first }} 31 | {% for commit in commits 32 | | filter(attribute="scope") 33 | | sort(attribute="scope") %} 34 | {{ self::print_commit(commit=commit) }} 35 | {%- endfor %} 36 | {% for commit in commits %} 37 | {%- if not commit.scope -%} 38 | {{ self::print_commit(commit=commit) }} 39 | {% endif -%} 40 | {% endfor -%} 41 | {% endfor -%} 42 | {%- if github -%} 43 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} 44 | ## New Contributors ❤️ 45 | {% endif %}\ 46 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} 47 | * @{{ contributor.username }} made their first contribution 48 | {%- if contributor.pr_number %} in \ 49 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \ 50 | {%- endif %} 51 | {%- endfor -%} 52 | {%- endif %} 53 | """ 54 | 55 | [git] 56 | conventional_commits = true 57 | filter_unconventional = true 58 | split_commits = false 59 | filter_commits = false 60 | tag_pattern = "v[0-9].*" 61 | topo_order = false 62 | sort_commits = "newest" 63 | commit_preprocessors = [ 64 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))" }, 65 | ] 66 | commit_parsers = [ 67 | { message = "^feat", group = " Features" }, 68 | { message = "^fix", group = " Bug Fixes" }, 69 | { message = "^chore", group = " Other" }, 70 | { message = "^ci", skip = true }, 71 | ] 72 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::{response::IntoResponse, routing::get, Router}; 4 | use db_client::RepoFilter; 5 | use reqwest::StatusCode; 6 | use state::AppState; 7 | use tower_http::trace::{self, TraceLayer}; 8 | use tracing::Level; 9 | use types::Res; 10 | 11 | mod db_client; 12 | mod gh_client; 13 | mod helpers; 14 | mod routes; 15 | mod state; 16 | mod types; 17 | mod utils; 18 | 19 | async fn check_new_release(state: Arc) -> Res { 20 | let tag = state.gh.get_latest_release_ver("vladkens/ghstats").await?; 21 | let mut last_tag = state.last_release.lock().unwrap(); 22 | if *last_tag != tag { 23 | tracing::info!("new release available: {} -> {}", *last_tag, tag); 24 | *last_tag = tag.clone(); 25 | } 26 | 27 | Ok(()) 28 | } 29 | 30 | async fn start_cron(state: Arc) -> Res { 31 | use tokio_cron_scheduler::{Job, JobScheduler}; 32 | 33 | // note: for development, uncomment to update metrics on start 34 | helpers::update_metrics(state.clone()).await?; 35 | 36 | // if new db, update metrics immediately 37 | let repos = state.db.get_repos(&RepoFilter::default()).await?; 38 | if repos.len() == 0 { 39 | tracing::info!("no repos found, load initial metrics"); 40 | match helpers::update_metrics(state.clone()).await { 41 | Err(e) => tracing::error!("failed to update metrics: {:?}", e), 42 | Ok(_) => {} 43 | } 44 | } else { 45 | state.db.update_deltas().await?; 46 | } 47 | 48 | // https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28 49 | // >> All of these requests count towards your personal rate limit of 5,000 requests per hour. 50 | 51 | // https://docs.github.com/en/repositories/viewing-activity-and-data-for-your-repository/viewing-traffic-to-a-repository 52 | // >> Full clones and visitor information update hourly, while referring sites and popular content sections update daily. 53 | 54 | // last minute of every hour 55 | let job = Job::new_async("0 59 * * * *", move |_, _| { 56 | let state = state.clone(); 57 | Box::pin(async move { 58 | let _ = check_new_release(state.clone()).await; 59 | 60 | match helpers::update_metrics(state.clone()).await { 61 | Err(e) => tracing::error!("failed to update metrics: {:?}", e), 62 | Ok(_) => {} 63 | } 64 | }) 65 | })?; 66 | 67 | let runner = JobScheduler::new().await?; 68 | runner.start().await?; 69 | runner.add(job).await?; 70 | 71 | Ok(()) 72 | } 73 | 74 | async fn health() -> impl IntoResponse { 75 | let msg = serde_json::json!({ "status": "ok" }); 76 | (StatusCode::OK, axum::response::Json(msg)) 77 | } 78 | 79 | #[tokio::main] 80 | async fn main() -> Res { 81 | dotenvy::dotenv().ok(); 82 | utils::init_logger(); 83 | 84 | let brand = format!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 85 | tracing::info!("{}", brand); 86 | 87 | let router = Router::new() 88 | .nest("/api", routes::api_routes()) 89 | .merge(routes::html_routes()) 90 | .layer( 91 | TraceLayer::new_for_http() 92 | .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) 93 | .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), 94 | ) 95 | .route("/health", get(health)); // do not show logs for this route 96 | 97 | let state = Arc::new(AppState::new().await?); 98 | let service = router.with_state(state.clone()).into_make_service(); 99 | 100 | let cron_state = state.clone(); 101 | tokio::spawn(async move { 102 | loop { 103 | match start_cron(cron_state.clone()).await { 104 | Err(e) => { 105 | tracing::error!("failed to start cron: {:?}", e); 106 | tokio::time::sleep(std::time::Duration::from_secs(10)).await; 107 | } 108 | Ok(_) => break, 109 | } 110 | } 111 | }); 112 | 113 | let host = std::env::var("HOST").unwrap_or("127.0.0.1".to_string()); 114 | let port = std::env::var("PORT").unwrap_or("8080".to_string()); 115 | let addr = format!("{}:{}", host, port); 116 | 117 | let listener = tokio::net::TcpListener::bind(&addr).await?; 118 | tracing::info!("listening on http://{}", addr); 119 | axum::serve(listener, service).with_graceful_shutdown(utils::shutdown_signal()).await?; 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ghstats 2 | 3 |
4 | 5 | Self-hosted dashboard for tracking GitHub repos traffic history longer than 14 days. 6 | 7 | [version](https://github.com/vladkens/ghstats/releases) 8 | [license](https://github.com/vladkens/ghstats/blob/main/LICENSE) 9 | [donate](https://buymeacoffee.com/vladkens) 10 | 11 |
12 | 13 |
14 | preview 15 |
16 | 17 | ## 🌟 Features 18 | 19 | - Collect & store traffic metrics for all your repos 20 | - List of repos and informative dashboard for each 21 | - No React / Next / Postgres etc, just single and small Docker image (20MB) & SQLite 22 | 23 | ## 🚀 Usage 24 | 25 | ```sh 26 | docker run -d --env-file .env -p 8080:8080 -v ./data:/app/data --name ghstats ghcr.io/vladkens/ghstats:latest 27 | ``` 28 | 29 | Or Docker Compose: 30 | 31 | ```yaml 32 | services: 33 | ghstats: 34 | image: ghcr.io/vladkens/ghstats:latest 35 | container_name: ghstats 36 | restart: always 37 | environment: 38 | - GITHUB_TOKEN=??? 39 | env_file: .env # or with .env file 40 | ports: 41 | - 8080:8080 42 | volumes: 43 | - ./data:/app/data 44 | ``` 45 | 46 | ### Github token generation 47 | 48 | `ghstats` needs Github Token to collect traffic data from API. Token can be obtained with following steps: 49 | 50 | 1. Go to https://github.com/settings/tokens 51 | 2. Generate new token > Generate new token (classic) 52 | 3. Enter name, e.g.: `ghstats`. Scopes: `public_repo` 53 | 4. Click genereate token & copy it 54 | 5. Save token to `.env` file with name `GITHUB_TOKEN=ghp_XXX` 55 | 56 | Note: If you want to access private repos too, choose full `repo` scope and set `GHS_INCLUDE_PRIVATE=true` to env. 57 | 58 | ## How it works? 59 | 60 | Every hour `ghstats` loads the list of public repositories and their statistics, and saves the data in SQLite. If at the first startup there is no repositories in the database, synchronization will happen immediately, if `ghstats` is restarted again, synchronization will be performed according to the scheduler. Data is stored per day, re-fetching data for the current day will update existing records in the database. 61 | 62 | All public repositories that can be accessed are saved. If you need more detailed configuration – open PR please. 63 | 64 | ## Configuration 65 | 66 | ### Host & Port 67 | 68 | You can to change default host / port app will run on with `HOST` (default `0.0.0.0`) and `PORT` (default `8080`) environment variables. 69 | 70 | ### Custom links 71 | 72 | If you plan to display your stats publicly, there is an option to add custom links to the header via environment variables, e.g.: 73 | 74 | ```sh 75 | GHS_CUSTOM_LINKS="Blog|https://medium.com/@vladkens,Github|https://github.com/vladkens,Buy me a coffee|https://buymeacoffee.com/vladkens" 76 | ``` 77 | 78 | ### Filter repos 79 | 80 | You can filter repos for display (and data collection). You can select a specific org/user or a specific list of repositories. This is configured via the `GHS_FILTER` environment variable. You can use negation in the rules to remove a specific repo or org/user using the `!` symbol. By default, all repos are shown. 81 | 82 | _Note: Statistics on previously downloaded repos remain in database, but they are hidden from display._ 83 | 84 | Usage examples: 85 | 86 | ```sh 87 | GHS_FILTER=vladkens/macmon,vladkens/ghstats # show only this two repo 88 | GHS_FILTER=vladkens/*,foo-org/bar # show all vladkens repos and one repo from `foo-org` 89 | GHS_FILTER=vladkens/*,!vladkens/apigen-ts # show all vladkens repos except `apigen-ts` 90 | GHS_FILTER=*,!vladkens/apigen-ts,!foo-org/bar # show all repos expect two 91 | 92 | GHS_FILTER=*,!fork # show all repos expect forks 93 | GHS_FILTER=vladkens/*,!fork # show all vladkens repos expect forks 94 | GHS_FILTER=*,vladkens/some-fork,!fork # show all repos expect forks and keep `some-fork` 95 | 96 | GHS_FILTER=*,!archived # show all repos expect archived 97 | ``` 98 | 99 | Filtering rules: 100 | 101 | - If no filter provided all repos will be shown (implicitly `*`) 102 | - There are two kind of rules: direct (`foo/bar`, `foo/*`) and meta (`*`, `!fork`, `!archived`) 103 | - Direct rule can be wildcard (`foo/*` – include all repos of `foo` org / user) 104 | - Direct rules are applied first, then meta 105 | - If no direct rules specified, all repos included by default (implicitly `*`) 106 | - If at least one direct rule – all repos excluded by default (pass `*` explicitly to include all) 107 | - Meta-exclusion rules are: `!fork`, `!archived` 108 | - Wildcard rules do not work with meta-exclusion rules 109 | 110 | ### API endpoint 111 | 112 | You have the ability to get collected data by `ghstats` via API. At the moment there is only one method available to get all repos list – if you need other data – open PR, please. `GHS_API_TOKEN` environment variable must be set for the API to work. All API calls if protected by `x-api-token` header, which should be same with `GHS_API_TOKEN` variable. CORS is enabled for all hosts, so you can access API from personal pages. 113 | 114 | #### Endpoints 115 | 116 | `/api/repos` – will return list of all repos and overall metrics. Data returted in JSON format. Usage example: 117 | 118 | ```sh 119 | curl -H "x-api-token:1234" http://127.0.0.1:8080/api/repos 120 | ``` 121 | 122 | ```jsonc 123 | { 124 | "total_count": 20, 125 | "total_stars": 1000, 126 | "total_forks": 200, 127 | "total_views": 20000, 128 | "total_clones": 500, 129 | "items": [ 130 | { 131 | "id": 833875266, 132 | "name": "vladkens/ghstats", 133 | "description": "🤩📈 Self-hosted dashboard for tracking GitHub repos traffic history longer than 14 days.", 134 | "date": "2024-09-08T00:00:00Z", 135 | "stars": 110, 136 | "forks": 1, 137 | "watchers": 110, 138 | "issues": 5, 139 | "prs": 1, 140 | "clones_count": 90, 141 | "clones_uniques": 45, 142 | "views_count": 1726, 143 | "views_uniques": 659 144 | } 145 | // ... 146 | ] 147 | } 148 | ``` 149 | 150 | ## 🤝 Contributing 151 | 152 | All contributions are welcome! Feel free to open an issue or submit a pull request. 153 | 154 | ## 🔍 See also 155 | 156 | - [repohistory](https://github.com/repohistory/repohistory) – NodeJS application as a service. 157 | -------------------------------------------------------------------------------- /src/gh_client.rs: -------------------------------------------------------------------------------- 1 | use std::{time::Duration, vec}; 2 | 3 | use reqwest::{ 4 | header::{HeaderMap, HeaderValue}, 5 | RequestBuilder, 6 | }; 7 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 8 | 9 | use crate::types::Res; 10 | 11 | // MARK: Types 12 | 13 | #[derive(Debug, Deserialize, Serialize)] 14 | pub struct Repo { 15 | pub id: u64, 16 | pub full_name: String, 17 | pub description: Option, 18 | pub stargazers_count: u32, 19 | pub forks_count: u32, 20 | pub watchers_count: u32, 21 | pub open_issues_count: u32, 22 | pub fork: bool, 23 | pub archived: bool, 24 | } 25 | 26 | #[derive(Debug, Deserialize, Serialize)] 27 | pub struct PullRequest { 28 | pub id: u64, 29 | pub title: String, 30 | } 31 | 32 | #[derive(Debug, Deserialize, Serialize)] 33 | pub struct TrafficDaily { 34 | pub timestamp: String, 35 | pub uniques: u32, 36 | pub count: u32, 37 | } 38 | 39 | #[derive(Debug, Deserialize, Serialize)] 40 | pub struct RepoClones { 41 | pub uniques: u32, 42 | pub count: u32, 43 | pub clones: Vec, 44 | } 45 | 46 | #[derive(Debug, Deserialize, Serialize)] 47 | pub struct RepoViews { 48 | pub uniques: u32, 49 | pub count: u32, 50 | pub views: Vec, 51 | } 52 | 53 | #[derive(Debug, Deserialize, Serialize)] 54 | pub struct RepoPopularPath { 55 | pub path: String, 56 | pub title: String, 57 | pub count: u32, 58 | pub uniques: u32, 59 | } 60 | 61 | #[derive(Debug, Deserialize, Serialize)] 62 | pub struct RepoReferrer { 63 | pub referrer: String, 64 | pub count: u32, 65 | pub uniques: u32, 66 | } 67 | 68 | #[derive(Debug, Deserialize, Serialize)] 69 | pub struct RepoStar { 70 | pub starred_at: String, 71 | } 72 | 73 | // MARK: GhClient 74 | 75 | pub struct GhClient { 76 | client: reqwest::Client, 77 | base_url: String, 78 | } 79 | 80 | impl GhClient { 81 | pub fn new(token: String) -> Res { 82 | let user_agent = format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 83 | 84 | let mut auth_header = HeaderValue::from_str(&format!("Bearer {}", token))?; 85 | auth_header.set_sensitive(true); 86 | 87 | let mut headers = HeaderMap::new(); 88 | headers.insert("Accept", HeaderValue::from_static("application/vnd.github+json")); 89 | headers.insert("X-GitHub-Api-Version", HeaderValue::from_static("2022-11-28")); 90 | headers.insert("Authorization", auth_header); 91 | headers.insert("User-Agent", HeaderValue::from_str(&user_agent)?); 92 | 93 | let client = reqwest::Client::builder() 94 | .default_headers(headers) 95 | .read_timeout(Duration::from_secs(30)) 96 | .build()?; 97 | 98 | let base_url = "https://api.github.com".to_string(); 99 | Ok(GhClient { client, base_url }) 100 | } 101 | 102 | async fn with_pagination(&self, req: RequestBuilder) -> Res> { 103 | let mut items: Vec = vec![]; 104 | let per_page = 100; 105 | let mut page = 1; 106 | 107 | loop { 108 | let req = req.try_clone().unwrap(); 109 | let req = req.query(&[("per_page", &per_page.to_string())]); 110 | let req = req.query(&[("page", &page.to_string())]); 111 | let rep = req.send().await?.error_for_status()?; 112 | 113 | let cur = match rep.headers().get("link") { 114 | Some(l) => l.to_str().unwrap().to_string(), 115 | None => "".to_string(), 116 | }; 117 | 118 | let dat = rep.json::>().await?; 119 | items.extend(dat); 120 | 121 | match cur.contains(r#"rel="next""#) { 122 | true => page += 1, 123 | false => break, 124 | } 125 | } 126 | 127 | Ok(items) 128 | } 129 | 130 | // https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user 131 | pub async fn get_repos(&self, include_private: bool) -> Res> { 132 | let visibility = if include_private { "all" } else { "public" }; 133 | let url = format!("{}/user/repos?visibility={}", self.base_url, visibility); 134 | let req = self.client.get(url); 135 | let dat: Vec = self.with_pagination(req).await?; 136 | Ok(dat) 137 | } 138 | 139 | pub async fn get_open_pull_requests(&self, repo: &str) -> Res> { 140 | let url = format!("{}/repos/{}/pulls?state=open", self.base_url, repo); 141 | let req = self.client.get(url); 142 | let dat: Vec = self.with_pagination(req).await?; 143 | Ok(dat) 144 | } 145 | 146 | // https://docs.github.com/en/rest/metrics/traffic?apiVersion=2022-11-28 147 | pub async fn traffic_clones(&self, repo: &str) -> Res { 148 | let url = format!("{}/repos/{}/traffic/clones", self.base_url, repo); 149 | let rep = self.client.get(url).send().await?.error_for_status()?; 150 | let dat = rep.json::().await?; 151 | Ok(dat) 152 | } 153 | 154 | pub async fn traffic_views(&self, repo: &str) -> Res { 155 | let url = format!("{}/repos/{}/traffic/views", self.base_url, repo); 156 | let rep = self.client.get(url).send().await?.error_for_status()?; 157 | let dat = rep.json::().await?; 158 | Ok(dat) 159 | } 160 | 161 | pub async fn traffic_paths(&self, repo: &str) -> Res> { 162 | let url = format!("{}/repos/{}/traffic/popular/paths", self.base_url, repo); 163 | let rep = self.client.get(url).send().await?.error_for_status()?; 164 | let dat = rep.json::>().await?; 165 | Ok(dat) 166 | } 167 | 168 | pub async fn traffic_refs(&self, repo: &str) -> Res> { 169 | let url = format!("{}/repos/{}/traffic/popular/referrers", self.base_url, repo); 170 | let rep = self.client.get(url).send().await?.error_for_status()?; 171 | let dat = rep.json::>().await?; 172 | Ok(dat) 173 | } 174 | 175 | pub async fn get_latest_release_ver(&self, repo: &str) -> Res { 176 | let url = format!("{}/repos/{}/releases/latest", self.base_url, repo); 177 | let rep = self.client.get(url).send().await?.error_for_status()?; 178 | let dat = rep.json::().await?; 179 | let ver = dat["tag_name"].as_str().unwrap().to_string(); 180 | let ver = ver.trim_start_matches("v").to_string(); 181 | Ok(ver) 182 | } 183 | 184 | pub async fn get_stars(&self, repo: &str) -> Res> { 185 | let url = format!("{}/repos/{}/stargazers", self.base_url, repo); 186 | let req = self.client.get(url).header("Accept", "application/vnd.github.v3.star+json"); 187 | 188 | let dat: Vec = self.with_pagination(req).await?; 189 | return Ok(dat); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/routes/html.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use axum::extract::{Path, Query, Request, State}; 4 | use maud::{html, Markup, PreEscaped}; 5 | use thousands::Separable; 6 | 7 | use crate::db_client::{ 8 | DbClient, Direction, PopularFilter, PopularKind, PopularSort, RepoFilter, RepoSort, RepoTotals, 9 | }; 10 | use crate::helpers::truncate_middle; 11 | use crate::types::{AppError, HtmlRes}; 12 | use crate::AppState; 13 | 14 | #[derive(Debug)] 15 | struct TablePopularItem { 16 | item: (String, Option), // title, url 17 | uniques: i64, 18 | count: i64, 19 | } 20 | 21 | fn get_hx_target(req: &Request) -> Option<&str> { 22 | crate::helpers::get_header(req, "hx-target") 23 | } 24 | 25 | fn maybe_url(item: &(String, Option)) -> Markup { 26 | let (name, url) = item; 27 | 28 | match url { 29 | Some(url) => html!(a href=(url) { (truncate_middle(name, 40)) }), 30 | None => html!(span { (name) }), 31 | } 32 | } 33 | 34 | fn get_custom_links() -> Vec<(String, String)> { 35 | let links = std::env::var("GHS_CUSTOM_LINKS").unwrap_or_default(); 36 | let links: Vec<(String, String)> = links 37 | .split(",") 38 | .map(|x| { 39 | let parts: Vec<&str> = x.split("|").collect(); 40 | if parts.len() != 2 { 41 | return None; 42 | } 43 | 44 | if parts[0].is_empty() || parts[1].is_empty() { 45 | return None; 46 | } 47 | 48 | Some((parts[0].to_string(), parts[1].to_string())) 49 | }) 50 | .filter(|x| x.is_some()) 51 | .map(|x| x.unwrap()) 52 | .collect(); 53 | 54 | links 55 | } 56 | 57 | fn base(state: &Arc, navs: Vec<(String, Option)>, inner: Markup) -> Markup { 58 | let (app_name, app_version) = (env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); 59 | 60 | let last_release = state.last_release.lock().unwrap().clone(); 61 | let is_new_release = last_release != app_version; 62 | 63 | let title = match navs.len() { 64 | 0 => app_name, 65 | _ => &format!("{} · {}", navs.last().unwrap().0, app_name), 66 | }; 67 | 68 | let favicon = include_str!("../../assets/favicon.svg") 69 | .replace("\n", "") 70 | .replace("\"", "%22") 71 | .replace("#", "%23"); 72 | let favicon = format!("data:image/svg+xml,{}", favicon); 73 | 74 | html!( 75 | html { 76 | head { 77 | meta charset="utf-8" {} 78 | meta name="viewport" content="width=device-width, initial-scale=1" {} 79 | title { (title) } 80 | 81 | link rel="icon" type="image/svg+xml" href=(PreEscaped(favicon)) {} 82 | link rel="stylesheet" href="https://unpkg.com/@picocss/pico@2.0" {} 83 | script src="https://unpkg.com/chart.js@4.4" {} 84 | script src="https://unpkg.com/luxon@3.5" {} 85 | script src="https://unpkg.com/chartjs-adapter-luxon@1.3" {} 86 | script src="https://unpkg.com/htmx.org@2.0" {} 87 | style { (PreEscaped(include_str!("../../assets/app.css"))) } 88 | } 89 | body { 90 | main class="container-fluid pt-0 main-box" { 91 | div class="flex-row items-center gap-2 justify-between" { 92 | nav aria-label="breadcrumb" { 93 | ul { 94 | li { a href="/" { "Repos" } } 95 | @for item in navs { 96 | li { (maybe_url(&item)) } 97 | } 98 | } 99 | } 100 | 101 | div class="flex-row items-center gap-2" { 102 | div class="flex-row items-center gap-4 pr-4" style="font-size: 18px;" { 103 | @for (name, url) in &get_custom_links() { 104 | a href=(url) target="_blank" { (name) } 105 | } 106 | } 107 | 108 | @if is_new_release { 109 | a href=(format!("https://github.com/vladkens/ghstats/releases/tag/v{last_release}")) 110 | target="_blank" class="no-underline" 111 | data-tooltip="New release available!" data-placement="bottom" { "🚨" } 112 | } 113 | 114 | a href="https://github.com/vladkens/ghstats" 115 | class="secondary flex-row items-center gap-2 no-underline font-mono" 116 | style="font-size: 18px;" 117 | target="_blank" 118 | { 119 | (format!("{} v{}", app_name, app_version)) 120 | } 121 | } 122 | } 123 | 124 | (inner) 125 | } 126 | } 127 | } 128 | ) 129 | } 130 | 131 | async fn popular_table( 132 | db: &DbClient, 133 | repo: &str, 134 | kind: &PopularKind, 135 | qs: &PopularFilter, 136 | ) -> HtmlRes { 137 | let items = db.get_popular_items(repo, kind, qs).await?; 138 | let items: Vec = match kind { 139 | PopularKind::Refs => items 140 | .into_iter() 141 | .map(|x| TablePopularItem { item: (x.name, None), uniques: x.uniques, count: x.count }) 142 | .collect(), 143 | PopularKind::Path => items 144 | .into_iter() 145 | .map(|x| { 146 | let prefix = format!("/{}", repo); 147 | let mut name = x.name.replace(&prefix, ""); 148 | if name.is_empty() { 149 | name = "/".to_string(); 150 | } 151 | 152 | let item = (name, Some(format!("https://github.com{}", x.name))); 153 | TablePopularItem { item, uniques: x.uniques, count: x.count } 154 | }) 155 | .collect(), 156 | }; 157 | 158 | let name = match kind { 159 | PopularKind::Refs => "Referring sites", 160 | PopularKind::Path => "Popular paths", 161 | }; 162 | 163 | let html_id = match kind { 164 | PopularKind::Refs => "refs_table", 165 | PopularKind::Path => "path_table", 166 | }; 167 | 168 | let cols: Vec<(&str, Box Markup>, PopularSort)> = vec![ 169 | (name, Box::new(|x| maybe_url(&x.item)), PopularSort::Name), 170 | ("Views", Box::new(|x| html!((x.count.separate_with_commas()))), PopularSort::Count), 171 | ("Unique", Box::new(|x| html!((x.uniques.separate_with_commas()))), PopularSort::Uniques), 172 | ]; 173 | 174 | fn filter_url(repo: &str, qs: &PopularFilter, col: &PopularSort) -> String { 175 | let dir = match qs.sort == *col && qs.direction == Direction::Desc { 176 | true => "asc", 177 | false => "desc", 178 | }; 179 | 180 | format!("/{}?sort={}&direction={}&period={}", repo, col, dir, qs.period) 181 | } 182 | 183 | let html = html!( 184 | article id=(html_id) class="p-0 mb-0 table-popular" { 185 | table class="mb-0" { 186 | thead { 187 | tr { 188 | @for (idx, col) in cols.iter().enumerate() { 189 | th scope="col" .cursor-pointer .select-none .text-right[idx > 0] 190 | hx-trigger="click" 191 | hx-get=(filter_url(repo, qs, &col.2)) 192 | hx-target=(format!("#{}", html_id)) 193 | hx-swap="outerHTML" 194 | { 195 | (col.0) 196 | @if col.2 == qs.sort { 197 | span class="ml-0.5" { 198 | @if qs.direction == Direction::Asc { "↑" } @else { "↓" } 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | 206 | tbody { 207 | @if items.is_empty() { 208 | tr { 209 | td colspan=(cols.len()) .text-center { "No data for given period" } 210 | } 211 | } 212 | 213 | @for item in items { 214 | tr { 215 | @for (idx, col) in cols.iter().enumerate() { 216 | td .text-right[idx > 0] { ((col.1)(&item)) } 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | ); 224 | 225 | Ok(html) 226 | } 227 | 228 | async fn repo_popular_tables(db: &DbClient, repo: &str, filter: &PopularFilter) -> HtmlRes { 229 | let html = html!( 230 | div id="popular_tables" class="grid" { 231 | (popular_table(db, repo, &PopularKind::Refs, filter).await?) 232 | (popular_table(db, repo, &PopularKind::Path, filter).await?) 233 | } 234 | ); 235 | 236 | return Ok(html); 237 | } 238 | 239 | pub async fn repo_page( 240 | State(state): State>, 241 | Path((owner, repo)): Path<(String, String)>, 242 | req: Request, 243 | ) -> HtmlRes { 244 | let repo = format!("{}/{}", owner, repo); 245 | let mut qs: Query = Query::try_from_uri(req.uri())?; 246 | let db = &state.db; 247 | 248 | let periods = vec![ 249 | (7, "Last 7 days"), 250 | (14, "Last 14 days"), 251 | (30, "Last 30 days"), 252 | (90, "Last 90 days"), 253 | (-1, "All time"), 254 | ]; 255 | 256 | qs.period = match periods.iter().all(|x| x.0 != qs.period) { 257 | true => 7, 258 | false => qs.period, 259 | }; 260 | 261 | match get_hx_target(&req) { 262 | Some("refs_table") => return Ok(popular_table(db, &repo, &PopularKind::Refs, &qs).await?), 263 | Some("path_table") => return Ok(popular_table(db, &repo, &PopularKind::Path, &qs).await?), 264 | Some("popular_tables") => return Ok(repo_popular_tables(&db, &repo, &qs).await?), 265 | _ => {} 266 | } 267 | 268 | let totals = match db.get_repo_totals(&repo).await? { 269 | Some(x) => x, 270 | None => return AppError::not_found(), 271 | }; 272 | 273 | if !state.filter.is_included(&totals.name, totals.fork, totals.archived) { 274 | return AppError::not_found(); 275 | } 276 | 277 | let metrics = db.get_metrics(&repo).await?; 278 | let stars = db.get_stars(&repo).await?; 279 | 280 | let html = html!( 281 | div class="grid" style="grid-template-columns: 1fr 2fr;" { 282 | div class="grid" style="grid-template-rows: 2fr 1fr; grid-template-columns: 1fr;" { 283 | article class="mb-0" { 284 | hgroup class="flex-row flex-col gap-2" { 285 | h3 { 286 | a href=(format!("https://github.com/{}", repo)) class="contrast" { (totals.name) } 287 | } 288 | p { (totals.description.unwrap_or("".to_string())) } 289 | } 290 | } 291 | 292 | div class="grid" { 293 | article class="flex-col" { 294 | h6 class="mb-0" { "Total Clones" } 295 | h4 class="mb-0 grow flex-row items-center" { 296 | (totals.clones_uniques.separate_with_commas()) 297 | " / " 298 | (totals.clones_count.separate_with_commas()) 299 | } 300 | } 301 | article class="flex-col" { 302 | h6 class="mb-0" { "Total Views" } 303 | h4 class="mb-0 grow flex-row items-center" { 304 | (totals.views_uniques.separate_with_commas()) 305 | " / " 306 | (totals.views_count.separate_with_commas()) 307 | } 308 | } 309 | } 310 | } 311 | 312 | article class="flex-col" { 313 | h6 { "Stars" } 314 | div class="grow" { canvas id="chart_stars" {} } 315 | } 316 | } 317 | 318 | div class="grid" { 319 | @for (title, canvas_id) in vec![("Clones", "chart_clones"), ("Views", "chart_views")] { 320 | article { 321 | h6 { (title) } 322 | canvas id=(canvas_id) {} 323 | } 324 | } 325 | } 326 | 327 | script { (PreEscaped(include_str!("../../assets/app.js"))) } 328 | script { 329 | "const Metrics = "(PreEscaped(serde_json::to_string(&metrics)?))";" 330 | "const Stars = "(PreEscaped(serde_json::to_string(&stars)?))";" 331 | "renderMetrics('chart_clones', Metrics, 'clones_uniques', 'clones_count');" 332 | "renderMetrics('chart_views', Metrics, 'views_uniques', 'views_count');" 333 | "renderStars('chart_stars', Stars);" 334 | } 335 | 336 | select name="period" hx-get=(format!("/{}", repo)) hx-target="#popular_tables" hx-swap="outerHTML" { 337 | @for (days, title) in &periods { 338 | option value=(days) selected[*days == qs.period] { (title) } 339 | } 340 | } 341 | 342 | (repo_popular_tables(db, &repo, &qs).await?) 343 | ); 344 | 345 | Ok(base(&state, vec![(repo, None)], html)) 346 | } 347 | 348 | // https://docs.rs/axum/latest/axum/extract/index.html#common-extractors 349 | pub async fn index(State(state): State>, req: Request) -> HtmlRes { 350 | // let qs: Query> = Query::try_from_uri(req.uri())?; 351 | let qs: Query = Query::try_from_uri(req.uri())?; 352 | let repos = state.get_repos_filtered(&qs).await?; 353 | 354 | let cols: Vec<(&str, Box Markup>, RepoSort)> = vec![ 355 | ("Name", Box::new(|x| html!(a href=(format!("/{}", x.name)) { (x.name) })), RepoSort::Name), 356 | ("Issues", Box::new(|x| html!((x.issues.separate_with_commas()))), RepoSort::Issues), 357 | ("PRs", Box::new(|x| html!((x.prs.separate_with_commas()))), RepoSort::Prs), 358 | ("Forks", Box::new(|x| html!((x.forks.separate_with_commas()))), RepoSort::Forks), 359 | ("Clones", Box::new(|x| html!((x.clones_count.separate_with_commas()))), RepoSort::Clones), 360 | ("Stars", Box::new(|x| html!((x.stars.separate_with_commas()))), RepoSort::Stars), 361 | ("Views", Box::new(|x| html!((x.views_count.separate_with_commas()))), RepoSort::Views), 362 | ]; 363 | 364 | fn filter_url(qs: &RepoFilter, col: &RepoSort) -> String { 365 | let dir = match qs.sort == *col && qs.direction == Direction::Desc { 366 | true => "asc", 367 | false => "desc", 368 | }; 369 | 370 | format!("/?sort={}&direction={}", col, dir) 371 | } 372 | 373 | let html = html!( 374 | table id="repos_table" { 375 | thead { 376 | tr { 377 | @for col in &cols { 378 | th scope="col" class="cursor-pointer select-none" 379 | hx-trigger="click" 380 | hx-get=(filter_url(&qs, &col.2)) 381 | hx-target="#repos_table" 382 | hx-swap="outerHTML" 383 | { 384 | (col.0) 385 | @if col.2 == qs.sort { 386 | span class="ml-0.5" { 387 | @if qs.direction == Direction::Asc { "↑" } @else { "↓" } 388 | } 389 | } 390 | } 391 | } 392 | } 393 | } 394 | tbody { 395 | @for repo in &repos { 396 | tr { 397 | @for col in &cols { 398 | td { ((col.1)(&repo)) } 399 | } 400 | } 401 | } 402 | } 403 | } 404 | ); 405 | 406 | match get_hx_target(&req) { 407 | Some("repos_table") => return Ok(html), 408 | _ => {} 409 | } 410 | 411 | Ok(base(&state, vec![], html)) 412 | } 413 | -------------------------------------------------------------------------------- /src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use axum::extract::Request; 4 | 5 | use crate::{ 6 | db_client::DbClient, 7 | gh_client::{GhClient, Repo}, 8 | state::AppState, 9 | types::Res, 10 | }; 11 | 12 | pub fn truncate_middle(text: &str, max_len: usize) -> String { 13 | if text.len() <= max_len { 14 | return text.to_string(); 15 | } 16 | 17 | let part_len = (max_len - 3) / 2; 18 | let start = &text[..part_len]; 19 | let end = &text[text.len() - part_len..]; 20 | 21 | format!("{}...{}", start, end) 22 | } 23 | 24 | pub fn get_header<'a>(req: &'a Request, name: &'a str) -> Option<&'a str> { 25 | match req.headers().get(name) { 26 | Some(x) => Some(x.to_str().unwrap_or_default()), 27 | None => None, 28 | } 29 | } 30 | 31 | async fn check_hidden_repos(db: &DbClient, repos: &Vec) -> Res { 32 | let now_ids = repos.iter().map(|r| r.id as i64).collect::>(); 33 | let was_ids = db.get_repos_ids().await?; 34 | let hidden = was_ids.into_iter().filter(|id| !now_ids.contains(id)).collect::>(); 35 | let _ = db.mark_repo_hidden(&hidden).await?; 36 | 37 | Ok(()) 38 | } 39 | 40 | pub async fn update_metrics(state: Arc) -> Res { 41 | let stime = std::time::Instant::now(); 42 | 43 | let date = chrono::Utc::now().to_utc().to_rfc3339(); 44 | let date = date.split("T").next().unwrap().to_owned() + "T00:00:00Z"; 45 | 46 | let repos = state.gh.get_repos(state.include_private).await?; 47 | let _ = check_hidden_repos(&state.db, &repos).await?; 48 | 49 | let repos = repos // 50 | .iter() 51 | .filter(|r| state.filter.is_included(&r.full_name, r.fork, r.archived)) 52 | .collect::>(); 53 | 54 | for repo in &repos { 55 | match update_repo_metrics(&state.db, &state.gh, &repo, &date).await { 56 | Err(e) => { 57 | tracing::warn!("failed to update metrics for {}: {:?}", repo.full_name, e); 58 | continue; 59 | } 60 | // Ok(_) => tracing::info!("updated metrics for {}", repo.full_name), 61 | Ok(_) => {} 62 | } 63 | } 64 | 65 | tracing::info!("update_metrics took {:?} for {} repos", stime.elapsed(), repos.len()); 66 | state.db.update_deltas().await?; 67 | sync_stars(&state.db, &state.gh).await?; 68 | 69 | Ok(()) 70 | } 71 | 72 | async fn update_repo_metrics(db: &DbClient, gh: &GhClient, repo: &Repo, date: &str) -> Res { 73 | let prs = gh.get_open_pull_requests(&repo.full_name).await?; 74 | let views = gh.traffic_views(&repo.full_name).await?; 75 | let clones = gh.traffic_clones(&repo.full_name).await?; 76 | let referrers = gh.traffic_refs(&repo.full_name).await?; 77 | 78 | let popular_paths = gh.traffic_paths(&repo.full_name).await?; 79 | 80 | db.insert_repo(&repo).await?; 81 | db.insert_stats(&repo, date, &prs).await?; 82 | db.insert_views(&repo, &views).await?; 83 | db.insert_clones(&repo, &clones).await?; 84 | db.insert_referrers(&repo, date, &referrers).await?; 85 | db.insert_paths(&repo, date, &popular_paths).await?; 86 | 87 | Ok(()) 88 | } 89 | 90 | /// Get stars history for a repo 91 | /// vec![(date_str, acc_stars, new_stars)), ...] 92 | pub async fn get_stars_history(gh: &GhClient, repo: &str) -> Res> { 93 | let stars = gh.get_stars(repo).await?; 94 | 95 | let mut dat: HashMap = HashMap::new(); 96 | for star in stars { 97 | let date = star.starred_at.split("T").next().unwrap().to_owned(); 98 | let date = format!("{date}T00:00:00Z"); // db stores dates as UTC midnight 99 | dat.entry(date).and_modify(|e| *e += 1).or_insert(1); 100 | } 101 | 102 | let mut dat = dat.into_iter().collect::>(); 103 | dat.sort_by(|a, b| a.0.cmp(&b.0)); 104 | 105 | let mut rs: Vec<(String, u32, u32)> = Vec::with_capacity(dat.len()); 106 | for i in 0..dat.len() { 107 | let (date, new_count) = &dat[i]; 108 | let acc_count = if i > 0 { rs[i - 1].1 + new_count } else { new_count.clone() }; 109 | rs.push((date.clone(), acc_count, new_count.clone())); 110 | } 111 | 112 | Ok(rs) 113 | } 114 | 115 | pub async fn sync_stars(db: &DbClient, gh: &GhClient) -> Res { 116 | let mut pages_collected = 0; 117 | 118 | let repos = db.repos_to_sync().await?; 119 | for repo in repos { 120 | let stime = std::time::Instant::now(); 121 | // tracing::info!("sync_stars for {}", repo.name); 122 | 123 | let stars = match get_stars_history(gh, &repo.name).await { 124 | Ok(stars) => stars, 125 | Err(e) => { 126 | tracing::warn!("failed to get stars for {}: {:?}", repo.name, e); 127 | break; 128 | } 129 | }; 130 | 131 | db.insert_stars(repo.id, &stars).await?; 132 | db.mark_repo_stars_synced(repo.id).await?; 133 | 134 | let stars_count = stars.iter().map(|(_, _, c)| c).sum::(); 135 | tracing::info!( 136 | "sync_stars for {} done in {:?}, {stars_count} starts added", 137 | repo.name, 138 | stime.elapsed(), 139 | ); 140 | 141 | // gh api rate limit is 5000 req/h, so this code will do up to 1000 req/h 142 | // to not block other possible user pipelines 143 | pages_collected += (stars_count + 99) / 100; 144 | if pages_collected > 1000 { 145 | tracing::info!("sync_stars: {} pages collected, will continue next hour", pages_collected); 146 | break; 147 | } 148 | } 149 | 150 | Ok(()) 151 | } 152 | 153 | #[derive(Debug)] 154 | pub struct GhsFilter { 155 | pub include_repos: Vec, 156 | pub exclude_repos: Vec, 157 | pub exclude_forks: bool, 158 | pub exclude_archs: bool, 159 | pub default_all: bool, 160 | } 161 | 162 | impl GhsFilter { 163 | pub fn new(rules: &str) -> Self { 164 | let mut default_all = false; 165 | let mut exclude_forks = false; 166 | let mut exclude_archs = false; 167 | let mut include_repos: Vec<&str> = Vec::new(); 168 | let mut exclude_repos: Vec<&str> = Vec::new(); 169 | 170 | let rules = rules.trim().to_lowercase(); 171 | for rule in rules.split(",").map(|x| x.trim()) { 172 | if rule.is_empty() { 173 | continue; 174 | } 175 | 176 | if rule == "*" { 177 | default_all = true; 178 | continue; 179 | } 180 | 181 | if rule == "!fork" { 182 | exclude_forks = true; 183 | continue; 184 | } 185 | 186 | if rule == "!archived" { 187 | exclude_archs = true; 188 | continue; 189 | } 190 | 191 | if rule.matches('/').count() != 1 { 192 | continue; 193 | } 194 | 195 | if rule.starts_with('!') { 196 | exclude_repos.push(rule.strip_prefix('!').unwrap()); 197 | } else { 198 | include_repos.push(rule); 199 | } 200 | } 201 | 202 | // if no repo rules, include all by default 203 | if exclude_repos.is_empty() && include_repos.is_empty() { 204 | default_all = true; 205 | } 206 | 207 | Self { 208 | include_repos: include_repos.into_iter().map(|x| x.to_string()).collect(), 209 | exclude_repos: exclude_repos.into_iter().map(|x| x.to_string()).collect(), 210 | exclude_forks, 211 | exclude_archs, 212 | default_all, 213 | } 214 | } 215 | 216 | pub fn is_included(&self, repo: &str, is_fork: bool, is_arch: bool) -> bool { 217 | let repo = repo.trim().to_lowercase(); 218 | if repo.is_empty() 219 | || repo.matches('/').count() != 1 220 | || repo.starts_with('/') 221 | || repo.ends_with('/') 222 | { 223 | return false; 224 | } 225 | 226 | for (flag, rules) in vec![(false, &self.exclude_repos), (true, &self.include_repos)] { 227 | for rule in rules { 228 | if rule == &repo { 229 | return flag; 230 | } 231 | 232 | // skip wildcards for forks / archived 233 | if (self.exclude_forks && is_fork) || (self.exclude_archs && is_arch) { 234 | continue; 235 | } 236 | 237 | if rule.ends_with("/*") 238 | && repo.starts_with(&rule[..rule.len() - 2]) 239 | && repo.chars().nth(rule.len() - 2) == Some('/') 240 | { 241 | return flag; 242 | } 243 | } 244 | } 245 | 246 | if self.exclude_forks && is_fork { 247 | return false; 248 | } 249 | 250 | if self.exclude_archs && is_arch { 251 | return false; 252 | } 253 | 254 | return self.default_all; 255 | } 256 | } 257 | 258 | #[cfg(test)] 259 | mod tests { 260 | use super::*; 261 | 262 | #[test] 263 | fn test_empty_fitler() { 264 | let r = &GhsFilter::new(""); 265 | 266 | assert!(r.is_included("foo/bar", false, false)); 267 | assert!(r.is_included("foo/baz", false, false)); 268 | assert!(r.is_included("abc/123", false, false)); 269 | assert!(r.is_included("abc/xyz-123", false, false)); 270 | 271 | // exclude invalid names 272 | assert!(!r.is_included("foo/", false, false)); 273 | assert!(!r.is_included("/bar", false, false)); 274 | assert!(!r.is_included("foo", false, false)); 275 | assert!(!r.is_included("foo/bar/baz", false, false)); 276 | 277 | // include forks / archived 278 | assert!(r.is_included("foo/bar", true, false)); 279 | assert!(r.is_included("foo/bar", false, true)); 280 | assert!(r.is_included("foo/bar", true, true)); 281 | } 282 | 283 | #[test] 284 | fn test_filter_names() { 285 | let r = &GhsFilter::new("foo/*,abc/xyz"); 286 | 287 | assert!(r.is_included("foo/bar", false, false)); 288 | assert!(r.is_included("foo/123", false, false)); 289 | assert!(r.is_included("abc/xyz", false, false)); 290 | 291 | assert!(!r.is_included("foo/bar/baz", false, false)); 292 | assert!(!r.is_included("abc/123", false, false)); 293 | 294 | // include forks / archived 295 | assert!(r.is_included("foo/bar", true, false)); 296 | assert!(r.is_included("foo/bar", false, true)); 297 | 298 | // exact org/user match 299 | let r = &GhsFilter::new("foo/*"); 300 | assert!(!r.is_included("fooo/bar", false, false)); 301 | } 302 | 303 | #[test] 304 | fn test_filter_names_case() { 305 | let r = &GhsFilter::new("foo/*,abc/xyz"); 306 | assert!(r.is_included("FOO/BAR", false, false)); 307 | assert!(r.is_included("Foo/Bar", false, false)); 308 | 309 | let r = &GhsFilter::new("FOO/*,Abc/XYZ"); 310 | assert!(r.is_included("foo/bar", false, false)); 311 | assert!(r.is_included("foo/baz", false, false)); 312 | assert!(r.is_included("abc/xyz", false, false)); 313 | } 314 | 315 | #[test] 316 | fn test_filter_all_expect() { 317 | let r = &GhsFilter::new("*"); 318 | assert!(r.is_included("foo/bar", false, false)); 319 | assert!(r.is_included("abc/123", false, false)); 320 | assert!(r.is_included("abc/123", true, false)); 321 | assert!(r.is_included("abc/123", true, true)); 322 | 323 | let r = &GhsFilter::new("-*"); // single rule invalid, include all 324 | assert!(r.is_included("foo/bar", false, false)); 325 | assert!(r.is_included("abc/123", false, false)); 326 | 327 | let r = &GhsFilter::new("*,!foo/bar,!abc/123"); 328 | assert!(!r.is_included("foo/bar", false, false)); 329 | assert!(!r.is_included("abc/123", false, false)); 330 | assert!(r.is_included("foo/baz", false, false)); 331 | assert!(r.is_included("abc/xyz", false, false)); 332 | 333 | let r = &GhsFilter::new("*,!foo/*"); 334 | assert!(!r.is_included("foo/bar", false, false)); 335 | assert!(!r.is_included("foo/baz", false, false)); 336 | assert!(r.is_included("abc/123", false, false)); 337 | assert!(r.is_included("abc/xyz", false, false)); 338 | } 339 | 340 | #[test] 341 | fn test_filter_names_only() { 342 | let r = &GhsFilter::new("foo/*,!foo/bar"); 343 | assert!(!r.is_included("abc/xyz", false, false)); 344 | assert!(!r.is_included("foo/bar", false, false)); 345 | assert!(!r.is_included("FOO/Bar", false, false)); 346 | 347 | assert!(r.is_included("foo/abc", false, false)); 348 | assert!(r.is_included("foo/abc", true, false)); 349 | assert!(r.is_included("foo/abc", true, true)); 350 | 351 | let r = &GhsFilter::new("foo/*,!foo/bar,!foo/baz,abc/xyz"); 352 | assert!(!r.is_included("foo/bar", false, false)); 353 | assert!(!r.is_included("foo/baz", false, false)); 354 | assert!(!r.is_included("abc/123", false, false)); 355 | 356 | assert!(r.is_included("foo/123", false, false)); 357 | assert!(r.is_included("foo/123", true, false)); 358 | assert!(r.is_included("foo/123", false, true)); 359 | 360 | assert!(r.is_included("abc/xyz", false, false)); 361 | assert!(r.is_included("abc/xyz", true, false)); 362 | assert!(r.is_included("abc/xyz", false, true)); 363 | } 364 | 365 | #[test] 366 | fn test_filter_meta() { 367 | let r = &GhsFilter::new("*,!fork,!archived,foo/baz"); 368 | assert!(r.exclude_forks); 369 | assert!(r.exclude_archs); 370 | assert!(r.default_all); 371 | 372 | assert!(r.is_included("foo/bar", false, false)); 373 | assert!(!r.is_included("foo/bar", true, false)); 374 | assert!(!r.is_included("foo/bar", false, true)); 375 | 376 | assert!(r.is_included("abc/123", false, false)); 377 | assert!(!r.is_included("abc/123", true, false)); 378 | assert!(!r.is_included("abc/123", false, true)); 379 | 380 | // explicitly added 381 | assert!(r.is_included("foo/baz", false, false)); 382 | assert!(r.is_included("foo/baz", true, false)); 383 | assert!(r.is_included("foo/baz", false, true)); 384 | } 385 | 386 | #[test] 387 | fn test_filter_meta_wildcard() { 388 | let r = &GhsFilter::new("!fork,abc/*,abc/xyz"); 389 | assert!(!r.is_included("abc/123", true, false)); // no wildcard for forks 390 | assert!(r.is_included("abc/xyz", true, false)); // explicitly added 391 | 392 | let r = &GhsFilter::new("!archived,abc/*,abc/xyz"); 393 | assert!(!r.is_included("abc/123", false, true)); // no wildcard for archived 394 | assert!(r.is_included("abc/xyz", false, true)); // explicitly added 395 | } 396 | 397 | #[test] 398 | fn test_issue18() { 399 | // test order of rules not affecting the result 400 | let rules = vec!["foo/*,!foo/bar", "!foo/bar,foo/*"]; 401 | for r in rules { 402 | let r = &GhsFilter::new(r); 403 | assert!(!r.is_included("foo/bar", false, false)); // explicitly excluded 404 | assert!(!r.is_included("abc/abc", false, false)); // not included by default 405 | assert!(r.is_included("foo/baz", false, false)); // wildcard included 406 | } 407 | 408 | let rules = vec!["foo/*,!fork", "!fork,foo/*"]; 409 | for r in rules { 410 | let r = &GhsFilter::new(r); 411 | assert!(r.is_included("foo/bar", false, false)); // wildcard included 412 | assert!(!r.is_included("foo/bar", true, false)); // forks excluded 413 | assert!(!r.is_included("abc/abc", false, false)); // not included by default 414 | } 415 | } 416 | } 417 | -------------------------------------------------------------------------------- /src/db_client.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | 4 | use anyhow::Ok; 5 | use serde::{Deserialize, Serialize}; 6 | use serde_variant::to_variant_name; 7 | use sqlx::{sqlite::SqliteConnectOptions, FromRow, SqlitePool}; 8 | 9 | use crate::gh_client::{PullRequest, Repo, RepoClones, RepoPopularPath, RepoReferrer, RepoViews}; 10 | use crate::types::Res; 11 | 12 | // MARK: Migrations 13 | 14 | async fn migrate_v1(db: &SqlitePool) -> Res { 15 | let mut queries = vec![]; 16 | 17 | let qs = "CREATE TABLE IF NOT EXISTS repos ( 18 | id INTEGER PRIMARY KEY, 19 | name TEXT NOT NULL, 20 | description TEXT DEFAULT NULL, 21 | archived BOOLEAN DEFAULT FALSE 22 | );"; 23 | queries.push(qs); 24 | 25 | let qs = "CREATE TABLE IF NOT EXISTS repo_stats ( 26 | repo_id INTEGER NOT NULL, 27 | date TEXT NOT NULL, 28 | stars INTEGER NOT NULL DEFAULT 0, 29 | forks INTEGER NOT NULL DEFAULT 0, 30 | watchers INTEGER NOT NULL DEFAULT 0, 31 | issues INTEGER NOT NULL DEFAULT 0, 32 | clones_count INTEGER NOT NULL DEFAULT 0, 33 | clones_uniques INTEGER NOT NULL DEFAULT 0, 34 | views_count INTEGER NOT NULL DEFAULT 0, 35 | views_uniques INTEGER NOT NULL DEFAULT 0, 36 | PRIMARY KEY (repo_id, date) 37 | -- FOREIGN KEY (repo_id) REFERENCES repos(id) 38 | );"; 39 | queries.push(qs); 40 | 41 | let qs = "CREATE TABLE IF NOT EXISTS repo_referrers ( 42 | repo_id INTEGER NOT NULL, 43 | date TEXT NOT NULL, 44 | referrer TEXT NOT NULL, 45 | count INTEGER NOT NULL DEFAULT 0, 46 | uniques INTEGER NOT NULL DEFAULT 0, 47 | count_delta INTEGER NOT NULL DEFAULT 0, 48 | uniques_delta INTEGER NOT NULL DEFAULT 0, 49 | PRIMARY KEY (repo_id, date, referrer) 50 | );"; 51 | queries.push(qs); 52 | 53 | let qs = " 54 | CREATE TABLE IF NOT EXISTS repo_popular_paths ( 55 | repo_id INTEGER NOT NULL, 56 | date TEXT NOT NULL, 57 | path TEXT NOT NULL, 58 | title TEXT NOT NULL, 59 | count INTEGER NOT NULL DEFAULT 0, 60 | uniques INTEGER NOT NULL DEFAULT 0, 61 | count_delta INTEGER NOT NULL DEFAULT 0, 62 | uniques_delta INTEGER NOT NULL DEFAULT 0, 63 | PRIMARY KEY (repo_id, date, path) 64 | );"; 65 | queries.push(qs); 66 | 67 | for qs in queries { 68 | let _ = sqlx::query(qs).execute(db).await?; 69 | } 70 | 71 | Ok(()) 72 | } 73 | 74 | async fn migrate_v2(db: &SqlitePool) -> Res { 75 | let queries = vec![ 76 | "ALTER TABLE repos ADD COLUMN stars_synced BOOLEAN DEFAULT FALSE;", 77 | "ALTER TABLE repos ADD COLUMN fork BOOLEAN DEFAULT FALSE;", 78 | // can be deleted or marked as private or user removed from org 79 | // keep in db – but hide from UI and updates 80 | "ALTER TABLE repos ADD COLUMN hidden BOOLEAN DEFAULT FALSE;", 81 | ]; 82 | 83 | for qs in queries { 84 | let _ = sqlx::query(qs).execute(db).await?; 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | async fn migrate_v3(db: &SqlitePool) -> Res { 91 | let queries = vec!["ALTER TABLE repo_stats ADD COLUMN prs INTEGER NOT NULL DEFAULT 0;"]; 92 | 93 | for qs in queries { 94 | let _ = sqlx::query(qs).execute(db).await?; 95 | } 96 | 97 | Ok(()) 98 | } 99 | 100 | async fn migrate<'a>(db: &'a SqlitePool) -> Res { 101 | type BoxFn = Box Fn(&'a SqlitePool) -> Pin + 'a>>>; 102 | let migrations: Vec = vec![ 103 | Box::new(|db| Box::pin(migrate_v1(db))), 104 | Box::new(|db| Box::pin(migrate_v2(db))), 105 | Box::new(|db| Box::pin(migrate_v3(db))), 106 | ]; 107 | 108 | let version: (i32,) = sqlx::query_as("PRAGMA user_version").fetch_one(db).await?; 109 | let version = version.0; 110 | 111 | for (idx, func) in migrations.iter().enumerate() { 112 | let mig_ver = idx as i32 + 1; 113 | if version < mig_ver { 114 | tracing::info!("running migration to v{}", mig_ver); 115 | let _ = func(db).await?; 116 | let qs = format!("PRAGMA user_version = {}", mig_ver); 117 | sqlx::raw_sql(&qs).execute(db).await?; 118 | } 119 | } 120 | 121 | Ok(()) 122 | } 123 | 124 | pub async fn get_db(db_path: &str) -> Res { 125 | let opts = SqliteConnectOptions::new().filename(db_path).create_if_missing(true); 126 | let pool = SqlitePool::connect_with(opts).await?; 127 | migrate(&pool).await?; 128 | Ok(pool) 129 | } 130 | 131 | // MARK: Models 132 | 133 | #[derive(Clone, Debug, Serialize, Deserialize, FromRow)] 134 | pub struct RepoTotals { 135 | pub id: i64, 136 | pub name: String, 137 | pub description: Option, 138 | pub fork: bool, 139 | pub archived: bool, 140 | pub date: String, 141 | pub stars: i32, 142 | pub forks: i32, 143 | pub watchers: i32, 144 | pub issues: i32, 145 | pub prs: i32, 146 | pub clones_count: i32, 147 | pub clones_uniques: i32, 148 | pub views_count: i32, 149 | pub views_uniques: i32, 150 | } 151 | 152 | #[derive(Clone, Debug, Serialize, Deserialize, FromRow)] 153 | pub struct RepoMetrics { 154 | pub date: String, 155 | pub clones_count: i32, 156 | pub clones_uniques: i32, 157 | pub views_count: i32, 158 | pub views_uniques: i32, 159 | } 160 | 161 | #[derive(Clone, Debug, Serialize, Deserialize, FromRow)] 162 | pub struct RepoStars { 163 | pub date: String, 164 | pub stars: i32, 165 | } 166 | 167 | #[derive(Clone, Debug, Serialize, Deserialize, FromRow)] 168 | pub struct RepoPopularItem { 169 | pub name: String, 170 | pub count: i64, 171 | pub uniques: i64, 172 | } 173 | 174 | #[derive(Clone, Debug, Serialize, Deserialize, FromRow)] 175 | pub struct RepoItem { 176 | pub id: i64, 177 | pub name: String, 178 | pub archived: bool, 179 | pub stars_synced: bool, 180 | } 181 | 182 | // MARK: Filters 183 | 184 | pub enum PopularKind { 185 | Refs, 186 | Path, 187 | } 188 | 189 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 190 | #[serde(rename_all = "snake_case")] 191 | pub enum Direction { 192 | Asc, 193 | Desc, 194 | } 195 | 196 | impl Default for Direction { 197 | fn default() -> Self { 198 | Direction::Desc 199 | } 200 | } 201 | 202 | impl std::fmt::Display for Direction { 203 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 204 | write!(f, "{}", to_variant_name(self).unwrap()) 205 | } 206 | } 207 | 208 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 209 | #[serde(rename_all = "snake_case")] 210 | pub enum RepoSort { 211 | Name, 212 | Stars, 213 | Forks, 214 | Watchers, 215 | Issues, 216 | Prs, 217 | #[serde(rename = "clones_count")] 218 | Clones, 219 | #[serde(rename = "views_count")] 220 | Views, 221 | } 222 | 223 | impl Default for RepoSort { 224 | fn default() -> Self { 225 | RepoSort::Views 226 | } 227 | } 228 | 229 | impl std::fmt::Display for RepoSort { 230 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 231 | write!(f, "{}", to_variant_name(self).unwrap()) 232 | } 233 | } 234 | 235 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 236 | #[serde(rename_all = "snake_case")] 237 | pub enum PopularSort { 238 | Name, 239 | Count, 240 | Uniques, 241 | } 242 | 243 | impl Default for PopularSort { 244 | fn default() -> Self { 245 | PopularSort::Uniques 246 | } 247 | } 248 | 249 | impl std::fmt::Display for PopularSort { 250 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 251 | write!(f, "{}", to_variant_name(self).unwrap()) 252 | } 253 | } 254 | 255 | #[derive(Debug, Deserialize, Serialize, Default)] 256 | #[serde(default)] 257 | pub struct RepoFilter { 258 | pub sort: RepoSort, 259 | pub direction: Direction, 260 | } 261 | 262 | #[derive(Debug, Deserialize, Serialize, Default)] 263 | #[serde(default)] 264 | pub struct PopularFilter { 265 | pub sort: PopularSort, 266 | pub direction: Direction, 267 | pub period: i32, 268 | } 269 | 270 | // MARK: DbClient 271 | 272 | const TOTAL_QUERY: &'static str = " 273 | SELECT * FROM repos r 274 | INNER JOIN ( 275 | SELECT 276 | rs.repo_id, 277 | SUM(clones_count) AS clones_count, SUM(clones_uniques) AS clones_uniques, 278 | SUM(views_count) AS views_count, SUM(views_uniques) AS views_uniques, 279 | latest.* 280 | FROM repo_stats rs 281 | INNER JOIN ( 282 | SELECT repo_id, MAX(date) AS date, stars, forks, watchers, issues, prs 283 | FROM repo_stats GROUP BY repo_id 284 | ) latest ON latest.repo_id = rs.repo_id 285 | GROUP BY rs.repo_id 286 | ) rs ON rs.repo_id = r.id 287 | "; 288 | 289 | pub struct DbClient { 290 | db: SqlitePool, 291 | } 292 | 293 | impl DbClient { 294 | pub async fn new(db_path: &str) -> Res { 295 | let db = get_db(db_path).await?; 296 | Ok(Self { db }) 297 | } 298 | 299 | // MARK: Getters 300 | 301 | pub async fn get_repos_ids(&self) -> Res> { 302 | let qs = "SELECT id FROM repos WHERE hidden = FALSE;"; 303 | let items: Vec<(i64,)> = sqlx::query_as(qs).fetch_all(&self.db).await?; 304 | Ok(items.into_iter().map(|x| x.0).collect()) 305 | } 306 | 307 | pub async fn get_repo_totals(&self, repo: &str) -> Res> { 308 | let qs = format!("{} WHERE r.hidden = FALSE AND r.name = $1;", TOTAL_QUERY); 309 | let item = sqlx::query_as(qs.as_str()).bind(repo).fetch_optional(&self.db).await?; 310 | Ok(item) 311 | } 312 | 313 | pub async fn get_metrics(&self, repo: &str) -> Res> { 314 | let qs = " 315 | SELECT * FROM repo_stats rs 316 | INNER JOIN repos r ON r.id = rs.repo_id 317 | WHERE r.hidden = FALSE AND r.name = $1 AND (rs.clones_count > 0 OR rs.views_count > 0) 318 | ORDER BY rs.date ASC; 319 | "; 320 | 321 | let items = sqlx::query_as(qs).bind(repo).fetch_all(&self.db).await?; 322 | Ok(items) 323 | } 324 | 325 | pub async fn get_repos(&self, filter: &RepoFilter) -> Res> { 326 | let qs = format!( 327 | "{} WHERE r.hidden = FALSE ORDER BY {} {}", 328 | TOTAL_QUERY, filter.sort, filter.direction 329 | ); 330 | let items = sqlx::query_as(qs.as_str()).fetch_all(&self.db).await?; 331 | Ok(items) 332 | } 333 | 334 | pub async fn get_stars(&self, repo: &str) -> Res> { 335 | let qs = " 336 | SELECT date, stars FROM repo_stats rs 337 | INNER JOIN repos r ON r.id = rs.repo_id 338 | WHERE r.hidden = FALSE AND r.name = $1 339 | ORDER BY rs.date ASC; 340 | "; 341 | 342 | let mut items: Vec = sqlx::query_as(qs).bind(repo).fetch_all(&self.db).await?; 343 | 344 | // restore gaps in data 345 | let mut prev_stars = 0; 346 | for (idx, item) in items.iter_mut().enumerate() { 347 | if idx == 0 { 348 | continue; 349 | } 350 | 351 | if item.stars == 0 { 352 | item.stars = prev_stars; 353 | } 354 | 355 | prev_stars = item.stars; 356 | } 357 | 358 | // in case when data start to be collected for exist repo with some stats 359 | // view and clone stats can be collected without stars, so remove them 360 | let items = items.into_iter().filter(|x| x.stars > 0).collect(); 361 | Ok(items) 362 | } 363 | 364 | pub async fn get_popular_items( 365 | &self, 366 | repo: &str, 367 | kind: &PopularKind, 368 | filter: &PopularFilter, 369 | ) -> Res> { 370 | let (table, col) = match kind { 371 | PopularKind::Refs => ("repo_referrers", "referrer"), 372 | PopularKind::Path => ("repo_popular_paths", "path"), 373 | }; 374 | 375 | let time_where = match filter.period { 376 | x if x > 0 => format!("date >= date('now', '-{} day')", x), 377 | _ => "1=1".to_string(), 378 | }; 379 | 380 | let order_by = format!("{} {}", filter.sort, filter.direction); 381 | 382 | #[rustfmt::skip] 383 | let qs = format!(" 384 | SELECT {col} as name, SUM(count_delta) AS count, SUM(uniques_delta) AS uniques 385 | FROM {table} rr 386 | INNER JOIN repos r ON r.id = rr.repo_id 387 | WHERE r.hidden = FALSE AND r.name = $1 AND {time_where} 388 | GROUP BY rr.{col} 389 | ORDER BY {order_by}; 390 | "); 391 | 392 | let items = sqlx::query_as(&qs).bind(repo).fetch_all(&self.db).await?; 393 | Ok(items) 394 | } 395 | 396 | pub async fn repos_to_sync(&self) -> Res> { 397 | let qs = "SELECT * FROM repos WHERE stars_synced = FALSE AND hidden = FALSE"; 398 | let items = sqlx::query_as(qs).fetch_all(&self.db).await?; 399 | Ok(items) 400 | } 401 | 402 | // MARK: Inserters 403 | 404 | pub async fn insert_repo(&self, repo: &Repo) -> Res { 405 | let qs = " 406 | INSERT INTO repos (id, name, description, archived, fork) 407 | VALUES ($1, $2, $3, $4, $5) 408 | ON CONFLICT(id) DO UPDATE SET 409 | name = excluded.name, 410 | description = excluded.description, 411 | archived = excluded.archived, 412 | fork = excluded.fork, 413 | hidden = FALSE; -- reset hidden flag if repo was hidden and appeared again 414 | "; 415 | 416 | let _ = sqlx::query(qs) 417 | .bind(repo.id as i64) 418 | .bind(&repo.full_name) 419 | .bind(&repo.description) 420 | .bind(repo.archived) 421 | .bind(repo.fork) 422 | .execute(&self.db) 423 | .await?; 424 | 425 | Ok(()) 426 | } 427 | 428 | pub async fn insert_stats(&self, repo: &Repo, date: &str, prs: &Vec) -> Res { 429 | let qs = " 430 | INSERT INTO repo_stats AS t (repo_id, date, stars, forks, watchers, issues, prs) 431 | VALUES ($1, $2, $3, $4, $5, $6, $7) 432 | ON CONFLICT(repo_id, date) DO UPDATE SET 433 | stars = MAX(t.stars, excluded.stars), 434 | forks = MAX(t.forks, excluded.forks), 435 | watchers = MAX(t.watchers, excluded.watchers), 436 | issues = MAX(t.issues, excluded.issues), 437 | prs = MAX(t.prs, excluded.prs); 438 | "; 439 | 440 | let _ = sqlx::query(qs) 441 | .bind(repo.id as i64) 442 | .bind(&date) 443 | .bind(repo.stargazers_count as i32) 444 | .bind(repo.forks_count as i32) 445 | .bind(repo.watchers_count as i32) 446 | .bind(repo.open_issues_count as i32 - prs.len() as i32) 447 | .bind(prs.len() as i32) 448 | .execute(&self.db) 449 | .await?; 450 | 451 | Ok(()) 452 | } 453 | 454 | pub async fn insert_stars(&self, repo_id: i64, stars: &Vec<(String, u32, u32)>) -> Res { 455 | let qs = " 456 | INSERT INTO repo_stats AS t (repo_id, date, stars) 457 | VALUES ((SELECT id FROM repos WHERE id = $1), $2, $3) 458 | ON CONFLICT(repo_id, date) DO UPDATE SET 459 | stars = MAX(t.stars, excluded.stars); 460 | "; 461 | 462 | for (date, acc_count, _) in stars { 463 | let _ = sqlx::query(qs) 464 | .bind(repo_id) 465 | .bind(&date) 466 | .bind(acc_count.clone() as i32) 467 | .execute(&self.db) 468 | .await?; 469 | } 470 | 471 | Ok(()) 472 | } 473 | 474 | pub async fn insert_clones(&self, repo: &Repo, clones: &RepoClones) -> Res { 475 | let qs = " 476 | INSERT INTO repo_stats AS t (repo_id, date, clones_count, clones_uniques) 477 | VALUES ($1, $2, $3, $4) 478 | ON CONFLICT(repo_id, date) DO UPDATE SET 479 | clones_count = MAX(t.clones_count, excluded.clones_count), 480 | clones_uniques = MAX(t.clones_uniques, excluded.clones_uniques); 481 | "; 482 | 483 | for doc in &clones.clones { 484 | let _ = sqlx::query(qs) 485 | .bind(repo.id as i64) 486 | .bind(&doc.timestamp) 487 | .bind(doc.count as i32) 488 | .bind(doc.uniques as i32) 489 | .execute(&self.db) 490 | .await?; 491 | } 492 | 493 | Ok(()) 494 | } 495 | 496 | pub async fn insert_views(&self, repo: &Repo, views: &RepoViews) -> Res { 497 | let qs = " 498 | INSERT INTO repo_stats AS t (repo_id, date, views_count, views_uniques) 499 | VALUES ($1, $2, $3, $4) 500 | ON CONFLICT(repo_id, date) DO UPDATE SET 501 | views_count = MAX(t.views_count, excluded.views_count), 502 | views_uniques = MAX(t.views_uniques, excluded.views_uniques); 503 | "; 504 | 505 | for doc in &views.views { 506 | let _ = sqlx::query(qs) 507 | .bind(repo.id as i64) 508 | .bind(&doc.timestamp) 509 | .bind(doc.count as i32) 510 | .bind(doc.uniques as i32) 511 | .execute(&self.db) 512 | .await?; 513 | } 514 | 515 | Ok(()) 516 | } 517 | 518 | pub async fn insert_referrers(&self, repo: &Repo, date: &str, docs: &Vec) -> Res { 519 | let qs = " 520 | INSERT INTO repo_referrers AS t (repo_id, date, referrer, count, uniques) 521 | VALUES ($1, $2, $3, $4, $5) 522 | ON CONFLICT(repo_id, date, referrer) DO UPDATE SET 523 | count = MAX(t.count, excluded.count), 524 | uniques = MAX(t.uniques, excluded.uniques); 525 | "; 526 | 527 | for rec in docs { 528 | let _ = sqlx::query(qs) 529 | .bind(repo.id as i64) 530 | .bind(&date) 531 | .bind(&rec.referrer) 532 | .bind(rec.count as i32) 533 | .bind(rec.uniques as i32) 534 | .execute(&self.db) 535 | .await?; 536 | } 537 | 538 | Ok(()) 539 | } 540 | 541 | pub async fn insert_paths(&self, repo: &Repo, date: &str, docs: &Vec) -> Res { 542 | let qs = " 543 | INSERT INTO repo_popular_paths AS t (repo_id, date, path, title, count, uniques) 544 | VALUES ($1, $2, $3, $4, $5, $6) 545 | ON CONFLICT(repo_id, date, path) DO UPDATE SET 546 | count = MAX(t.count, excluded.count), 547 | uniques = MAX(t.uniques, excluded.uniques); 548 | "; 549 | 550 | for rec in docs { 551 | let _ = sqlx::query(qs) 552 | .bind(repo.id as i64) 553 | .bind(&date) 554 | .bind(&rec.path) 555 | .bind(&rec.title) 556 | .bind(rec.count as i32) 557 | .bind(rec.uniques as i32) 558 | .execute(&self.db) 559 | .await?; 560 | } 561 | 562 | Ok(()) 563 | } 564 | 565 | // MARK: Updater 566 | 567 | pub async fn update_deltas(&self) -> Res { 568 | let items = [("repo_referrers", "referrer"), ("repo_popular_paths", "path")]; 569 | 570 | for (table, col) in items { 571 | #[rustfmt::skip] 572 | let qs = format!(" 573 | WITH cte AS ( 574 | SELECT 575 | rr.repo_id, rr.date, rr.{col}, rr.uniques, rr.count, 576 | LAG(rr.uniques) OVER (PARTITION BY rr.repo_id, rr.{col} ORDER BY rr.date) AS prev_uniques, 577 | LAG(rr.count) OVER (PARTITION BY rr.repo_id, rr.{col} ORDER BY rr.date) AS prev_count 578 | FROM {table} rr 579 | ) 580 | UPDATE {table} AS rr SET 581 | uniques_delta = MAX(0, cte.uniques - COALESCE(cte.prev_uniques, 0)), 582 | count_delta = MAX(0, cte.count - COALESCE(cte.prev_count, 0)) 583 | FROM cte 584 | WHERE rr.repo_id = cte.repo_id AND rr.date = cte.date AND rr.{col} = cte.{col}; 585 | "); 586 | 587 | let _ = sqlx::query(qs.as_str()).execute(&self.db).await?; 588 | } 589 | 590 | Ok(()) 591 | } 592 | 593 | pub async fn mark_repo_hidden(&self, repos_ids: &Vec) -> Res { 594 | let ids = repos_ids.iter().map(|x| x.to_string()).collect::>().join(","); 595 | let qs = format!("UPDATE repos SET hidden = TRUE WHERE id IN ({});", ids); 596 | let _ = sqlx::query(&qs).execute(&self.db).await?; 597 | Ok(()) 598 | } 599 | 600 | pub async fn mark_repo_stars_synced(&self, repo_id: i64) -> Res { 601 | let qs = "UPDATE repos SET stars_synced = TRUE WHERE id = $1;"; 602 | let _ = sqlx::query(qs).bind(repo_id).execute(&self.db).await?; 603 | Ok(()) 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "ahash" 22 | version = "0.8.11" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 25 | dependencies = [ 26 | "cfg-if", 27 | "once_cell", 28 | "version_check", 29 | "zerocopy", 30 | ] 31 | 32 | [[package]] 33 | name = "aho-corasick" 34 | version = "1.1.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 37 | dependencies = [ 38 | "memchr", 39 | ] 40 | 41 | [[package]] 42 | name = "allocator-api2" 43 | version = "0.2.18" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 46 | 47 | [[package]] 48 | name = "android-tzdata" 49 | version = "0.1.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 52 | 53 | [[package]] 54 | name = "android_system_properties" 55 | version = "0.1.5" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 58 | dependencies = [ 59 | "libc", 60 | ] 61 | 62 | [[package]] 63 | name = "anyhow" 64 | version = "1.0.94" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" 67 | 68 | [[package]] 69 | name = "async-trait" 70 | version = "0.1.83" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 73 | dependencies = [ 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "atoi" 81 | version = "2.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" 84 | dependencies = [ 85 | "num-traits", 86 | ] 87 | 88 | [[package]] 89 | name = "autocfg" 90 | version = "1.4.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 93 | 94 | [[package]] 95 | name = "axum" 96 | version = "0.7.9" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" 99 | dependencies = [ 100 | "async-trait", 101 | "axum-core", 102 | "bytes", 103 | "futures-util", 104 | "http", 105 | "http-body", 106 | "http-body-util", 107 | "hyper", 108 | "hyper-util", 109 | "itoa", 110 | "matchit", 111 | "memchr", 112 | "mime", 113 | "percent-encoding", 114 | "pin-project-lite", 115 | "rustversion", 116 | "serde", 117 | "serde_json", 118 | "serde_path_to_error", 119 | "serde_urlencoded", 120 | "sync_wrapper 1.0.1", 121 | "tokio", 122 | "tower", 123 | "tower-layer", 124 | "tower-service", 125 | "tracing", 126 | ] 127 | 128 | [[package]] 129 | name = "axum-core" 130 | version = "0.4.5" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" 133 | dependencies = [ 134 | "async-trait", 135 | "bytes", 136 | "futures-util", 137 | "http", 138 | "http-body", 139 | "http-body-util", 140 | "mime", 141 | "pin-project-lite", 142 | "rustversion", 143 | "sync_wrapper 1.0.1", 144 | "tower-layer", 145 | "tower-service", 146 | "tracing", 147 | ] 148 | 149 | [[package]] 150 | name = "backtrace" 151 | version = "0.3.74" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 154 | dependencies = [ 155 | "addr2line", 156 | "cfg-if", 157 | "libc", 158 | "miniz_oxide", 159 | "object", 160 | "rustc-demangle", 161 | "windows-targets 0.52.6", 162 | ] 163 | 164 | [[package]] 165 | name = "base64" 166 | version = "0.22.1" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 169 | 170 | [[package]] 171 | name = "base64ct" 172 | version = "1.6.0" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 175 | 176 | [[package]] 177 | name = "bitflags" 178 | version = "2.6.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 181 | dependencies = [ 182 | "serde", 183 | ] 184 | 185 | [[package]] 186 | name = "block-buffer" 187 | version = "0.10.4" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 190 | dependencies = [ 191 | "generic-array", 192 | ] 193 | 194 | [[package]] 195 | name = "bumpalo" 196 | version = "3.16.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 199 | 200 | [[package]] 201 | name = "byteorder" 202 | version = "1.5.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 205 | 206 | [[package]] 207 | name = "bytes" 208 | version = "1.7.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" 211 | 212 | [[package]] 213 | name = "cc" 214 | version = "1.1.24" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" 217 | dependencies = [ 218 | "shlex", 219 | ] 220 | 221 | [[package]] 222 | name = "cfg-if" 223 | version = "1.0.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 226 | 227 | [[package]] 228 | name = "chrono" 229 | version = "0.4.39" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 232 | dependencies = [ 233 | "android-tzdata", 234 | "iana-time-zone", 235 | "js-sys", 236 | "num-traits", 237 | "serde", 238 | "wasm-bindgen", 239 | "windows-targets 0.52.6", 240 | ] 241 | 242 | [[package]] 243 | name = "concurrent-queue" 244 | version = "2.5.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 247 | dependencies = [ 248 | "crossbeam-utils", 249 | ] 250 | 251 | [[package]] 252 | name = "const-oid" 253 | version = "0.9.6" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 256 | 257 | [[package]] 258 | name = "core-foundation-sys" 259 | version = "0.8.7" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 262 | 263 | [[package]] 264 | name = "cpufeatures" 265 | version = "0.2.14" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 268 | dependencies = [ 269 | "libc", 270 | ] 271 | 272 | [[package]] 273 | name = "crc" 274 | version = "3.2.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 277 | dependencies = [ 278 | "crc-catalog", 279 | ] 280 | 281 | [[package]] 282 | name = "crc-catalog" 283 | version = "2.4.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 286 | 287 | [[package]] 288 | name = "croner" 289 | version = "2.0.6" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "09b42c74d1c9fc4680c245ca7287bd631fe73eb364657268b0e65bafdec83d47" 292 | dependencies = [ 293 | "chrono", 294 | ] 295 | 296 | [[package]] 297 | name = "crossbeam-queue" 298 | version = "0.3.11" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" 301 | dependencies = [ 302 | "crossbeam-utils", 303 | ] 304 | 305 | [[package]] 306 | name = "crossbeam-utils" 307 | version = "0.8.20" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 310 | 311 | [[package]] 312 | name = "crypto-common" 313 | version = "0.1.6" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 316 | dependencies = [ 317 | "generic-array", 318 | "typenum", 319 | ] 320 | 321 | [[package]] 322 | name = "der" 323 | version = "0.7.9" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 326 | dependencies = [ 327 | "const-oid", 328 | "pem-rfc7468", 329 | "zeroize", 330 | ] 331 | 332 | [[package]] 333 | name = "deranged" 334 | version = "0.3.11" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 337 | dependencies = [ 338 | "powerfmt", 339 | ] 340 | 341 | [[package]] 342 | name = "digest" 343 | version = "0.10.7" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 346 | dependencies = [ 347 | "block-buffer", 348 | "const-oid", 349 | "crypto-common", 350 | "subtle", 351 | ] 352 | 353 | [[package]] 354 | name = "dotenvy" 355 | version = "0.15.7" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" 358 | 359 | [[package]] 360 | name = "either" 361 | version = "1.13.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 364 | dependencies = [ 365 | "serde", 366 | ] 367 | 368 | [[package]] 369 | name = "equivalent" 370 | version = "1.0.1" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 373 | 374 | [[package]] 375 | name = "errno" 376 | version = "0.3.9" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 379 | dependencies = [ 380 | "libc", 381 | "windows-sys 0.52.0", 382 | ] 383 | 384 | [[package]] 385 | name = "etcetera" 386 | version = "0.8.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" 389 | dependencies = [ 390 | "cfg-if", 391 | "home", 392 | "windows-sys 0.48.0", 393 | ] 394 | 395 | [[package]] 396 | name = "event-listener" 397 | version = "5.3.1" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" 400 | dependencies = [ 401 | "concurrent-queue", 402 | "parking", 403 | "pin-project-lite", 404 | ] 405 | 406 | [[package]] 407 | name = "fastrand" 408 | version = "2.1.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 411 | 412 | [[package]] 413 | name = "flume" 414 | version = "0.11.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" 417 | dependencies = [ 418 | "futures-core", 419 | "futures-sink", 420 | "spin", 421 | ] 422 | 423 | [[package]] 424 | name = "fnv" 425 | version = "1.0.7" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 428 | 429 | [[package]] 430 | name = "form_urlencoded" 431 | version = "1.2.1" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 434 | dependencies = [ 435 | "percent-encoding", 436 | ] 437 | 438 | [[package]] 439 | name = "futures-channel" 440 | version = "0.3.30" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 443 | dependencies = [ 444 | "futures-core", 445 | "futures-sink", 446 | ] 447 | 448 | [[package]] 449 | name = "futures-core" 450 | version = "0.3.30" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 453 | 454 | [[package]] 455 | name = "futures-executor" 456 | version = "0.3.30" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 459 | dependencies = [ 460 | "futures-core", 461 | "futures-task", 462 | "futures-util", 463 | ] 464 | 465 | [[package]] 466 | name = "futures-intrusive" 467 | version = "0.5.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" 470 | dependencies = [ 471 | "futures-core", 472 | "lock_api", 473 | "parking_lot", 474 | ] 475 | 476 | [[package]] 477 | name = "futures-io" 478 | version = "0.3.30" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 481 | 482 | [[package]] 483 | name = "futures-sink" 484 | version = "0.3.30" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 487 | 488 | [[package]] 489 | name = "futures-task" 490 | version = "0.3.30" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 493 | 494 | [[package]] 495 | name = "futures-util" 496 | version = "0.3.30" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 499 | dependencies = [ 500 | "futures-core", 501 | "futures-io", 502 | "futures-sink", 503 | "futures-task", 504 | "memchr", 505 | "pin-project-lite", 506 | "pin-utils", 507 | "slab", 508 | ] 509 | 510 | [[package]] 511 | name = "generic-array" 512 | version = "0.14.7" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 515 | dependencies = [ 516 | "typenum", 517 | "version_check", 518 | ] 519 | 520 | [[package]] 521 | name = "getrandom" 522 | version = "0.2.15" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 525 | dependencies = [ 526 | "cfg-if", 527 | "libc", 528 | "wasi", 529 | ] 530 | 531 | [[package]] 532 | name = "ghstats" 533 | version = "0.7.1" 534 | dependencies = [ 535 | "anyhow", 536 | "axum", 537 | "chrono", 538 | "dotenvy", 539 | "maud", 540 | "reqwest", 541 | "serde", 542 | "serde_json", 543 | "serde_variant", 544 | "sqlx", 545 | "thousands", 546 | "tokio", 547 | "tokio-cron-scheduler", 548 | "tower-http", 549 | "tracing", 550 | "tracing-logfmt", 551 | "tracing-subscriber", 552 | ] 553 | 554 | [[package]] 555 | name = "gimli" 556 | version = "0.31.0" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" 559 | 560 | [[package]] 561 | name = "hashbrown" 562 | version = "0.14.5" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 565 | dependencies = [ 566 | "ahash", 567 | "allocator-api2", 568 | ] 569 | 570 | [[package]] 571 | name = "hashbrown" 572 | version = "0.15.0" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 575 | 576 | [[package]] 577 | name = "hashlink" 578 | version = "0.9.1" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" 581 | dependencies = [ 582 | "hashbrown 0.14.5", 583 | ] 584 | 585 | [[package]] 586 | name = "heck" 587 | version = "0.5.0" 588 | source = "registry+https://github.com/rust-lang/crates.io-index" 589 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 590 | 591 | [[package]] 592 | name = "hermit-abi" 593 | version = "0.3.9" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 596 | 597 | [[package]] 598 | name = "hex" 599 | version = "0.4.3" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 602 | 603 | [[package]] 604 | name = "hkdf" 605 | version = "0.12.4" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" 608 | dependencies = [ 609 | "hmac", 610 | ] 611 | 612 | [[package]] 613 | name = "hmac" 614 | version = "0.12.1" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 617 | dependencies = [ 618 | "digest", 619 | ] 620 | 621 | [[package]] 622 | name = "home" 623 | version = "0.5.9" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 626 | dependencies = [ 627 | "windows-sys 0.52.0", 628 | ] 629 | 630 | [[package]] 631 | name = "http" 632 | version = "1.1.0" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 635 | dependencies = [ 636 | "bytes", 637 | "fnv", 638 | "itoa", 639 | ] 640 | 641 | [[package]] 642 | name = "http-body" 643 | version = "1.0.1" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 646 | dependencies = [ 647 | "bytes", 648 | "http", 649 | ] 650 | 651 | [[package]] 652 | name = "http-body-util" 653 | version = "0.1.2" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 656 | dependencies = [ 657 | "bytes", 658 | "futures-util", 659 | "http", 660 | "http-body", 661 | "pin-project-lite", 662 | ] 663 | 664 | [[package]] 665 | name = "httparse" 666 | version = "1.9.5" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 669 | 670 | [[package]] 671 | name = "httpdate" 672 | version = "1.0.3" 673 | source = "registry+https://github.com/rust-lang/crates.io-index" 674 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 675 | 676 | [[package]] 677 | name = "hyper" 678 | version = "1.4.1" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" 681 | dependencies = [ 682 | "bytes", 683 | "futures-channel", 684 | "futures-util", 685 | "http", 686 | "http-body", 687 | "httparse", 688 | "httpdate", 689 | "itoa", 690 | "pin-project-lite", 691 | "smallvec", 692 | "tokio", 693 | "want", 694 | ] 695 | 696 | [[package]] 697 | name = "hyper-rustls" 698 | version = "0.27.3" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" 701 | dependencies = [ 702 | "futures-util", 703 | "http", 704 | "hyper", 705 | "hyper-util", 706 | "rustls", 707 | "rustls-pki-types", 708 | "tokio", 709 | "tokio-rustls", 710 | "tower-service", 711 | "webpki-roots", 712 | ] 713 | 714 | [[package]] 715 | name = "hyper-util" 716 | version = "0.1.9" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" 719 | dependencies = [ 720 | "bytes", 721 | "futures-channel", 722 | "futures-util", 723 | "http", 724 | "http-body", 725 | "hyper", 726 | "pin-project-lite", 727 | "socket2", 728 | "tokio", 729 | "tower-service", 730 | "tracing", 731 | ] 732 | 733 | [[package]] 734 | name = "iana-time-zone" 735 | version = "0.1.61" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 738 | dependencies = [ 739 | "android_system_properties", 740 | "core-foundation-sys", 741 | "iana-time-zone-haiku", 742 | "js-sys", 743 | "wasm-bindgen", 744 | "windows-core", 745 | ] 746 | 747 | [[package]] 748 | name = "iana-time-zone-haiku" 749 | version = "0.1.2" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 752 | dependencies = [ 753 | "cc", 754 | ] 755 | 756 | [[package]] 757 | name = "idna" 758 | version = "0.5.0" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 761 | dependencies = [ 762 | "unicode-bidi", 763 | "unicode-normalization", 764 | ] 765 | 766 | [[package]] 767 | name = "indexmap" 768 | version = "2.6.0" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 771 | dependencies = [ 772 | "equivalent", 773 | "hashbrown 0.15.0", 774 | ] 775 | 776 | [[package]] 777 | name = "ipnet" 778 | version = "2.10.1" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" 781 | 782 | [[package]] 783 | name = "itoa" 784 | version = "1.0.11" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 787 | 788 | [[package]] 789 | name = "js-sys" 790 | version = "0.3.70" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" 793 | dependencies = [ 794 | "wasm-bindgen", 795 | ] 796 | 797 | [[package]] 798 | name = "lazy_static" 799 | version = "1.5.0" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 802 | dependencies = [ 803 | "spin", 804 | ] 805 | 806 | [[package]] 807 | name = "libc" 808 | version = "0.2.159" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" 811 | 812 | [[package]] 813 | name = "libm" 814 | version = "0.2.8" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 817 | 818 | [[package]] 819 | name = "libsqlite3-sys" 820 | version = "0.30.1" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" 823 | dependencies = [ 824 | "cc", 825 | "pkg-config", 826 | "vcpkg", 827 | ] 828 | 829 | [[package]] 830 | name = "linux-raw-sys" 831 | version = "0.4.14" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 834 | 835 | [[package]] 836 | name = "lock_api" 837 | version = "0.4.12" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 840 | dependencies = [ 841 | "autocfg", 842 | "scopeguard", 843 | ] 844 | 845 | [[package]] 846 | name = "log" 847 | version = "0.4.22" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 850 | 851 | [[package]] 852 | name = "matchers" 853 | version = "0.1.0" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 856 | dependencies = [ 857 | "regex-automata 0.1.10", 858 | ] 859 | 860 | [[package]] 861 | name = "matchit" 862 | version = "0.7.3" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" 865 | 866 | [[package]] 867 | name = "maud" 868 | version = "0.26.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa" 871 | dependencies = [ 872 | "axum-core", 873 | "http", 874 | "itoa", 875 | "maud_macros", 876 | ] 877 | 878 | [[package]] 879 | name = "maud_macros" 880 | version = "0.26.0" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18" 883 | dependencies = [ 884 | "proc-macro-error", 885 | "proc-macro2", 886 | "quote", 887 | "syn", 888 | ] 889 | 890 | [[package]] 891 | name = "md-5" 892 | version = "0.10.6" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" 895 | dependencies = [ 896 | "cfg-if", 897 | "digest", 898 | ] 899 | 900 | [[package]] 901 | name = "memchr" 902 | version = "2.7.4" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 905 | 906 | [[package]] 907 | name = "mime" 908 | version = "0.3.17" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 911 | 912 | [[package]] 913 | name = "minimal-lexical" 914 | version = "0.2.1" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 917 | 918 | [[package]] 919 | name = "miniz_oxide" 920 | version = "0.8.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 923 | dependencies = [ 924 | "adler2", 925 | ] 926 | 927 | [[package]] 928 | name = "mio" 929 | version = "1.0.2" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 932 | dependencies = [ 933 | "hermit-abi", 934 | "libc", 935 | "wasi", 936 | "windows-sys 0.52.0", 937 | ] 938 | 939 | [[package]] 940 | name = "nom" 941 | version = "7.1.3" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 944 | dependencies = [ 945 | "memchr", 946 | "minimal-lexical", 947 | ] 948 | 949 | [[package]] 950 | name = "nu-ansi-term" 951 | version = "0.46.0" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 954 | dependencies = [ 955 | "overload", 956 | "winapi", 957 | ] 958 | 959 | [[package]] 960 | name = "nu-ansi-term" 961 | version = "0.50.1" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" 964 | dependencies = [ 965 | "windows-sys 0.52.0", 966 | ] 967 | 968 | [[package]] 969 | name = "num-bigint-dig" 970 | version = "0.8.4" 971 | source = "registry+https://github.com/rust-lang/crates.io-index" 972 | checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 973 | dependencies = [ 974 | "byteorder", 975 | "lazy_static", 976 | "libm", 977 | "num-integer", 978 | "num-iter", 979 | "num-traits", 980 | "rand", 981 | "smallvec", 982 | "zeroize", 983 | ] 984 | 985 | [[package]] 986 | name = "num-conv" 987 | version = "0.1.0" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 990 | 991 | [[package]] 992 | name = "num-derive" 993 | version = "0.4.2" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 996 | dependencies = [ 997 | "proc-macro2", 998 | "quote", 999 | "syn", 1000 | ] 1001 | 1002 | [[package]] 1003 | name = "num-integer" 1004 | version = "0.1.46" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 1007 | dependencies = [ 1008 | "num-traits", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "num-iter" 1013 | version = "0.1.45" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 1016 | dependencies = [ 1017 | "autocfg", 1018 | "num-integer", 1019 | "num-traits", 1020 | ] 1021 | 1022 | [[package]] 1023 | name = "num-traits" 1024 | version = "0.2.19" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1027 | dependencies = [ 1028 | "autocfg", 1029 | "libm", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "object" 1034 | version = "0.36.4" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 1037 | dependencies = [ 1038 | "memchr", 1039 | ] 1040 | 1041 | [[package]] 1042 | name = "once_cell" 1043 | version = "1.20.1" 1044 | source = "registry+https://github.com/rust-lang/crates.io-index" 1045 | checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" 1046 | dependencies = [ 1047 | "portable-atomic", 1048 | ] 1049 | 1050 | [[package]] 1051 | name = "overload" 1052 | version = "0.1.1" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1055 | 1056 | [[package]] 1057 | name = "parking" 1058 | version = "2.2.1" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 1061 | 1062 | [[package]] 1063 | name = "parking_lot" 1064 | version = "0.12.3" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1067 | dependencies = [ 1068 | "lock_api", 1069 | "parking_lot_core", 1070 | ] 1071 | 1072 | [[package]] 1073 | name = "parking_lot_core" 1074 | version = "0.9.10" 1075 | source = "registry+https://github.com/rust-lang/crates.io-index" 1076 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1077 | dependencies = [ 1078 | "cfg-if", 1079 | "libc", 1080 | "redox_syscall", 1081 | "smallvec", 1082 | "windows-targets 0.52.6", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "paste" 1087 | version = "1.0.15" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1090 | 1091 | [[package]] 1092 | name = "pem-rfc7468" 1093 | version = "0.7.0" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 1096 | dependencies = [ 1097 | "base64ct", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "percent-encoding" 1102 | version = "2.3.1" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1105 | 1106 | [[package]] 1107 | name = "pin-project-lite" 1108 | version = "0.2.14" 1109 | source = "registry+https://github.com/rust-lang/crates.io-index" 1110 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 1111 | 1112 | [[package]] 1113 | name = "pin-utils" 1114 | version = "0.1.0" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1117 | 1118 | [[package]] 1119 | name = "pkcs1" 1120 | version = "0.7.5" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 1123 | dependencies = [ 1124 | "der", 1125 | "pkcs8", 1126 | "spki", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "pkcs8" 1131 | version = "0.10.2" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 1134 | dependencies = [ 1135 | "der", 1136 | "spki", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "pkg-config" 1141 | version = "0.3.31" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1144 | 1145 | [[package]] 1146 | name = "portable-atomic" 1147 | version = "1.9.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 1150 | 1151 | [[package]] 1152 | name = "powerfmt" 1153 | version = "0.2.0" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1156 | 1157 | [[package]] 1158 | name = "ppv-lite86" 1159 | version = "0.2.20" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1162 | dependencies = [ 1163 | "zerocopy", 1164 | ] 1165 | 1166 | [[package]] 1167 | name = "proc-macro-error" 1168 | version = "1.0.4" 1169 | source = "registry+https://github.com/rust-lang/crates.io-index" 1170 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1171 | dependencies = [ 1172 | "proc-macro-error-attr", 1173 | "proc-macro2", 1174 | "quote", 1175 | "version_check", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "proc-macro-error-attr" 1180 | version = "1.0.4" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1183 | dependencies = [ 1184 | "proc-macro2", 1185 | "quote", 1186 | "version_check", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "proc-macro2" 1191 | version = "1.0.92" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 1194 | dependencies = [ 1195 | "unicode-ident", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "quinn" 1200 | version = "0.11.5" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" 1203 | dependencies = [ 1204 | "bytes", 1205 | "pin-project-lite", 1206 | "quinn-proto", 1207 | "quinn-udp", 1208 | "rustc-hash", 1209 | "rustls", 1210 | "socket2", 1211 | "thiserror", 1212 | "tokio", 1213 | "tracing", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "quinn-proto" 1218 | version = "0.11.8" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" 1221 | dependencies = [ 1222 | "bytes", 1223 | "rand", 1224 | "ring", 1225 | "rustc-hash", 1226 | "rustls", 1227 | "slab", 1228 | "thiserror", 1229 | "tinyvec", 1230 | "tracing", 1231 | ] 1232 | 1233 | [[package]] 1234 | name = "quinn-udp" 1235 | version = "0.5.5" 1236 | source = "registry+https://github.com/rust-lang/crates.io-index" 1237 | checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" 1238 | dependencies = [ 1239 | "libc", 1240 | "once_cell", 1241 | "socket2", 1242 | "tracing", 1243 | "windows-sys 0.59.0", 1244 | ] 1245 | 1246 | [[package]] 1247 | name = "quote" 1248 | version = "1.0.37" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 1251 | dependencies = [ 1252 | "proc-macro2", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "rand" 1257 | version = "0.8.5" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1260 | dependencies = [ 1261 | "libc", 1262 | "rand_chacha", 1263 | "rand_core", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "rand_chacha" 1268 | version = "0.3.1" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1271 | dependencies = [ 1272 | "ppv-lite86", 1273 | "rand_core", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "rand_core" 1278 | version = "0.6.4" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1281 | dependencies = [ 1282 | "getrandom", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "redox_syscall" 1287 | version = "0.5.7" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 1290 | dependencies = [ 1291 | "bitflags", 1292 | ] 1293 | 1294 | [[package]] 1295 | name = "regex" 1296 | version = "1.11.1" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1299 | dependencies = [ 1300 | "aho-corasick", 1301 | "memchr", 1302 | "regex-automata 0.4.9", 1303 | "regex-syntax 0.8.5", 1304 | ] 1305 | 1306 | [[package]] 1307 | name = "regex-automata" 1308 | version = "0.1.10" 1309 | source = "registry+https://github.com/rust-lang/crates.io-index" 1310 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1311 | dependencies = [ 1312 | "regex-syntax 0.6.29", 1313 | ] 1314 | 1315 | [[package]] 1316 | name = "regex-automata" 1317 | version = "0.4.9" 1318 | source = "registry+https://github.com/rust-lang/crates.io-index" 1319 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1320 | dependencies = [ 1321 | "aho-corasick", 1322 | "memchr", 1323 | "regex-syntax 0.8.5", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "regex-syntax" 1328 | version = "0.6.29" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 1331 | 1332 | [[package]] 1333 | name = "regex-syntax" 1334 | version = "0.8.5" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1337 | 1338 | [[package]] 1339 | name = "reqwest" 1340 | version = "0.12.9" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" 1343 | dependencies = [ 1344 | "base64", 1345 | "bytes", 1346 | "futures-core", 1347 | "futures-util", 1348 | "http", 1349 | "http-body", 1350 | "http-body-util", 1351 | "hyper", 1352 | "hyper-rustls", 1353 | "hyper-util", 1354 | "ipnet", 1355 | "js-sys", 1356 | "log", 1357 | "mime", 1358 | "once_cell", 1359 | "percent-encoding", 1360 | "pin-project-lite", 1361 | "quinn", 1362 | "rustls", 1363 | "rustls-pemfile", 1364 | "rustls-pki-types", 1365 | "serde", 1366 | "serde_json", 1367 | "serde_urlencoded", 1368 | "sync_wrapper 1.0.1", 1369 | "tokio", 1370 | "tokio-rustls", 1371 | "tower-service", 1372 | "url", 1373 | "wasm-bindgen", 1374 | "wasm-bindgen-futures", 1375 | "web-sys", 1376 | "webpki-roots", 1377 | "windows-registry", 1378 | ] 1379 | 1380 | [[package]] 1381 | name = "ring" 1382 | version = "0.17.8" 1383 | source = "registry+https://github.com/rust-lang/crates.io-index" 1384 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1385 | dependencies = [ 1386 | "cc", 1387 | "cfg-if", 1388 | "getrandom", 1389 | "libc", 1390 | "spin", 1391 | "untrusted", 1392 | "windows-sys 0.52.0", 1393 | ] 1394 | 1395 | [[package]] 1396 | name = "rsa" 1397 | version = "0.9.6" 1398 | source = "registry+https://github.com/rust-lang/crates.io-index" 1399 | checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" 1400 | dependencies = [ 1401 | "const-oid", 1402 | "digest", 1403 | "num-bigint-dig", 1404 | "num-integer", 1405 | "num-traits", 1406 | "pkcs1", 1407 | "pkcs8", 1408 | "rand_core", 1409 | "signature", 1410 | "spki", 1411 | "subtle", 1412 | "zeroize", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "rustc-demangle" 1417 | version = "0.1.24" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1420 | 1421 | [[package]] 1422 | name = "rustc-hash" 1423 | version = "2.0.0" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" 1426 | 1427 | [[package]] 1428 | name = "rustix" 1429 | version = "0.38.37" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" 1432 | dependencies = [ 1433 | "bitflags", 1434 | "errno", 1435 | "libc", 1436 | "linux-raw-sys", 1437 | "windows-sys 0.52.0", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "rustls" 1442 | version = "0.23.13" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" 1445 | dependencies = [ 1446 | "once_cell", 1447 | "ring", 1448 | "rustls-pki-types", 1449 | "rustls-webpki", 1450 | "subtle", 1451 | "zeroize", 1452 | ] 1453 | 1454 | [[package]] 1455 | name = "rustls-pemfile" 1456 | version = "2.2.0" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1459 | dependencies = [ 1460 | "rustls-pki-types", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "rustls-pki-types" 1465 | version = "1.9.0" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" 1468 | 1469 | [[package]] 1470 | name = "rustls-webpki" 1471 | version = "0.102.8" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1474 | dependencies = [ 1475 | "ring", 1476 | "rustls-pki-types", 1477 | "untrusted", 1478 | ] 1479 | 1480 | [[package]] 1481 | name = "rustversion" 1482 | version = "1.0.17" 1483 | source = "registry+https://github.com/rust-lang/crates.io-index" 1484 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 1485 | 1486 | [[package]] 1487 | name = "ryu" 1488 | version = "1.0.18" 1489 | source = "registry+https://github.com/rust-lang/crates.io-index" 1490 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1491 | 1492 | [[package]] 1493 | name = "scopeguard" 1494 | version = "1.2.0" 1495 | source = "registry+https://github.com/rust-lang/crates.io-index" 1496 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1497 | 1498 | [[package]] 1499 | name = "serde" 1500 | version = "1.0.216" 1501 | source = "registry+https://github.com/rust-lang/crates.io-index" 1502 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 1503 | dependencies = [ 1504 | "serde_derive", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "serde_derive" 1509 | version = "1.0.216" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 1512 | dependencies = [ 1513 | "proc-macro2", 1514 | "quote", 1515 | "syn", 1516 | ] 1517 | 1518 | [[package]] 1519 | name = "serde_json" 1520 | version = "1.0.133" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" 1523 | dependencies = [ 1524 | "itoa", 1525 | "memchr", 1526 | "ryu", 1527 | "serde", 1528 | ] 1529 | 1530 | [[package]] 1531 | name = "serde_path_to_error" 1532 | version = "0.1.16" 1533 | source = "registry+https://github.com/rust-lang/crates.io-index" 1534 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 1535 | dependencies = [ 1536 | "itoa", 1537 | "serde", 1538 | ] 1539 | 1540 | [[package]] 1541 | name = "serde_urlencoded" 1542 | version = "0.7.1" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1545 | dependencies = [ 1546 | "form_urlencoded", 1547 | "itoa", 1548 | "ryu", 1549 | "serde", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "serde_variant" 1554 | version = "0.1.3" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432" 1557 | dependencies = [ 1558 | "serde", 1559 | ] 1560 | 1561 | [[package]] 1562 | name = "sha1" 1563 | version = "0.10.6" 1564 | source = "registry+https://github.com/rust-lang/crates.io-index" 1565 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1566 | dependencies = [ 1567 | "cfg-if", 1568 | "cpufeatures", 1569 | "digest", 1570 | ] 1571 | 1572 | [[package]] 1573 | name = "sha2" 1574 | version = "0.10.8" 1575 | source = "registry+https://github.com/rust-lang/crates.io-index" 1576 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1577 | dependencies = [ 1578 | "cfg-if", 1579 | "cpufeatures", 1580 | "digest", 1581 | ] 1582 | 1583 | [[package]] 1584 | name = "sharded-slab" 1585 | version = "0.1.7" 1586 | source = "registry+https://github.com/rust-lang/crates.io-index" 1587 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 1588 | dependencies = [ 1589 | "lazy_static", 1590 | ] 1591 | 1592 | [[package]] 1593 | name = "shlex" 1594 | version = "1.3.0" 1595 | source = "registry+https://github.com/rust-lang/crates.io-index" 1596 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1597 | 1598 | [[package]] 1599 | name = "signal-hook-registry" 1600 | version = "1.4.2" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1603 | dependencies = [ 1604 | "libc", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "signature" 1609 | version = "2.2.0" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1612 | dependencies = [ 1613 | "digest", 1614 | "rand_core", 1615 | ] 1616 | 1617 | [[package]] 1618 | name = "slab" 1619 | version = "0.4.9" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1622 | dependencies = [ 1623 | "autocfg", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "smallvec" 1628 | version = "1.13.2" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1631 | dependencies = [ 1632 | "serde", 1633 | ] 1634 | 1635 | [[package]] 1636 | name = "socket2" 1637 | version = "0.5.7" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 1640 | dependencies = [ 1641 | "libc", 1642 | "windows-sys 0.52.0", 1643 | ] 1644 | 1645 | [[package]] 1646 | name = "spin" 1647 | version = "0.9.8" 1648 | source = "registry+https://github.com/rust-lang/crates.io-index" 1649 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1650 | dependencies = [ 1651 | "lock_api", 1652 | ] 1653 | 1654 | [[package]] 1655 | name = "spki" 1656 | version = "0.7.3" 1657 | source = "registry+https://github.com/rust-lang/crates.io-index" 1658 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1659 | dependencies = [ 1660 | "base64ct", 1661 | "der", 1662 | ] 1663 | 1664 | [[package]] 1665 | name = "sqlformat" 1666 | version = "0.2.6" 1667 | source = "registry+https://github.com/rust-lang/crates.io-index" 1668 | checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" 1669 | dependencies = [ 1670 | "nom", 1671 | "unicode_categories", 1672 | ] 1673 | 1674 | [[package]] 1675 | name = "sqlx" 1676 | version = "0.8.2" 1677 | source = "registry+https://github.com/rust-lang/crates.io-index" 1678 | checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e" 1679 | dependencies = [ 1680 | "sqlx-core", 1681 | "sqlx-macros", 1682 | "sqlx-mysql", 1683 | "sqlx-postgres", 1684 | "sqlx-sqlite", 1685 | ] 1686 | 1687 | [[package]] 1688 | name = "sqlx-core" 1689 | version = "0.8.2" 1690 | source = "registry+https://github.com/rust-lang/crates.io-index" 1691 | checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e" 1692 | dependencies = [ 1693 | "atoi", 1694 | "byteorder", 1695 | "bytes", 1696 | "crc", 1697 | "crossbeam-queue", 1698 | "either", 1699 | "event-listener", 1700 | "futures-channel", 1701 | "futures-core", 1702 | "futures-intrusive", 1703 | "futures-io", 1704 | "futures-util", 1705 | "hashbrown 0.14.5", 1706 | "hashlink", 1707 | "hex", 1708 | "indexmap", 1709 | "log", 1710 | "memchr", 1711 | "once_cell", 1712 | "paste", 1713 | "percent-encoding", 1714 | "serde", 1715 | "serde_json", 1716 | "sha2", 1717 | "smallvec", 1718 | "sqlformat", 1719 | "thiserror", 1720 | "tokio", 1721 | "tokio-stream", 1722 | "tracing", 1723 | "url", 1724 | ] 1725 | 1726 | [[package]] 1727 | name = "sqlx-macros" 1728 | version = "0.8.2" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" 1731 | dependencies = [ 1732 | "proc-macro2", 1733 | "quote", 1734 | "sqlx-core", 1735 | "sqlx-macros-core", 1736 | "syn", 1737 | ] 1738 | 1739 | [[package]] 1740 | name = "sqlx-macros-core" 1741 | version = "0.8.2" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" 1744 | dependencies = [ 1745 | "dotenvy", 1746 | "either", 1747 | "heck", 1748 | "hex", 1749 | "once_cell", 1750 | "proc-macro2", 1751 | "quote", 1752 | "serde", 1753 | "serde_json", 1754 | "sha2", 1755 | "sqlx-core", 1756 | "sqlx-mysql", 1757 | "sqlx-postgres", 1758 | "sqlx-sqlite", 1759 | "syn", 1760 | "tempfile", 1761 | "tokio", 1762 | "url", 1763 | ] 1764 | 1765 | [[package]] 1766 | name = "sqlx-mysql" 1767 | version = "0.8.2" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" 1770 | dependencies = [ 1771 | "atoi", 1772 | "base64", 1773 | "bitflags", 1774 | "byteorder", 1775 | "bytes", 1776 | "crc", 1777 | "digest", 1778 | "dotenvy", 1779 | "either", 1780 | "futures-channel", 1781 | "futures-core", 1782 | "futures-io", 1783 | "futures-util", 1784 | "generic-array", 1785 | "hex", 1786 | "hkdf", 1787 | "hmac", 1788 | "itoa", 1789 | "log", 1790 | "md-5", 1791 | "memchr", 1792 | "once_cell", 1793 | "percent-encoding", 1794 | "rand", 1795 | "rsa", 1796 | "serde", 1797 | "sha1", 1798 | "sha2", 1799 | "smallvec", 1800 | "sqlx-core", 1801 | "stringprep", 1802 | "thiserror", 1803 | "tracing", 1804 | "whoami", 1805 | ] 1806 | 1807 | [[package]] 1808 | name = "sqlx-postgres" 1809 | version = "0.8.2" 1810 | source = "registry+https://github.com/rust-lang/crates.io-index" 1811 | checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" 1812 | dependencies = [ 1813 | "atoi", 1814 | "base64", 1815 | "bitflags", 1816 | "byteorder", 1817 | "crc", 1818 | "dotenvy", 1819 | "etcetera", 1820 | "futures-channel", 1821 | "futures-core", 1822 | "futures-io", 1823 | "futures-util", 1824 | "hex", 1825 | "hkdf", 1826 | "hmac", 1827 | "home", 1828 | "itoa", 1829 | "log", 1830 | "md-5", 1831 | "memchr", 1832 | "once_cell", 1833 | "rand", 1834 | "serde", 1835 | "serde_json", 1836 | "sha2", 1837 | "smallvec", 1838 | "sqlx-core", 1839 | "stringprep", 1840 | "thiserror", 1841 | "tracing", 1842 | "whoami", 1843 | ] 1844 | 1845 | [[package]] 1846 | name = "sqlx-sqlite" 1847 | version = "0.8.2" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680" 1850 | dependencies = [ 1851 | "atoi", 1852 | "flume", 1853 | "futures-channel", 1854 | "futures-core", 1855 | "futures-executor", 1856 | "futures-intrusive", 1857 | "futures-util", 1858 | "libsqlite3-sys", 1859 | "log", 1860 | "percent-encoding", 1861 | "serde", 1862 | "serde_urlencoded", 1863 | "sqlx-core", 1864 | "tracing", 1865 | "url", 1866 | ] 1867 | 1868 | [[package]] 1869 | name = "stringprep" 1870 | version = "0.1.5" 1871 | source = "registry+https://github.com/rust-lang/crates.io-index" 1872 | checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" 1873 | dependencies = [ 1874 | "unicode-bidi", 1875 | "unicode-normalization", 1876 | "unicode-properties", 1877 | ] 1878 | 1879 | [[package]] 1880 | name = "subtle" 1881 | version = "2.6.1" 1882 | source = "registry+https://github.com/rust-lang/crates.io-index" 1883 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1884 | 1885 | [[package]] 1886 | name = "syn" 1887 | version = "2.0.90" 1888 | source = "registry+https://github.com/rust-lang/crates.io-index" 1889 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 1890 | dependencies = [ 1891 | "proc-macro2", 1892 | "quote", 1893 | "unicode-ident", 1894 | ] 1895 | 1896 | [[package]] 1897 | name = "sync_wrapper" 1898 | version = "0.1.2" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" 1901 | 1902 | [[package]] 1903 | name = "sync_wrapper" 1904 | version = "1.0.1" 1905 | source = "registry+https://github.com/rust-lang/crates.io-index" 1906 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1907 | dependencies = [ 1908 | "futures-core", 1909 | ] 1910 | 1911 | [[package]] 1912 | name = "tempfile" 1913 | version = "3.13.0" 1914 | source = "registry+https://github.com/rust-lang/crates.io-index" 1915 | checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" 1916 | dependencies = [ 1917 | "cfg-if", 1918 | "fastrand", 1919 | "once_cell", 1920 | "rustix", 1921 | "windows-sys 0.59.0", 1922 | ] 1923 | 1924 | [[package]] 1925 | name = "thiserror" 1926 | version = "1.0.64" 1927 | source = "registry+https://github.com/rust-lang/crates.io-index" 1928 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 1929 | dependencies = [ 1930 | "thiserror-impl", 1931 | ] 1932 | 1933 | [[package]] 1934 | name = "thiserror-impl" 1935 | version = "1.0.64" 1936 | source = "registry+https://github.com/rust-lang/crates.io-index" 1937 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 1938 | dependencies = [ 1939 | "proc-macro2", 1940 | "quote", 1941 | "syn", 1942 | ] 1943 | 1944 | [[package]] 1945 | name = "thousands" 1946 | version = "0.2.0" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" 1949 | 1950 | [[package]] 1951 | name = "thread_local" 1952 | version = "1.1.8" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1955 | dependencies = [ 1956 | "cfg-if", 1957 | "once_cell", 1958 | ] 1959 | 1960 | [[package]] 1961 | name = "time" 1962 | version = "0.3.37" 1963 | source = "registry+https://github.com/rust-lang/crates.io-index" 1964 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 1965 | dependencies = [ 1966 | "deranged", 1967 | "itoa", 1968 | "num-conv", 1969 | "powerfmt", 1970 | "serde", 1971 | "time-core", 1972 | "time-macros", 1973 | ] 1974 | 1975 | [[package]] 1976 | name = "time-core" 1977 | version = "0.1.2" 1978 | source = "registry+https://github.com/rust-lang/crates.io-index" 1979 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1980 | 1981 | [[package]] 1982 | name = "time-macros" 1983 | version = "0.2.19" 1984 | source = "registry+https://github.com/rust-lang/crates.io-index" 1985 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 1986 | dependencies = [ 1987 | "num-conv", 1988 | "time-core", 1989 | ] 1990 | 1991 | [[package]] 1992 | name = "tinyvec" 1993 | version = "1.8.0" 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" 1995 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1996 | dependencies = [ 1997 | "tinyvec_macros", 1998 | ] 1999 | 2000 | [[package]] 2001 | name = "tinyvec_macros" 2002 | version = "0.1.1" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2005 | 2006 | [[package]] 2007 | name = "tokio" 2008 | version = "1.42.0" 2009 | source = "registry+https://github.com/rust-lang/crates.io-index" 2010 | checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" 2011 | dependencies = [ 2012 | "backtrace", 2013 | "bytes", 2014 | "libc", 2015 | "mio", 2016 | "parking_lot", 2017 | "pin-project-lite", 2018 | "signal-hook-registry", 2019 | "socket2", 2020 | "tokio-macros", 2021 | "windows-sys 0.52.0", 2022 | ] 2023 | 2024 | [[package]] 2025 | name = "tokio-cron-scheduler" 2026 | version = "0.13.0" 2027 | source = "registry+https://github.com/rust-lang/crates.io-index" 2028 | checksum = "6a5597b569b4712cf78aa0c9ae29742461b7bda1e49c2a5fdad1d79bf022f8f0" 2029 | dependencies = [ 2030 | "chrono", 2031 | "croner", 2032 | "num-derive", 2033 | "num-traits", 2034 | "tokio", 2035 | "tracing", 2036 | "uuid", 2037 | ] 2038 | 2039 | [[package]] 2040 | name = "tokio-macros" 2041 | version = "2.4.0" 2042 | source = "registry+https://github.com/rust-lang/crates.io-index" 2043 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 2044 | dependencies = [ 2045 | "proc-macro2", 2046 | "quote", 2047 | "syn", 2048 | ] 2049 | 2050 | [[package]] 2051 | name = "tokio-rustls" 2052 | version = "0.26.0" 2053 | source = "registry+https://github.com/rust-lang/crates.io-index" 2054 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 2055 | dependencies = [ 2056 | "rustls", 2057 | "rustls-pki-types", 2058 | "tokio", 2059 | ] 2060 | 2061 | [[package]] 2062 | name = "tokio-stream" 2063 | version = "0.1.16" 2064 | source = "registry+https://github.com/rust-lang/crates.io-index" 2065 | checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" 2066 | dependencies = [ 2067 | "futures-core", 2068 | "pin-project-lite", 2069 | "tokio", 2070 | ] 2071 | 2072 | [[package]] 2073 | name = "tower" 2074 | version = "0.5.1" 2075 | source = "registry+https://github.com/rust-lang/crates.io-index" 2076 | checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" 2077 | dependencies = [ 2078 | "futures-core", 2079 | "futures-util", 2080 | "pin-project-lite", 2081 | "sync_wrapper 0.1.2", 2082 | "tokio", 2083 | "tower-layer", 2084 | "tower-service", 2085 | "tracing", 2086 | ] 2087 | 2088 | [[package]] 2089 | name = "tower-http" 2090 | version = "0.6.2" 2091 | source = "registry+https://github.com/rust-lang/crates.io-index" 2092 | checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" 2093 | dependencies = [ 2094 | "bitflags", 2095 | "bytes", 2096 | "http", 2097 | "http-body", 2098 | "pin-project-lite", 2099 | "tower-layer", 2100 | "tower-service", 2101 | "tracing", 2102 | ] 2103 | 2104 | [[package]] 2105 | name = "tower-layer" 2106 | version = "0.3.3" 2107 | source = "registry+https://github.com/rust-lang/crates.io-index" 2108 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2109 | 2110 | [[package]] 2111 | name = "tower-service" 2112 | version = "0.3.3" 2113 | source = "registry+https://github.com/rust-lang/crates.io-index" 2114 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2115 | 2116 | [[package]] 2117 | name = "tracing" 2118 | version = "0.1.41" 2119 | source = "registry+https://github.com/rust-lang/crates.io-index" 2120 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2121 | dependencies = [ 2122 | "log", 2123 | "pin-project-lite", 2124 | "tracing-attributes", 2125 | "tracing-core", 2126 | ] 2127 | 2128 | [[package]] 2129 | name = "tracing-attributes" 2130 | version = "0.1.28" 2131 | source = "registry+https://github.com/rust-lang/crates.io-index" 2132 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 2133 | dependencies = [ 2134 | "proc-macro2", 2135 | "quote", 2136 | "syn", 2137 | ] 2138 | 2139 | [[package]] 2140 | name = "tracing-core" 2141 | version = "0.1.33" 2142 | source = "registry+https://github.com/rust-lang/crates.io-index" 2143 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2144 | dependencies = [ 2145 | "once_cell", 2146 | "valuable", 2147 | ] 2148 | 2149 | [[package]] 2150 | name = "tracing-log" 2151 | version = "0.2.0" 2152 | source = "registry+https://github.com/rust-lang/crates.io-index" 2153 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2154 | dependencies = [ 2155 | "log", 2156 | "once_cell", 2157 | "tracing-core", 2158 | ] 2159 | 2160 | [[package]] 2161 | name = "tracing-logfmt" 2162 | version = "0.3.5" 2163 | source = "registry+https://github.com/rust-lang/crates.io-index" 2164 | checksum = "6b1f47d22deb79c3f59fcf2a1f00f60cbdc05462bf17d1cd356c1fefa3f444bd" 2165 | dependencies = [ 2166 | "nu-ansi-term 0.50.1", 2167 | "time", 2168 | "tracing", 2169 | "tracing-core", 2170 | "tracing-subscriber", 2171 | ] 2172 | 2173 | [[package]] 2174 | name = "tracing-subscriber" 2175 | version = "0.3.19" 2176 | source = "registry+https://github.com/rust-lang/crates.io-index" 2177 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 2178 | dependencies = [ 2179 | "matchers", 2180 | "nu-ansi-term 0.46.0", 2181 | "once_cell", 2182 | "regex", 2183 | "sharded-slab", 2184 | "smallvec", 2185 | "thread_local", 2186 | "tracing", 2187 | "tracing-core", 2188 | "tracing-log", 2189 | ] 2190 | 2191 | [[package]] 2192 | name = "try-lock" 2193 | version = "0.2.5" 2194 | source = "registry+https://github.com/rust-lang/crates.io-index" 2195 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2196 | 2197 | [[package]] 2198 | name = "typenum" 2199 | version = "1.17.0" 2200 | source = "registry+https://github.com/rust-lang/crates.io-index" 2201 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2202 | 2203 | [[package]] 2204 | name = "unicode-bidi" 2205 | version = "0.3.17" 2206 | source = "registry+https://github.com/rust-lang/crates.io-index" 2207 | checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" 2208 | 2209 | [[package]] 2210 | name = "unicode-ident" 2211 | version = "1.0.13" 2212 | source = "registry+https://github.com/rust-lang/crates.io-index" 2213 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 2214 | 2215 | [[package]] 2216 | name = "unicode-normalization" 2217 | version = "0.1.24" 2218 | source = "registry+https://github.com/rust-lang/crates.io-index" 2219 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 2220 | dependencies = [ 2221 | "tinyvec", 2222 | ] 2223 | 2224 | [[package]] 2225 | name = "unicode-properties" 2226 | version = "0.1.3" 2227 | source = "registry+https://github.com/rust-lang/crates.io-index" 2228 | checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 2229 | 2230 | [[package]] 2231 | name = "unicode_categories" 2232 | version = "0.1.1" 2233 | source = "registry+https://github.com/rust-lang/crates.io-index" 2234 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 2235 | 2236 | [[package]] 2237 | name = "untrusted" 2238 | version = "0.9.0" 2239 | source = "registry+https://github.com/rust-lang/crates.io-index" 2240 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2241 | 2242 | [[package]] 2243 | name = "url" 2244 | version = "2.5.2" 2245 | source = "registry+https://github.com/rust-lang/crates.io-index" 2246 | checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" 2247 | dependencies = [ 2248 | "form_urlencoded", 2249 | "idna", 2250 | "percent-encoding", 2251 | ] 2252 | 2253 | [[package]] 2254 | name = "uuid" 2255 | version = "1.10.0" 2256 | source = "registry+https://github.com/rust-lang/crates.io-index" 2257 | checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" 2258 | dependencies = [ 2259 | "getrandom", 2260 | ] 2261 | 2262 | [[package]] 2263 | name = "valuable" 2264 | version = "0.1.0" 2265 | source = "registry+https://github.com/rust-lang/crates.io-index" 2266 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2267 | 2268 | [[package]] 2269 | name = "vcpkg" 2270 | version = "0.2.15" 2271 | source = "registry+https://github.com/rust-lang/crates.io-index" 2272 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2273 | 2274 | [[package]] 2275 | name = "version_check" 2276 | version = "0.9.5" 2277 | source = "registry+https://github.com/rust-lang/crates.io-index" 2278 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2279 | 2280 | [[package]] 2281 | name = "want" 2282 | version = "0.3.1" 2283 | source = "registry+https://github.com/rust-lang/crates.io-index" 2284 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2285 | dependencies = [ 2286 | "try-lock", 2287 | ] 2288 | 2289 | [[package]] 2290 | name = "wasi" 2291 | version = "0.11.0+wasi-snapshot-preview1" 2292 | source = "registry+https://github.com/rust-lang/crates.io-index" 2293 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2294 | 2295 | [[package]] 2296 | name = "wasite" 2297 | version = "0.1.0" 2298 | source = "registry+https://github.com/rust-lang/crates.io-index" 2299 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2300 | 2301 | [[package]] 2302 | name = "wasm-bindgen" 2303 | version = "0.2.93" 2304 | source = "registry+https://github.com/rust-lang/crates.io-index" 2305 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 2306 | dependencies = [ 2307 | "cfg-if", 2308 | "once_cell", 2309 | "wasm-bindgen-macro", 2310 | ] 2311 | 2312 | [[package]] 2313 | name = "wasm-bindgen-backend" 2314 | version = "0.2.93" 2315 | source = "registry+https://github.com/rust-lang/crates.io-index" 2316 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 2317 | dependencies = [ 2318 | "bumpalo", 2319 | "log", 2320 | "once_cell", 2321 | "proc-macro2", 2322 | "quote", 2323 | "syn", 2324 | "wasm-bindgen-shared", 2325 | ] 2326 | 2327 | [[package]] 2328 | name = "wasm-bindgen-futures" 2329 | version = "0.4.43" 2330 | source = "registry+https://github.com/rust-lang/crates.io-index" 2331 | checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" 2332 | dependencies = [ 2333 | "cfg-if", 2334 | "js-sys", 2335 | "wasm-bindgen", 2336 | "web-sys", 2337 | ] 2338 | 2339 | [[package]] 2340 | name = "wasm-bindgen-macro" 2341 | version = "0.2.93" 2342 | source = "registry+https://github.com/rust-lang/crates.io-index" 2343 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 2344 | dependencies = [ 2345 | "quote", 2346 | "wasm-bindgen-macro-support", 2347 | ] 2348 | 2349 | [[package]] 2350 | name = "wasm-bindgen-macro-support" 2351 | version = "0.2.93" 2352 | source = "registry+https://github.com/rust-lang/crates.io-index" 2353 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 2354 | dependencies = [ 2355 | "proc-macro2", 2356 | "quote", 2357 | "syn", 2358 | "wasm-bindgen-backend", 2359 | "wasm-bindgen-shared", 2360 | ] 2361 | 2362 | [[package]] 2363 | name = "wasm-bindgen-shared" 2364 | version = "0.2.93" 2365 | source = "registry+https://github.com/rust-lang/crates.io-index" 2366 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 2367 | 2368 | [[package]] 2369 | name = "web-sys" 2370 | version = "0.3.70" 2371 | source = "registry+https://github.com/rust-lang/crates.io-index" 2372 | checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" 2373 | dependencies = [ 2374 | "js-sys", 2375 | "wasm-bindgen", 2376 | ] 2377 | 2378 | [[package]] 2379 | name = "webpki-roots" 2380 | version = "0.26.6" 2381 | source = "registry+https://github.com/rust-lang/crates.io-index" 2382 | checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" 2383 | dependencies = [ 2384 | "rustls-pki-types", 2385 | ] 2386 | 2387 | [[package]] 2388 | name = "whoami" 2389 | version = "1.5.2" 2390 | source = "registry+https://github.com/rust-lang/crates.io-index" 2391 | checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 2392 | dependencies = [ 2393 | "redox_syscall", 2394 | "wasite", 2395 | ] 2396 | 2397 | [[package]] 2398 | name = "winapi" 2399 | version = "0.3.9" 2400 | source = "registry+https://github.com/rust-lang/crates.io-index" 2401 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2402 | dependencies = [ 2403 | "winapi-i686-pc-windows-gnu", 2404 | "winapi-x86_64-pc-windows-gnu", 2405 | ] 2406 | 2407 | [[package]] 2408 | name = "winapi-i686-pc-windows-gnu" 2409 | version = "0.4.0" 2410 | source = "registry+https://github.com/rust-lang/crates.io-index" 2411 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2412 | 2413 | [[package]] 2414 | name = "winapi-x86_64-pc-windows-gnu" 2415 | version = "0.4.0" 2416 | source = "registry+https://github.com/rust-lang/crates.io-index" 2417 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2418 | 2419 | [[package]] 2420 | name = "windows-core" 2421 | version = "0.52.0" 2422 | source = "registry+https://github.com/rust-lang/crates.io-index" 2423 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2424 | dependencies = [ 2425 | "windows-targets 0.52.6", 2426 | ] 2427 | 2428 | [[package]] 2429 | name = "windows-registry" 2430 | version = "0.2.0" 2431 | source = "registry+https://github.com/rust-lang/crates.io-index" 2432 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 2433 | dependencies = [ 2434 | "windows-result", 2435 | "windows-strings", 2436 | "windows-targets 0.52.6", 2437 | ] 2438 | 2439 | [[package]] 2440 | name = "windows-result" 2441 | version = "0.2.0" 2442 | source = "registry+https://github.com/rust-lang/crates.io-index" 2443 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2444 | dependencies = [ 2445 | "windows-targets 0.52.6", 2446 | ] 2447 | 2448 | [[package]] 2449 | name = "windows-strings" 2450 | version = "0.1.0" 2451 | source = "registry+https://github.com/rust-lang/crates.io-index" 2452 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2453 | dependencies = [ 2454 | "windows-result", 2455 | "windows-targets 0.52.6", 2456 | ] 2457 | 2458 | [[package]] 2459 | name = "windows-sys" 2460 | version = "0.48.0" 2461 | source = "registry+https://github.com/rust-lang/crates.io-index" 2462 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2463 | dependencies = [ 2464 | "windows-targets 0.48.5", 2465 | ] 2466 | 2467 | [[package]] 2468 | name = "windows-sys" 2469 | version = "0.52.0" 2470 | source = "registry+https://github.com/rust-lang/crates.io-index" 2471 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2472 | dependencies = [ 2473 | "windows-targets 0.52.6", 2474 | ] 2475 | 2476 | [[package]] 2477 | name = "windows-sys" 2478 | version = "0.59.0" 2479 | source = "registry+https://github.com/rust-lang/crates.io-index" 2480 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2481 | dependencies = [ 2482 | "windows-targets 0.52.6", 2483 | ] 2484 | 2485 | [[package]] 2486 | name = "windows-targets" 2487 | version = "0.48.5" 2488 | source = "registry+https://github.com/rust-lang/crates.io-index" 2489 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2490 | dependencies = [ 2491 | "windows_aarch64_gnullvm 0.48.5", 2492 | "windows_aarch64_msvc 0.48.5", 2493 | "windows_i686_gnu 0.48.5", 2494 | "windows_i686_msvc 0.48.5", 2495 | "windows_x86_64_gnu 0.48.5", 2496 | "windows_x86_64_gnullvm 0.48.5", 2497 | "windows_x86_64_msvc 0.48.5", 2498 | ] 2499 | 2500 | [[package]] 2501 | name = "windows-targets" 2502 | version = "0.52.6" 2503 | source = "registry+https://github.com/rust-lang/crates.io-index" 2504 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2505 | dependencies = [ 2506 | "windows_aarch64_gnullvm 0.52.6", 2507 | "windows_aarch64_msvc 0.52.6", 2508 | "windows_i686_gnu 0.52.6", 2509 | "windows_i686_gnullvm", 2510 | "windows_i686_msvc 0.52.6", 2511 | "windows_x86_64_gnu 0.52.6", 2512 | "windows_x86_64_gnullvm 0.52.6", 2513 | "windows_x86_64_msvc 0.52.6", 2514 | ] 2515 | 2516 | [[package]] 2517 | name = "windows_aarch64_gnullvm" 2518 | version = "0.48.5" 2519 | source = "registry+https://github.com/rust-lang/crates.io-index" 2520 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2521 | 2522 | [[package]] 2523 | name = "windows_aarch64_gnullvm" 2524 | version = "0.52.6" 2525 | source = "registry+https://github.com/rust-lang/crates.io-index" 2526 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2527 | 2528 | [[package]] 2529 | name = "windows_aarch64_msvc" 2530 | version = "0.48.5" 2531 | source = "registry+https://github.com/rust-lang/crates.io-index" 2532 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2533 | 2534 | [[package]] 2535 | name = "windows_aarch64_msvc" 2536 | version = "0.52.6" 2537 | source = "registry+https://github.com/rust-lang/crates.io-index" 2538 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2539 | 2540 | [[package]] 2541 | name = "windows_i686_gnu" 2542 | version = "0.48.5" 2543 | source = "registry+https://github.com/rust-lang/crates.io-index" 2544 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2545 | 2546 | [[package]] 2547 | name = "windows_i686_gnu" 2548 | version = "0.52.6" 2549 | source = "registry+https://github.com/rust-lang/crates.io-index" 2550 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2551 | 2552 | [[package]] 2553 | name = "windows_i686_gnullvm" 2554 | version = "0.52.6" 2555 | source = "registry+https://github.com/rust-lang/crates.io-index" 2556 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2557 | 2558 | [[package]] 2559 | name = "windows_i686_msvc" 2560 | version = "0.48.5" 2561 | source = "registry+https://github.com/rust-lang/crates.io-index" 2562 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2563 | 2564 | [[package]] 2565 | name = "windows_i686_msvc" 2566 | version = "0.52.6" 2567 | source = "registry+https://github.com/rust-lang/crates.io-index" 2568 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2569 | 2570 | [[package]] 2571 | name = "windows_x86_64_gnu" 2572 | version = "0.48.5" 2573 | source = "registry+https://github.com/rust-lang/crates.io-index" 2574 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2575 | 2576 | [[package]] 2577 | name = "windows_x86_64_gnu" 2578 | version = "0.52.6" 2579 | source = "registry+https://github.com/rust-lang/crates.io-index" 2580 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2581 | 2582 | [[package]] 2583 | name = "windows_x86_64_gnullvm" 2584 | version = "0.48.5" 2585 | source = "registry+https://github.com/rust-lang/crates.io-index" 2586 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2587 | 2588 | [[package]] 2589 | name = "windows_x86_64_gnullvm" 2590 | version = "0.52.6" 2591 | source = "registry+https://github.com/rust-lang/crates.io-index" 2592 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2593 | 2594 | [[package]] 2595 | name = "windows_x86_64_msvc" 2596 | version = "0.48.5" 2597 | source = "registry+https://github.com/rust-lang/crates.io-index" 2598 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2599 | 2600 | [[package]] 2601 | name = "windows_x86_64_msvc" 2602 | version = "0.52.6" 2603 | source = "registry+https://github.com/rust-lang/crates.io-index" 2604 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2605 | 2606 | [[package]] 2607 | name = "zerocopy" 2608 | version = "0.7.35" 2609 | source = "registry+https://github.com/rust-lang/crates.io-index" 2610 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2611 | dependencies = [ 2612 | "byteorder", 2613 | "zerocopy-derive", 2614 | ] 2615 | 2616 | [[package]] 2617 | name = "zerocopy-derive" 2618 | version = "0.7.35" 2619 | source = "registry+https://github.com/rust-lang/crates.io-index" 2620 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2621 | dependencies = [ 2622 | "proc-macro2", 2623 | "quote", 2624 | "syn", 2625 | ] 2626 | 2627 | [[package]] 2628 | name = "zeroize" 2629 | version = "1.8.1" 2630 | source = "registry+https://github.com/rust-lang/crates.io-index" 2631 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2632 | --------------------------------------------------------------------------------