feat(server): improve settings page and settings
This commit is contained in:
parent
4155be9f00
commit
3ba0dd1c14
|
|
@ -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
118
src/db.rs
|
|
@ -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(¤t.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(¤t.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(¤t.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"),
|
||||
})
|
||||
|
|
|
|||
10
src/main.rs
10
src/main.rs
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue