feat(server): improve settings page and settings

This commit is contained in:
perf3ct 2025-06-12 18:49:08 +00:00
parent 4155be9f00
commit 3ba0dd1c14
7 changed files with 558 additions and 42 deletions

View File

@ -7,6 +7,7 @@ import {
Tabs,
Tab,
FormControl,
FormControlLabel,
InputLabel,
Select,
MenuItem,
@ -29,6 +30,7 @@ import {
Card,
CardContent,
Divider,
Switch,
} from '@mui/material';
import { Edit as EditIcon, Delete as DeleteIcon, Add as AddIcon } from '@mui/icons-material';
import { useAuth } from '../contexts/AuthContext';
@ -39,6 +41,21 @@ const SettingsPage = () => {
const [tabValue, setTabValue] = useState(0);
const [settings, setSettings] = useState({
ocrLanguage: 'eng',
concurrentOcrJobs: 4,
ocrTimeoutSeconds: 300,
maxFileSizeMb: 50,
allowedFileTypes: ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'],
autoRotateImages: true,
enableImagePreprocessing: true,
searchResultsPerPage: 25,
searchSnippetLength: 200,
fuzzySearchThreshold: 0.8,
retentionDays: null,
enableAutoCleanup: false,
enableCompression: false,
memoryLimitMb: 512,
cpuPriority: 'normal',
enableBackgroundOcr: true,
});
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
@ -72,7 +89,24 @@ const SettingsPage = () => {
const fetchSettings = async () => {
try {
const response = await api.get('/settings');
setSettings(response.data);
setSettings({
ocrLanguage: response.data.ocr_language || 'eng',
concurrentOcrJobs: response.data.concurrent_ocr_jobs || 4,
ocrTimeoutSeconds: response.data.ocr_timeout_seconds || 300,
maxFileSizeMb: response.data.max_file_size_mb || 50,
allowedFileTypes: response.data.allowed_file_types || ['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'],
autoRotateImages: response.data.auto_rotate_images !== undefined ? response.data.auto_rotate_images : true,
enableImagePreprocessing: response.data.enable_image_preprocessing !== undefined ? response.data.enable_image_preprocessing : true,
searchResultsPerPage: response.data.search_results_per_page || 25,
searchSnippetLength: response.data.search_snippet_length || 200,
fuzzySearchThreshold: response.data.fuzzy_search_threshold || 0.8,
retentionDays: response.data.retention_days,
enableAutoCleanup: response.data.enable_auto_cleanup || false,
enableCompression: response.data.enable_compression || false,
memoryLimitMb: response.data.memory_limit_mb || 512,
cpuPriority: response.data.cpu_priority || 'normal',
enableBackgroundOcr: response.data.enable_background_ocr !== undefined ? response.data.enable_background_ocr : true,
});
} catch (error) {
console.error('Error fetching settings:', error);
if (error.response?.status !== 404) {
@ -96,7 +130,14 @@ const SettingsPage = () => {
const handleSettingsChange = async (key, value) => {
setLoading(true);
try {
await api.put('/settings', { ...settings, [key]: value });
// Convert camelCase to snake_case for API
const snakeCase = (str) => str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
const apiKey = snakeCase(key);
// Build the update payload with only the changed field
const updatePayload = { [apiKey]: value };
await api.put('/settings', updatePayload);
setSettings({ ...settings, [key]: value });
showSnackbar('Settings updated successfully', 'success');
} catch (error) {
@ -199,24 +240,258 @@ const SettingsPage = () => {
OCR Configuration
</Typography>
<Divider sx={{ mb: 2 }} />
<FormControl fullWidth>
<InputLabel>OCR Language</InputLabel>
<Select
value={settings.ocrLanguage}
label="OCR Language"
onChange={(e) => handleSettingsChange('ocrLanguage', e.target.value)}
disabled={loading}
>
{ocrLanguages.map((lang) => (
<MenuItem key={lang.code} value={lang.code}>
{lang.name}
</MenuItem>
))}
</Select>
</FormControl>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
Select the primary language for OCR text extraction. This affects how accurately text is recognized from images and scanned documents.
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>OCR Language</InputLabel>
<Select
value={settings.ocrLanguage}
label="OCR Language"
onChange={(e) => handleSettingsChange('ocrLanguage', e.target.value)}
disabled={loading}
>
{ocrLanguages.map((lang) => (
<MenuItem key={lang.code} value={lang.code}>
{lang.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Concurrent OCR Jobs"
value={settings.concurrentOcrJobs}
onChange={(e) => handleSettingsChange('concurrentOcrJobs', parseInt(e.target.value))}
disabled={loading}
inputProps={{ min: 1, max: 16 }}
helperText="Number of OCR jobs that can run simultaneously"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="OCR Timeout (seconds)"
value={settings.ocrTimeoutSeconds}
onChange={(e) => handleSettingsChange('ocrTimeoutSeconds', parseInt(e.target.value))}
disabled={loading}
inputProps={{ min: 30, max: 3600 }}
helperText="Maximum time for OCR processing per file"
/>
</Grid>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>CPU Priority</InputLabel>
<Select
value={settings.cpuPriority}
label="CPU Priority"
onChange={(e) => handleSettingsChange('cpuPriority', e.target.value)}
disabled={loading}
>
<MenuItem value="low">Low</MenuItem>
<MenuItem value="normal">Normal</MenuItem>
<MenuItem value="high">High</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
File Processing
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Max File Size (MB)"
value={settings.maxFileSizeMb}
onChange={(e) => handleSettingsChange('maxFileSizeMb', parseInt(e.target.value))}
disabled={loading}
inputProps={{ min: 1, max: 500 }}
helperText="Maximum allowed file size for uploads"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Memory Limit (MB)"
value={settings.memoryLimitMb}
onChange={(e) => handleSettingsChange('memoryLimitMb', parseInt(e.target.value))}
disabled={loading}
inputProps={{ min: 128, max: 4096 }}
helperText="Memory limit per OCR job"
/>
</Grid>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.autoRotateImages}
onChange={(e) => handleSettingsChange('autoRotateImages', e.target.checked)}
disabled={loading}
/>
}
label="Auto-rotate Images"
/>
<Typography variant="body2" color="text.secondary">
Automatically detect and correct image orientation
</Typography>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.enableImagePreprocessing}
onChange={(e) => handleSettingsChange('enableImagePreprocessing', e.target.checked)}
disabled={loading}
/>
}
label="Enable Image Preprocessing"
/>
<Typography variant="body2" color="text.secondary">
Enhance images for better OCR accuracy (deskew, denoise, contrast)
</Typography>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.enableBackgroundOcr}
onChange={(e) => handleSettingsChange('enableBackgroundOcr', e.target.checked)}
disabled={loading}
/>
}
label="Enable Background OCR"
/>
<Typography variant="body2" color="text.secondary">
Process OCR in the background after file upload
</Typography>
</FormControl>
</Grid>
</Grid>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
Search Configuration
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<FormControl fullWidth>
<InputLabel>Results Per Page</InputLabel>
<Select
value={settings.searchResultsPerPage}
label="Results Per Page"
onChange={(e) => handleSettingsChange('searchResultsPerPage', parseInt(e.target.value))}
disabled={loading}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Snippet Length"
value={settings.searchSnippetLength}
onChange={(e) => handleSettingsChange('searchSnippetLength', parseInt(e.target.value))}
disabled={loading}
inputProps={{ min: 50, max: 500 }}
helperText="Characters to show in search result previews"
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Fuzzy Search Threshold"
value={settings.fuzzySearchThreshold}
onChange={(e) => handleSettingsChange('fuzzySearchThreshold', parseFloat(e.target.value))}
disabled={loading}
inputProps={{ min: 0, max: 1, step: 0.1 }}
helperText="Tolerance for spelling mistakes (0.0-1.0)"
/>
</Grid>
</Grid>
</CardContent>
</Card>
<Card sx={{ mb: 3 }}>
<CardContent>
<Typography variant="subtitle1" sx={{ mb: 2 }}>
Storage Management
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<TextField
fullWidth
type="number"
label="Retention Days"
value={settings.retentionDays || ''}
onChange={(e) => handleSettingsChange('retentionDays', e.target.value ? parseInt(e.target.value) : null)}
disabled={loading}
inputProps={{ min: 1 }}
helperText="Auto-delete documents after X days (leave empty to disable)"
/>
</Grid>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.enableAutoCleanup}
onChange={(e) => handleSettingsChange('enableAutoCleanup', e.target.checked)}
disabled={loading}
/>
}
label="Enable Auto Cleanup"
/>
<Typography variant="body2" color="text.secondary">
Automatically remove orphaned files and clean up storage
</Typography>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.enableCompression}
onChange={(e) => handleSettingsChange('enableCompression', e.target.checked)}
disabled={loading}
/>
}
label="Enable Compression"
/>
<Typography variant="body2" color="text.secondary">
Compress stored documents to save disk space
</Typography>
</FormControl>
</Grid>
</Grid>
</CardContent>
</Card>
</Box>

118
src/db.rs
View File

@ -92,6 +92,21 @@ impl Database {
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
ocr_language VARCHAR(10) DEFAULT 'eng',
concurrent_ocr_jobs INT DEFAULT 4,
ocr_timeout_seconds INT DEFAULT 300,
max_file_size_mb INT DEFAULT 50,
allowed_file_types TEXT[] DEFAULT ARRAY['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt'],
auto_rotate_images BOOLEAN DEFAULT TRUE,
enable_image_preprocessing BOOLEAN DEFAULT TRUE,
search_results_per_page INT DEFAULT 25,
search_snippet_length INT DEFAULT 200,
fuzzy_search_threshold REAL DEFAULT 0.8,
retention_days INT,
enable_auto_cleanup BOOLEAN DEFAULT FALSE,
enable_compression BOOLEAN DEFAULT FALSE,
memory_limit_mb INT DEFAULT 512,
cpu_priority VARCHAR(10) DEFAULT 'normal',
enable_background_ocr BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
)
@ -569,7 +584,12 @@ impl Database {
pub async fn get_user_settings(&self, user_id: Uuid) -> Result<Option<crate::models::Settings>> {
let row = sqlx::query(
"SELECT id, user_id, ocr_language, created_at, updated_at FROM settings WHERE user_id = $1"
r#"SELECT id, user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds,
max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing,
search_results_per_page, search_snippet_length, fuzzy_search_threshold,
retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb,
cpu_priority, enable_background_ocr, created_at, updated_at
FROM settings WHERE user_id = $1"#
)
.bind(user_id)
.fetch_optional(&self.pool)
@ -580,6 +600,21 @@ impl Database {
id: row.get("id"),
user_id: row.get("user_id"),
ocr_language: row.get("ocr_language"),
concurrent_ocr_jobs: row.get("concurrent_ocr_jobs"),
ocr_timeout_seconds: row.get("ocr_timeout_seconds"),
max_file_size_mb: row.get("max_file_size_mb"),
allowed_file_types: row.get("allowed_file_types"),
auto_rotate_images: row.get("auto_rotate_images"),
enable_image_preprocessing: row.get("enable_image_preprocessing"),
search_results_per_page: row.get("search_results_per_page"),
search_snippet_length: row.get("search_snippet_length"),
fuzzy_search_threshold: row.get("fuzzy_search_threshold"),
retention_days: row.get("retention_days"),
enable_auto_cleanup: row.get("enable_auto_cleanup"),
enable_compression: row.get("enable_compression"),
memory_limit_mb: row.get("memory_limit_mb"),
cpu_priority: row.get("cpu_priority"),
enable_background_ocr: row.get("enable_background_ocr"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})),
@ -587,18 +622,70 @@ impl Database {
}
}
pub async fn create_or_update_settings(&self, user_id: Uuid, ocr_language: &str) -> Result<crate::models::Settings> {
pub async fn create_or_update_settings(&self, user_id: Uuid, settings: &crate::models::UpdateSettings) -> Result<crate::models::Settings> {
// Get existing settings to merge with updates
let existing = self.get_user_settings(user_id).await?;
let defaults = crate::models::Settings::default();
// Merge existing/defaults with updates
let current = existing.unwrap_or_else(|| {
let mut s = defaults;
s.user_id = user_id;
s
});
let row = sqlx::query(
r#"
INSERT INTO settings (user_id, ocr_language)
VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE
SET ocr_language = $2, updated_at = NOW()
RETURNING id, user_id, ocr_language, created_at, updated_at
INSERT INTO settings (
user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds,
max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing,
search_results_per_page, search_snippet_length, fuzzy_search_threshold,
retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb,
cpu_priority, enable_background_ocr
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
ON CONFLICT (user_id) DO UPDATE SET
ocr_language = $2,
concurrent_ocr_jobs = $3,
ocr_timeout_seconds = $4,
max_file_size_mb = $5,
allowed_file_types = $6,
auto_rotate_images = $7,
enable_image_preprocessing = $8,
search_results_per_page = $9,
search_snippet_length = $10,
fuzzy_search_threshold = $11,
retention_days = $12,
enable_auto_cleanup = $13,
enable_compression = $14,
memory_limit_mb = $15,
cpu_priority = $16,
enable_background_ocr = $17,
updated_at = NOW()
RETURNING id, user_id, ocr_language, concurrent_ocr_jobs, ocr_timeout_seconds,
max_file_size_mb, allowed_file_types, auto_rotate_images, enable_image_preprocessing,
search_results_per_page, search_snippet_length, fuzzy_search_threshold,
retention_days, enable_auto_cleanup, enable_compression, memory_limit_mb,
cpu_priority, enable_background_ocr, created_at, updated_at
"#
)
.bind(user_id)
.bind(ocr_language)
.bind(settings.ocr_language.as_ref().unwrap_or(&current.ocr_language))
.bind(settings.concurrent_ocr_jobs.unwrap_or(current.concurrent_ocr_jobs))
.bind(settings.ocr_timeout_seconds.unwrap_or(current.ocr_timeout_seconds))
.bind(settings.max_file_size_mb.unwrap_or(current.max_file_size_mb))
.bind(settings.allowed_file_types.as_ref().unwrap_or(&current.allowed_file_types))
.bind(settings.auto_rotate_images.unwrap_or(current.auto_rotate_images))
.bind(settings.enable_image_preprocessing.unwrap_or(current.enable_image_preprocessing))
.bind(settings.search_results_per_page.unwrap_or(current.search_results_per_page))
.bind(settings.search_snippet_length.unwrap_or(current.search_snippet_length))
.bind(settings.fuzzy_search_threshold.unwrap_or(current.fuzzy_search_threshold))
.bind(settings.retention_days.unwrap_or(current.retention_days))
.bind(settings.enable_auto_cleanup.unwrap_or(current.enable_auto_cleanup))
.bind(settings.enable_compression.unwrap_or(current.enable_compression))
.bind(settings.memory_limit_mb.unwrap_or(current.memory_limit_mb))
.bind(settings.cpu_priority.as_ref().unwrap_or(&current.cpu_priority))
.bind(settings.enable_background_ocr.unwrap_or(current.enable_background_ocr))
.fetch_one(&self.pool)
.await?;
@ -606,6 +693,21 @@ impl Database {
id: row.get("id"),
user_id: row.get("user_id"),
ocr_language: row.get("ocr_language"),
concurrent_ocr_jobs: row.get("concurrent_ocr_jobs"),
ocr_timeout_seconds: row.get("ocr_timeout_seconds"),
max_file_size_mb: row.get("max_file_size_mb"),
allowed_file_types: row.get("allowed_file_types"),
auto_rotate_images: row.get("auto_rotate_images"),
enable_image_preprocessing: row.get("enable_image_preprocessing"),
search_results_per_page: row.get("search_results_per_page"),
search_snippet_length: row.get("search_snippet_length"),
fuzzy_search_threshold: row.get("fuzzy_search_threshold"),
retention_days: row.get("retention_days"),
enable_auto_cleanup: row.get("enable_auto_cleanup"),
enable_compression: row.get("enable_compression"),
memory_limit_mb: row.get("memory_limit_mb"),
cpu_priority: row.get("cpu_priority"),
enable_background_ocr: row.get("enable_background_ocr"),
created_at: row.get("created_at"),
updated_at: row.get("updated_at"),
})

View File

@ -1,6 +1,6 @@
use axum::{
http::StatusCode,
response::Json,
response::{Json, Html},
routing::get,
Router,
};
@ -52,6 +52,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.nest("/api/settings", routes::settings::router())
.nest("/api/users", routes::users::router())
.nest_service("/", ServeDir::new("/app/frontend"))
.fallback(serve_spa)
.layer(CorsLayer::permissive())
.with_state(Arc::new(state));
@ -73,3 +74,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn health_check() -> Result<Json<serde_json::Value>, StatusCode> {
Ok(Json(serde_json::json!({"status": "ok"})))
}
async fn serve_spa() -> Result<Html<String>, StatusCode> {
match tokio::fs::read_to_string("/app/frontend/index.html").await {
Ok(html) => Ok(Html(html)),
Err(_) => Err(StatusCode::NOT_FOUND),
}
}

View File

@ -170,6 +170,21 @@ pub struct Settings {
pub id: Uuid,
pub user_id: Uuid,
pub ocr_language: String,
pub concurrent_ocr_jobs: i32,
pub ocr_timeout_seconds: i32,
pub max_file_size_mb: i32,
pub allowed_file_types: Vec<String>,
pub auto_rotate_images: bool,
pub enable_image_preprocessing: bool,
pub search_results_per_page: i32,
pub search_snippet_length: i32,
pub fuzzy_search_threshold: f32,
pub retention_days: Option<i32>,
pub enable_auto_cleanup: bool,
pub enable_compression: bool,
pub memory_limit_mb: i32,
pub cpu_priority: String,
pub enable_background_ocr: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@ -177,17 +192,97 @@ pub struct Settings {
#[derive(Debug, Serialize, Deserialize)]
pub struct SettingsResponse {
pub ocr_language: String,
pub concurrent_ocr_jobs: i32,
pub ocr_timeout_seconds: i32,
pub max_file_size_mb: i32,
pub allowed_file_types: Vec<String>,
pub auto_rotate_images: bool,
pub enable_image_preprocessing: bool,
pub search_results_per_page: i32,
pub search_snippet_length: i32,
pub fuzzy_search_threshold: f32,
pub retention_days: Option<i32>,
pub enable_auto_cleanup: bool,
pub enable_compression: bool,
pub memory_limit_mb: i32,
pub cpu_priority: String,
pub enable_background_ocr: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateSettings {
pub ocr_language: String,
pub ocr_language: Option<String>,
pub concurrent_ocr_jobs: Option<i32>,
pub ocr_timeout_seconds: Option<i32>,
pub max_file_size_mb: Option<i32>,
pub allowed_file_types: Option<Vec<String>>,
pub auto_rotate_images: Option<bool>,
pub enable_image_preprocessing: Option<bool>,
pub search_results_per_page: Option<i32>,
pub search_snippet_length: Option<i32>,
pub fuzzy_search_threshold: Option<f32>,
pub retention_days: Option<Option<i32>>,
pub enable_auto_cleanup: Option<bool>,
pub enable_compression: Option<bool>,
pub memory_limit_mb: Option<i32>,
pub cpu_priority: Option<String>,
pub enable_background_ocr: Option<bool>,
}
impl From<Settings> for SettingsResponse {
fn from(settings: Settings) -> Self {
Self {
ocr_language: settings.ocr_language,
concurrent_ocr_jobs: settings.concurrent_ocr_jobs,
ocr_timeout_seconds: settings.ocr_timeout_seconds,
max_file_size_mb: settings.max_file_size_mb,
allowed_file_types: settings.allowed_file_types,
auto_rotate_images: settings.auto_rotate_images,
enable_image_preprocessing: settings.enable_image_preprocessing,
search_results_per_page: settings.search_results_per_page,
search_snippet_length: settings.search_snippet_length,
fuzzy_search_threshold: settings.fuzzy_search_threshold,
retention_days: settings.retention_days,
enable_auto_cleanup: settings.enable_auto_cleanup,
enable_compression: settings.enable_compression,
memory_limit_mb: settings.memory_limit_mb,
cpu_priority: settings.cpu_priority,
enable_background_ocr: settings.enable_background_ocr,
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
id: Uuid::new_v4(),
user_id: Uuid::nil(),
ocr_language: "eng".to_string(),
concurrent_ocr_jobs: 4,
ocr_timeout_seconds: 300,
max_file_size_mb: 50,
allowed_file_types: vec![
"pdf".to_string(),
"png".to_string(),
"jpg".to_string(),
"jpeg".to_string(),
"tiff".to_string(),
"bmp".to_string(),
"txt".to_string(),
],
auto_rotate_images: true,
enable_image_preprocessing: true,
search_results_per_page: 25,
search_snippet_length: 200,
fuzzy_search_threshold: 0.8,
retention_days: None,
enable_auto_cleanup: false,
enable_compression: false,
memory_limit_mb: 512,
cpu_priority: "normal".to_string(),
enable_background_ocr: true,
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
}

View File

@ -37,6 +37,14 @@ async fn upload_document(
) -> Result<Json<DocumentResponse>, StatusCode> {
let file_service = FileService::new(state.config.upload_path.clone());
// Get user settings for file upload restrictions
let settings = state
.db
.get_user_settings(auth_user.user.id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.unwrap_or_else(|| crate::models::Settings::default());
while let Some(field) = multipart.next_field().await.map_err(|_| StatusCode::BAD_REQUEST)? {
let name = field.name().unwrap_or("").to_string();
@ -46,13 +54,19 @@ async fn upload_document(
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
if !file_service.is_allowed_file_type(&filename, &state.config.allowed_file_types) {
if !file_service.is_allowed_file_type(&filename, &settings.allowed_file_types) {
return Err(StatusCode::BAD_REQUEST);
}
let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
let file_size = data.len() as i64;
// Check file size limit
let max_size_bytes = (settings.max_file_size_mb as i64) * 1024 * 1024;
if file_size > max_size_bytes {
return Err(StatusCode::PAYLOAD_TOO_LARGE);
}
let mime_type = mime_guess::from_path(&filename)
.first_or_octet_stream()
.to_string();
@ -81,15 +95,19 @@ async fn upload_document(
let db_clone = state.db.clone();
let file_path_clone = file_path.clone();
let mime_type_clone = mime_type.clone();
let ocr_language = settings.ocr_language.clone();
let enable_background_ocr = settings.enable_background_ocr;
spawn(async move {
let ocr_service = OcrService::new();
if let Ok(text) = ocr_service.extract_text(&file_path_clone, &mime_type_clone).await {
if !text.is_empty() {
let _ = db_clone.update_document_ocr(document_id, &text).await;
if enable_background_ocr {
spawn(async move {
let ocr_service = OcrService::new();
if let Ok(text) = ocr_service.extract_text_with_lang(&file_path_clone, &mime_type_clone, &ocr_language).await {
if !text.is_empty() {
let _ = db_clone.update_document_ocr(document_id, &text).await;
}
}
}
});
});
}
return Ok(Json(saved_document.into()));
}

View File

@ -2,7 +2,7 @@ use axum::{
extract::State,
http::StatusCode,
response::Json,
routing::{get, put},
routing::get,
Router,
};
use std::sync::Arc;
@ -30,8 +30,26 @@ async fn get_settings(
let response = match settings {
Some(s) => s.into(),
None => SettingsResponse {
ocr_language: "eng".to_string(),
None => {
let default = crate::models::Settings::default();
SettingsResponse {
ocr_language: default.ocr_language,
concurrent_ocr_jobs: default.concurrent_ocr_jobs,
ocr_timeout_seconds: default.ocr_timeout_seconds,
max_file_size_mb: default.max_file_size_mb,
allowed_file_types: default.allowed_file_types,
auto_rotate_images: default.auto_rotate_images,
enable_image_preprocessing: default.enable_image_preprocessing,
search_results_per_page: default.search_results_per_page,
search_snippet_length: default.search_snippet_length,
fuzzy_search_threshold: default.fuzzy_search_threshold,
retention_days: default.retention_days,
enable_auto_cleanup: default.enable_auto_cleanup,
enable_compression: default.enable_compression,
memory_limit_mb: default.memory_limit_mb,
cpu_priority: default.cpu_priority,
enable_background_ocr: default.enable_background_ocr,
}
},
};
@ -45,7 +63,7 @@ async fn update_settings(
) -> Result<Json<SettingsResponse>, StatusCode> {
let settings = state
.db
.create_or_update_settings(auth_user.user.id, &update_data.ocr_language)
.create_or_update_settings(auth_user.user.id, &update_data)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;

View File

@ -2,7 +2,7 @@ use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{delete, get, post, put},
routing::get,
Router,
};
use std::sync::Arc;