Skip to content

Commit

Permalink
add postgresql tls support
Browse files Browse the repository at this point in the history
  • Loading branch information
Erinable committed Jan 11, 2025
1 parent 3e18f36 commit 5eae0eb
Show file tree
Hide file tree
Showing 10 changed files with 330 additions and 171 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ lcov.info
# Project-specific temporary files
podcast_crawler.log*
podcast_crawler.pid
.aider*
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ harness = false
name = "load_test"

[dependencies]
actix-cors = "0.7.0"
# Web & HTTP
actix-web = "4.4"
ammonia = "3.3"
Expand Down Expand Up @@ -40,6 +41,8 @@ rand = "0.8"
reqwest = {version = "0.11", features = ["json", "rustls-tls"]}
# RSS and Parsing
rss = "2.0"
rustls = "0.23.21"
rustls-platform-verifier = "0.5.0"
# Serialization
serde = {version = "1.0", features = ["derive"]}
serde_json = "1.0"
Expand All @@ -48,6 +51,8 @@ thiserror = "1.0"
time = {version = "0.3", features = ["formatting"]}
# Async Runtime
tokio = {version = "1.32", features = ["full"]}
tokio-postgres = "0.7.12"
tokio-postgres-rustls = "0.13.0"
tokio-stream = "0.1"
tokio-util = {version = "0.7.13", features = ["rt"]}
# Logging and Tracing
Expand Down
13 changes: 13 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Build stage
FROM rust:bookworm AS builder

WORKDIR /app
COPY . .
RUN cargo build --release

# Final run stage
FROM debian:bookworm-slim AS runner

WORKDIR /app
COPY --from=builder /app/target/release/podcast_crawler /app/podcast_crawler
CMD ["/app/podcast_crawler"]
203 changes: 49 additions & 154 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -1,170 +1,65 @@
# API 文档

## 认证

目前处于开发阶段,暂未实现认证机制。后续会添加基于 JWT 的认证。

## 端点

### Podcast 相关接口

#### 获取播客列表

```http
GET /api/podcasts
```

查询参数:

- `page`: 页码(默认:1)
- `per_page`: 每页数量(默认:20)
- `sort`: 排序字段(可选:title, updated_at)
- `order`: 排序方向(asc/desc)

响应格式:

```json
{
"data": [
{
"id": "uuid",
"title": "播客标题",
"description": "描述",
"author": "作者",
"website": "网站",
"feed_url": "RSS feed URL",
"image_url": "封面图片 URL",
"created_at": "创建时间",
"updated_at": "更新时间"
}
],
"meta": {
"total": 100,
"page": 1,
"per_page": 20,
"total_pages": 5
}
}
```

#### 获取单个播客

```http
GET /api/podcasts/{id}
```

响应格式:

```json
{
"data": {
"id": "uuid",
"title": "播客标题",
"description": "描述",
"author": "作者",
"website": "网站",
"feed_url": "RSS feed URL",
"image_url": "封面图片 URL",
"episodes": [
{
"id": "uuid",
"title": "标题",
"description": "描述",
"audio_url": "音频 URL",
"duration": "时长",
"published_at": "发布时间"
}
],
"created_at": "创建时间",
"updated_at": "更新时间"
}
}
```
# 播客系统 API 文档

### 抓取任务相关接口
## 监控指标接口

#### 创建抓取任务
### 1. 获取监控指标

```http
POST /api/crawl
```
- **路径**: `/metrics`
- **方法**: GET
- **功能**: 获取系统运行监控指标
- **响应**: Prometheus 格式的监控数据

请求体:
### 2. 添加任务

```json
{
"feed_url": "要抓取的 RSS feed URL",
"force_update": false
}
```
- **路径**: `/add_task`
- **方法**: POST
- **功能**: 添加新的 RSS 爬取任务
- **请求体**:

响应格式:

```json
{
"data": {
"task_id": "uuid",
"status": "pending",
"created_at": "创建时间"
}
}
```

#### 获取任务状态

```http
GET /api/crawl/{task_id}
```

响应格式:

