Compare commits

..

4 Commits

6 changed files with 90 additions and 38 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ node_modules/
docs/
logs/

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE file_records\n SET thumbnail_storage_path = $1\n WHERE id = $2\n AND user_id = $3\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int8",
"Int8"
]
},
"nullable": []
},
"hash": "ac8099bc9fa42a4152f2230903f9e9b51edfc01f03fd18cc2fe0ab3a956f3175"
}

29
backend/Cargo.lock generated
View File

@@ -307,6 +307,7 @@ dependencies = [
"tower-http",
"tower_governor",
"tracing",
"tracing-appender",
"tracing-subscriber",
"utoipa",
"utoipa-swagger-ui",
@@ -577,6 +578,15 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@@ -3588,6 +3598,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "2.0.117"
@@ -3900,6 +3916,19 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"

View File

@@ -26,6 +26,7 @@ tower_governor = { version = "0.8.0", default-features = false, features = ["axu
utoipa = { version = "5.5.0", features = ["axum_extras", "chrono"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
image = "0.25.10"
tracing-appender = "0.2.5"
[dev-dependencies]
axum-test = "20.0.0"

View File

@@ -44,12 +44,20 @@ const BODY_LIMIT: usize = 1000 * 1000 * 1000 * 5;
#[tokio::main]
async fn main() -> Result<()> {
let file_appender = tracing_appender::rolling::daily("logs", "loft.log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
format!("{}=info,tower_http=debug", env!("CARGO_CRATE_NAME")).into()
}),
)
.with(
tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(non_blocking),
)
.with(tracing_subscriber::fmt::layer())
.init();
@@ -70,31 +78,12 @@ async fn main() -> Result<()> {
.layer(DefaultBodyLimit::max(BODY_LIMIT));
let user_repository = UserRepository::new(pool);
let mut routes_auth = routes_auth(user_repository.clone());
if std::env::var("ENVIRONMENT").unwrap_or_default() != "test" {
let governor_conf_auth = GovernorConfigBuilder::default()
.per_second(4)
.burst_size(2)
.key_extractor(SmartIpKeyExtractor)
.finish()
.expect("failed to initialize rate limiter configurations");
let governor_auth_limiter = governor_conf_auth.limiter().clone();
let interval = Duration::from_secs(60);
std::thread::spawn(move || {
loop {
std::thread::sleep(interval);
let len = governor_auth_limiter.len();
if len > 0 {
info!("rate limiting auth storage size: {len}");
}
governor_auth_limiter.retain_recent();
}
});
routes_auth = routes_auth.layer(GovernorLayer::new(governor_conf_auth));
}
let routes_auth = routes_auth(user_repository.clone());
let routes_auth = if std::env::var("ENVIRONMENT").unwrap_or_default() == "test" {
routes_auth
} else {
apply_governor(routes_auth)
};
let mut openapi = AuthApi::openapi();
openapi.merge(FileApi::openapi());
@@ -133,3 +122,27 @@ async fn main() -> Result<()> {
Ok(())
}
fn apply_governor(routes_auth: Router) -> Router {
let governor_conf_auth = GovernorConfigBuilder::default()
.per_second(4)
.burst_size(2)
.key_extractor(SmartIpKeyExtractor)
.finish()
.expect("failed to initialize rate limiter configurations");
let governor_auth_limiter = governor_conf_auth.limiter().clone();
let interval = Duration::from_secs(60);
std::thread::spawn(move || {
loop {
std::thread::sleep(interval);
let len = governor_auth_limiter.len();
if len > 0 {
info!("rate limiting auth storage size: {len}");
}
governor_auth_limiter.retain_recent();
}
});
routes_auth.layer(GovernorLayer::new(governor_conf_auth))
}

View File

@@ -1,6 +1,6 @@
use axum::{body::Bytes, extract::multipart::MultipartError};
use futures_util::{Stream, StreamExt};
use opendal::{Operator, layers::LoggingLayer, services};
use opendal::{FuturesBytesStream, Operator, layers::LoggingLayer, services};
use serde::Serialize;
use sqlx::{PgPool, prelude::FromRow};
@@ -80,22 +80,14 @@ impl FileRepository {
Ok(record)
}
pub async fn download_file(
&self,
file_id: i64,
user_id: i64,
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>> {
pub async fn download_file(&self, file_id: i64, user_id: i64) -> Result<FuturesBytesStream> {
let record = self.get_file(file_id, user_id).await?;
let reader = self.op.reader(&record.storage_key).await?;
let stream = reader.into_bytes_stream(0..).await?;
Ok(stream)
}
pub async fn thumbnail(
&self,
file_id: i64,
user_id: i64,
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>> {
pub async fn thumbnail(&self, file_id: i64, user_id: i64) -> Result<FuturesBytesStream> {
let record = self.get_file(file_id, user_id).await?;
let path = format!("thumbnail/{}", &record.storage_key);
let reader = self.op.reader(&path).await?;
@@ -109,7 +101,7 @@ impl FileRepository {
user_id: i64,
from: u64,
to: u64,
) -> Result<impl Stream<Item = std::io::Result<Bytes>> + use<>> {
) -> Result<FuturesBytesStream> {
let record = self.get_file(file_id, user_id).await?;
let reader = self.op.reader(&record.storage_key).await?;
let stream = reader.into_bytes_stream(from..to).await?;
@@ -435,7 +427,7 @@ mod tests {
.await
.unwrap();
assert_eq!(record.thumbnail_storage_path, None);
assert_eq!(record.thumbnail_storage_path, None);
let path = "thumbnail/a.png-uuid";
file_repository