This commit is contained in:
2025-10-09 22:41:04 +02:00
parent 768f8cb21c
commit 4a428b9af6
13 changed files with 1019 additions and 50 deletions
+10
View File
@@ -0,0 +1,10 @@
pub mod auth;
pub mod config;
pub mod db;
pub mod error;
pub mod models;
pub mod routes;
pub mod s3;
pub mod schema;
pub mod state;
pub mod storage;
+10 -15
View File
@@ -1,23 +1,17 @@
mod auth;
mod config;
mod db;
mod error;
mod models;
mod routes;
mod s3;
mod schema;
mod state;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower::make::Shared;
use tracing_subscriber::EnvFilter;
use crate::auth::jwt::JwtService;
use crate::config::AppConfig;
use crate::s3::build_client;
use crate::state::AppState;
use paperless_backend::auth::jwt::JwtService;
use paperless_backend::config::AppConfig;
use paperless_backend::db;
use paperless_backend::routes;
use paperless_backend::s3::build_client;
use paperless_backend::state::AppState;
use paperless_backend::storage::S3Storage;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -27,9 +21,10 @@ async fn main() -> anyhow::Result<()> {
let config = AppConfig::from_env()?;
let pool = db::init_pool(&config.database_url)?;
let s3_client = build_client(&config).await?;
let storage = Arc::new(S3Storage::new(s3_client, config.s3_bucket.clone()));
let jwt = JwtService::from_config(&config)?;
let state = AppState::new(pool, config, s3_client, jwt);
let state = AppState::new(pool, config, storage, jwt);
let router = routes::create_router(state.clone());
+11 -28
View File
@@ -1,8 +1,6 @@
use std::collections::HashMap;
use std::time::Duration;
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::primitives::ByteStream;
use axum::extract::{Json, Multipart, Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
@@ -267,21 +265,11 @@ pub async fn upload_document(
}
}
let mut put_request = state
.s3
.put_object()
.bucket(&state.config.s3_bucket)
.key(&s3_key)
.body(ByteStream::from(file_bytes.clone()));
if let Some(ref ct) = content_type {
put_request = put_request.content_type(ct.clone());
}
put_request
.send()
state
.storage
.put_object(&s3_key, file_bytes.clone(), content_type.clone())
.await
.map_err(|err| AppError::internal(format!("failed to upload to s3: {err}")))?;
.map_err(|err| AppError::internal(format!("failed to store document: {err}")))?;
let metadata_value = if metadata.is_null() {
Value::Object(Default::default())
@@ -349,22 +337,17 @@ pub async fn download_document(
.filter(document_versions::version_number.eq(doc.current_version))
.first(&mut conn)?;
let presign_config = PresigningConfig::builder()
.expires_in(Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS))
.build()
.map_err(|err| AppError::internal(format!("failed to build presigning config: {err}")))?;
let presigned = state
.s3
.get_object()
.bucket(&state.config.s3_bucket)
.key(&version.s3_key)
.presigned(presign_config)
let presigned_url = state
.storage
.presign_get_object(
&version.s3_key,
Duration::from_secs(PRESIGNED_URL_EXPIRY_SECONDS),
)
.await
.map_err(|err| AppError::internal(format!("failed to generate download URL: {err}")))?;
Ok(Json(DocumentDownloadResponse {
url: presigned.uri().to_string(),
url: presigned_url,
expires_in: PRESIGNED_URL_EXPIRY_SECONDS,
filename: doc.original_name.clone(),
content_type: doc.content_type.clone(),
-2
View File
@@ -16,8 +16,6 @@ diesel::table! {
version_number -> Int4,
#[max_length = 500]
s3_key -> Varchar,
#[max_length = 100]
s3_bucket -> Varchar,
size_bytes -> Int8,
#[max_length = 64]
checksum -> Varchar,
+9 -4
View File
@@ -1,6 +1,5 @@
use std::sync::Arc;
use aws_sdk_s3::Client as S3Client;
use diesel::{
pg::PgConnection,
r2d2::{ConnectionManager, PooledConnection},
@@ -11,6 +10,7 @@ use crate::{
config::AppConfig,
db::PgPool,
error::{AppError, AppResult},
storage::ObjectStorage,
};
type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
@@ -19,16 +19,21 @@ type PgPooledConnection = PooledConnection<ConnectionManager<PgConnection>>;
pub struct AppState {
pub pool: PgPool,
pub config: Arc<AppConfig>,
pub s3: S3Client,
pub storage: Arc<dyn ObjectStorage>,
pub jwt: JwtService,
}
impl AppState {
pub fn new(pool: PgPool, config: AppConfig, s3: S3Client, jwt: JwtService) -> Self {
pub fn new(
pool: PgPool,
config: AppConfig,
storage: Arc<dyn ObjectStorage>,
jwt: JwtService,
) -> Self {
Self {
pool,
config: Arc::new(config),
s3,
storage,
jwt,
}
}
+79
View File
@@ -0,0 +1,79 @@
use std::time::Duration;
use anyhow::{Context, Result};
use async_trait::async_trait;
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::Client as S3Client;
#[async_trait]
pub trait ObjectStorage: Send + Sync + 'static {
async fn put_object(
&self,
key: &str,
bytes: Vec<u8>,
content_type: Option<String>,
) -> Result<()>;
async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result<String>;
}
pub struct S3Storage {
client: S3Client,
bucket: String,
}
impl S3Storage {
pub fn new(client: S3Client, bucket: impl Into<String>) -> Self {
Self {
client,
bucket: bucket.into(),
}
}
}
#[async_trait]
impl ObjectStorage for S3Storage {
async fn put_object(
&self,
key: &str,
bytes: Vec<u8>,
content_type: Option<String>,
) -> Result<()> {
let mut request = self
.client
.put_object()
.bucket(&self.bucket)
.key(key)
.body(ByteStream::from(bytes));
if let Some(content_type) = content_type {
request = request.content_type(content_type);
}
request
.send()
.await
.context("failed to upload object to S3")?;
Ok(())
}
async fn presign_get_object(&self, key: &str, expires_in: Duration) -> Result<String> {
let presign_config = PresigningConfig::builder()
.expires_in(expires_in)
.build()
.context("failed to build S3 presigning config")?;
let presigned = self
.client
.get_object()
.bucket(&self.bucket)
.key(key)
.presigned(presign_config)
.await
.context("failed to generate presigned download URL")?;
Ok(presigned.uri().to_string())
}
}