```json
{
"data": {
"task_id": "uuid",
"status": "running|completed|failed",
"progress": 80,
"error": "错误信息(如果失败)",
"created_at": "创建时间",
"updated_at": "更新时间"
```json
{
"rss_url": "string"
}
}
```

## 错误处理
## 播客查询接口

所有错误响应使用统一格式:
### 1. 搜索播客

```json
{
"error": {
"code": "ERROR_CODE",
"message": "错误描述",
"details": {
"字段": "具体错误"
}
}
}
```
- 路径: `/podcasts/search`
- 方法: GET
- 参数:
- q: 搜索关键词
- 功能: 按标题搜索播客

### 2. 获取播客列表

- 路径: `/podcasts`
- 方法: GET
- 参数:
- include_episodes: 是否包含剧集信息(可选)
- 功能: 获取播客列表

常见错误码:
## 3. 分页获取播客

- `INVALID_REQUEST`: 请求格式错误
- `NOT_FOUND`: 资源不存在
- `VALIDATION_ERROR`: 数据验证失败
- `CRAWLER_ERROR`: 抓取过程错误
- `DATABASE_ERROR`: 数据库操作错误
- `INTERNAL_ERROR`: 内部服务器错误
- 路径: `/podcasts/page/{page}/{per_page}`
- 方法: GET
- 参数:
- page: 页码
- per_page: 每页数量
- 功能: 分页获取播客列表

## 限流策略
## 4. 按标题获取播客

- 每个 IP 每分钟最多 60 个请求
- 抓取任务每个 IP 每小时最多 10 个
- 超出限制返回 429 状态码
- 路径: `/podcasts/by-title/{title}`
- 方法: GET
- 功能: 根据播客标题获取详细信息

## 版本控制
## 5. 获取播客剧集

- 当前版本:v1
- API 版本通过 URL 前缀指定:`/api/v1/...`
- 重大更改会增加版本号
- 向后兼容的更改在当前版本进行
- 路径: `/podcasts/{id}/episodes/{page}/{per_page}`
- 方法: GET
- 参数:
- id: 播客ID
- page: 页码
- per_page: 每页数量
- 功能: 分页获取指定播客的剧集列表
3 changes: 3 additions & 0 deletions src/infrastructure/config/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub struct DatabaseConfig {
pub min_connections: u32,
pub connect_timeout_seconds: u64,
pub idle_timeout_seconds: u64,
pub no_ssl: bool,
}

impl Default for DatabaseConfig {
Expand All @@ -73,6 +74,7 @@ impl Default for DatabaseConfig {
min_connections: 2,
connect_timeout_seconds: 30,
idle_timeout_seconds: 300,
no_ssl: true,
}
}
}
Expand Down Expand Up @@ -109,6 +111,7 @@ impl DatabaseConfig {
self.connect_timeout_seconds
);
config_set_env!(self, "DATABASE_IDLE_TIMEOUT", self.idle_timeout_seconds);
config_set_env!(self, "NO_SSL", self.no_ssl);
Ok(())
}

Expand Down
18 changes: 11 additions & 7 deletions src/infrastructure/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,17 @@ impl AppState {
})?;

// Basic repository checks
self.repositories.podcast.get_all().await.map_err(|e| {
AppError::Infrastructure(InfrastructureError::new(
InfrastructureErrorKind::Database,
"Podcast repository check failed".to_string(),
Some(Box::new(e)),
))
})?;
self.repositories
.podcast
.get_all(1, 10)
.await
.map_err(|e| {
AppError::Infrastructure(InfrastructureError::new(
InfrastructureErrorKind::Database,
"Podcast repository check failed".to_string(),
Some(Box::new(e)),
))
})?;

Ok(())
}
Expand Down
37 changes: 35 additions & 2 deletions src/infrastructure/persistence/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
//! - Error handling with context
//! - Connection management
use diesel::{ConnectionError, ConnectionResult};
use diesel_async::pooled_connection::bb8::PooledConnection;
use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager};
use diesel_async::pooled_connection::{bb8::Pool, AsyncDieselConnectionManager, ManagerConfig};
use diesel_async::AsyncPgConnection;

use crate::infrastructure::config::DatabaseConfig;
Expand All @@ -17,6 +18,10 @@ use crate::infrastructure::{
error::infrastructure::{InfrastructureError, InfrastructureErrorKind},
AppError, AppResult,
};
use futures::future::BoxFuture;
use futures::FutureExt;
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;

pub type DbPool = Pool<AsyncPgConnection>;
pub type DbConnection<'a> = PooledConnection<'a, AsyncPgConnection>;
Expand All @@ -30,7 +35,16 @@ pub struct DatabaseContext {
impl DatabaseContext {
/// Creates a new `DatabaseContext` with the provided configuration
pub async fn new_with_config(config: &DatabaseConfig) -> AppResult<Self> {
let manager = AsyncDieselConnectionManager::<AsyncPgConnection>::new(config.url.clone());
let manager = if config.no_ssl {
AsyncDieselConnectionManager::<AsyncPgConnection>::new(config.url.clone())
} else {
let mut mgr_config = ManagerConfig::default();
mgr_config.custom_setup = Box::new(establish_connection);
AsyncDieselConnectionManager::<AsyncPgConnection>::new_with_config(
config.url.clone(),
mgr_config,
)
};

let mut builder = Pool::builder()
.max_size(config.max_connections)
Expand Down Expand Up @@ -77,6 +91,25 @@ impl DatabaseContext {
}
}

fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConnection>> {
let fut = async {
// Create a new connection to the database
// Setup the TLS configuration for the connection using native certs
// Using https://crates.io/crates/rustls-platform-verifier
// replaces using rustls-native-certs on its own (recommended)
let tls_config = ClientConfig::with_platform_verifier();
let tls = tokio_postgres_rustls::MakeRustlsConnect::new(tls_config);

// get the client and connection future
let (client, conn) = tokio_postgres::connect(config, tls)
.await
.map_err(|e| ConnectionError::BadConnection(e.to_string()))?;

AsyncPgConnection::try_from_client_and_connection(client, conn).await
};
fut.boxed()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading

0 comments on commit 5eae0eb

Please sign in to comment.