diff --git a/backend/Dockerfile b/backend/Dockerfile index 1b407d8..1aeced2 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -11,6 +11,7 @@ RUN apt-get update \ libpq-dev \ libjpeg-dev \ libpng-dev \ + curl \ && rm -rf /var/lib/apt/lists/* COPY Cargo.toml Cargo.lock ./ @@ -28,11 +29,25 @@ WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends \ ca-certificates \ + curl \ libssl3 \ libpq5 \ libjpeg62-turbo \ libpng16-16 \ + ocrmypdf \ + tesseract-ocr \ + ghostscript \ + qpdf \ && rm -rf /var/lib/apt/lists/* \ + && mkdir -p /usr/local/lib \ + && curl -fsSL https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-arm64.tgz -o /tmp/pdfium.tgz \ + && mkdir -p /tmp/pdfium \ + && tar -xzf /tmp/pdfium.tgz -C /tmp/pdfium --strip-components=1 \ + && pdfium_so="$(find /tmp/pdfium -name libpdfium.so -type f | head -n1)" \ + && [ -n "${pdfium_so}" ] \ + && mv "${pdfium_so}" /usr/local/lib/libpdfium.so \ + && ldconfig \ + && rm -rf /tmp/pdfium.tgz /tmp/pdfium \ && useradd --system --create-home --uid 10001 appuser COPY --from=builder /app/target/release/backend /usr/local/bin/papercrate-backend diff --git a/backend/src/routes/documents.rs b/backend/src/routes/documents.rs index 0f47ced..306eaba 100644 --- a/backend/src/routes/documents.rs +++ b/backend/src/routes/documents.rs @@ -145,6 +145,11 @@ pub struct BulkMoveRequest { pub folder_id: Option, } +#[derive(Deserialize)] +pub struct UpdateDocumentRequest { + pub title: Option, +} + #[derive(Serialize)] pub struct BulkMoveResponse { pub updated: usize, @@ -627,6 +632,70 @@ pub async fn delete_document( Ok(StatusCode::NO_CONTENT) } +pub async fn update_document( + State(state): State, + Path(document_id): Path, + user: AuthenticatedUser, + Json(payload): Json, +) -> AppResult> { + let mut conn = state.db()?; + + let mut document: Document = documents::table.find(document_id).first(&mut conn)?; + if document.deleted_at.is_some() { + return Err(AppError::not_found()); + } + + let new_title = match payload.title { + Some(ref title) => { + let trimmed = title.trim(); + if trimmed.is_empty() { + return Err(AppError::bad_request("title must not be empty")); + } + Some(trimmed.to_string()) + } + None => None, + }; + + if new_title.is_none() { + return Err(AppError::bad_request("no changes provided")); + } + + if let Some(title) = new_title { + let now = Utc::now().naive_utc(); + diesel::update(documents::table.find(document_id)) + .set((documents::title.eq(title), documents::updated_at.eq(now))) + .execute(&mut conn)?; + document = documents::table.find(document_id).first(&mut conn)?; + } + + let current_version: DocumentVersion = document_versions::table + .filter(document_versions::document_id.eq(document_id)) + .filter(document_versions::version_number.eq(document.current_version)) + .first(&mut conn)?; + + let tags_map = load_tags_for_documents(&mut conn, &[document_id])?; + let version_id = current_version.id; + drop(conn); + + let assets = load_asset_responses(&state, version_id).await?; + let thumbnail = assets + .iter() + .find(|asset| asset.asset_type == "thumbnail") + .cloned(); + + Ok(Json(DocumentDetailResponse { + document: to_document_response( + &state, + user.user_id, + document, + tags_map.get(&document_id).cloned(), + thumbnail, + )?, + current_version: to_version_response(current_version), + assets, + })) +} + pub async fn move_document( State(state): State, Path(document_id): Path, diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 1a0ec60..95f0643 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -64,7 +64,9 @@ pub fn create_router(state: AppState) -> Router<()> { ) .route( "/:id", - get(documents::get_document).delete(documents::delete_document), + get(documents::get_document) + .delete(documents::delete_document) + .patch(documents::update_document), ) .route("/:id/download", get(documents::download_document)) .route( diff --git a/backend/tests/common/mod.rs b/backend/tests/common/mod.rs index efd9a8b..e861821 100644 --- a/backend/tests/common/mod.rs +++ b/backend/tests/common/mod.rs @@ -8,12 +8,6 @@ use async_trait::async_trait; use axum::body::Body; use axum::http::{Method, Request, StatusCode}; use axum::Router; -use diesel::connection::SimpleConnection; -use diesel::prelude::*; -use diesel::PgConnection; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use http_body_util::BodyExt; -use once_cell::sync::Lazy; use backend::auth::jwt::JwtService; use backend::config::AppConfig; use backend::db::{self, PgPool}; @@ -21,6 +15,12 @@ use backend::models::{Job, NewUser}; use backend::routes; use backend::state::AppState; use backend::storage::ObjectStorage; +use diesel::connection::SimpleConnection; +use diesel::prelude::*; +use diesel::PgConnection; +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; +use http_body_util::BodyExt; +use once_cell::sync::Lazy; use rand::rngs::OsRng; use serde::Serialize; use tokio::sync::Mutex; @@ -238,9 +238,7 @@ impl TestApp { pub async fn jobs_by_type(&self, ty: &str) -> Result> { let ty = ty.to_string(); self.with_conn(move |conn| { - use backend::schema::jobs::dsl::{ - job_type as job_type_col, jobs as jobs_table, - }; + use backend::schema::jobs::dsl::{job_type as job_type_col, jobs as jobs_table}; let rows = jobs_table .filter(job_type_col.eq(&ty)) .load::(conn)