file size limits, keep active filters in session

This commit is contained in:
Stefan Hardegger
2025-10-30 13:11:40 +01:00
parent 924ae12b5b
commit a3bc83db8a
7 changed files with 112 additions and 25 deletions

View File

@@ -354,14 +354,24 @@ public class DatabaseManagementService implements ApplicationContextAware {
Path tempBackupFile = Files.createTempFile("storycove_restore_", ".sql"); Path tempBackupFile = Files.createTempFile("storycove_restore_", ".sql");
try { try {
// Write backup stream to temporary file // Write backup stream to temporary file, filtering out incompatible commands
System.err.println("Writing backup data to temporary file..."); System.err.println("Writing backup data to temporary file...");
try (InputStream input = backupStream; try (InputStream input = backupStream;
OutputStream output = Files.newOutputStream(tempBackupFile)) { BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
byte[] buffer = new byte[8192]; BufferedWriter writer = Files.newBufferedWriter(tempBackupFile, StandardCharsets.UTF_8)) {
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) { String line;
output.write(buffer, 0, bytesRead); while ((line = reader.readLine()) != null) {
// Skip DROP DATABASE and CREATE DATABASE commands - we're already connected to the DB
// Also skip database connection commands as we're already connected
if (line.trim().startsWith("DROP DATABASE") ||
line.trim().startsWith("CREATE DATABASE") ||
line.trim().startsWith("\\connect")) {
System.err.println("Skipping incompatible command: " + line.substring(0, Math.min(50, line.length())));
continue;
}
writer.write(line);
writer.newLine();
} }
} }

View File

@@ -21,8 +21,8 @@ spring:
servlet: servlet:
multipart: multipart:
max-file-size: 600MB # Increased for large backup restore (425MB+) max-file-size: 2048MB # 2GB for large backup restore
max-request-size: 610MB # Slightly higher to account for form data max-request-size: 2100MB # Slightly higher to account for form data
jackson: jackson:
serialization: serialization:
@@ -33,7 +33,7 @@ spring:
server: server:
port: 8080 port: 8080
tomcat: tomcat:
max-http-request-size: 650MB # Tomcat HTTP request size limit (separate from multipart) max-http-request-size: 2150MB # Tomcat HTTP request size limit (2GB + overhead)
storycove: storycove:
app: app:

View File

@@ -124,7 +124,7 @@ configs:
} }
server { server {
listen 80; listen 80;
client_max_body_size 600M; client_max_body_size 2048M;
location / { location / {
proxy_pass http://frontend; proxy_pass http://frontend;
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -145,8 +145,8 @@ configs:
proxy_connect_timeout 900s; proxy_connect_timeout 900s;
proxy_send_timeout 900s; proxy_send_timeout 900s;
proxy_read_timeout 900s; proxy_read_timeout 900s;
# Large upload settings # Large upload settings (2GB for backups)
client_max_body_size 600M; client_max_body_size 2048M;
proxy_request_buffering off; proxy_request_buffering off;
proxy_max_temp_file_size 0; proxy_max_temp_file_size 0;
} }

View File

@@ -13,6 +13,7 @@ import SidebarLayout from '../../components/library/SidebarLayout';
import ToolbarLayout from '../../components/library/ToolbarLayout'; import ToolbarLayout from '../../components/library/ToolbarLayout';
import MinimalLayout from '../../components/library/MinimalLayout'; import MinimalLayout from '../../components/library/MinimalLayout';
import { useLibraryLayout } from '../../hooks/useLibraryLayout'; import { useLibraryLayout } from '../../hooks/useLibraryLayout';
import { useLibraryFilters, clearLibraryFilters } from '../../hooks/useLibraryFilters';
type ViewMode = 'grid' | 'list'; type ViewMode = 'grid' | 'list';
type SortOption = 'createdAt' | 'title' | 'authorName' | 'rating' | 'wordCount' | 'lastReadAt'; type SortOption = 'createdAt' | 'title' | 'authorName' | 'rating' | 'wordCount' | 'lastReadAt';
@@ -26,17 +27,21 @@ export default function LibraryContent() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchLoading, setSearchLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false);
const [randomLoading, setRandomLoading] = useState(false); const [randomLoading, setRandomLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTags, setSelectedTags] = useState<string[]>([]); // Persisted filter state (survives navigation within session)
const [viewMode, setViewMode] = useState<ViewMode>('list'); const [searchQuery, setSearchQuery] = useLibraryFilters<string>('searchQuery', '');
const [sortOption, setSortOption] = useState<SortOption>('lastReadAt'); const [selectedTags, setSelectedTags] = useLibraryFilters<string[]>('selectedTags', []);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [viewMode, setViewMode] = useLibraryFilters<ViewMode>('viewMode', 'list');
const [sortOption, setSortOption] = useLibraryFilters<SortOption>('sortOption', 'lastReadAt');
const [sortDirection, setSortDirection] = useLibraryFilters<'asc' | 'desc'>('sortDirection', 'desc');
const [advancedFilters, setAdvancedFilters] = useLibraryFilters<AdvancedFilters>('advancedFilters', {});
// Non-persisted state (resets on navigation)
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [totalPages, setTotalPages] = useState(1); const [totalPages, setTotalPages] = useState(1);
const [totalElements, setTotalElements] = useState(0); const [totalElements, setTotalElements] = useState(0);
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [urlParamsProcessed, setUrlParamsProcessed] = useState(false); const [urlParamsProcessed, setUrlParamsProcessed] = useState(false);
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilters>({});
// Initialize filters from URL parameters // Initialize filters from URL parameters
useEffect(() => { useEffect(() => {
@@ -209,11 +214,15 @@ export default function LibraryContent() {
} }
}; };
const clearFilters = () => { const handleClearFilters = () => {
// Clear state
setSearchQuery(''); setSearchQuery('');
setSelectedTags([]); setSelectedTags([]);
setAdvancedFilters({}); setAdvancedFilters({});
setPage(0); setPage(0);
// Clear sessionStorage
clearLibraryFilters();
// Trigger refresh
setRefreshTrigger(prev => prev + 1); setRefreshTrigger(prev => prev + 1);
}; };
@@ -266,7 +275,7 @@ export default function LibraryContent() {
onSortDirectionToggle: handleSortDirectionToggle, onSortDirectionToggle: handleSortDirectionToggle,
onAdvancedFiltersChange: handleAdvancedFiltersChange, onAdvancedFiltersChange: handleAdvancedFiltersChange,
onRandomStory: handleRandomStory, onRandomStory: handleRandomStory,
onClearFilters: clearFilters, onClearFilters: handleClearFilters,
}; };
const renderContent = () => { const renderContent = () => {
@@ -280,7 +289,7 @@ export default function LibraryContent() {
} }
</p> </p>
{searchQuery || selectedTags.length > 0 || Object.values(advancedFilters).some(v => v !== undefined && v !== '' && v !== 'all' && v !== false) ? ( {searchQuery || selectedTags.length > 0 || Object.values(advancedFilters).some(v => v !== undefined && v !== '' && v !== 'all' && v !== false) ? (
<Button variant="ghost" onClick={clearFilters}> <Button variant="ghost" onClick={handleClearFilters}>
Clear Filters Clear Filters
</Button> </Button>
) : ( ) : (

View File

@@ -0,0 +1,68 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
/**
* Custom hook for persisting library filter state in sessionStorage.
* Filters are preserved during the browser session but cleared when the tab is closed.
*
* @param key - Unique identifier for the filter value in sessionStorage
* @param defaultValue - Default value if no stored value exists
* @returns Tuple of [value, setValue] similar to useState
*/
export function useLibraryFilters<T>(
key: string,
defaultValue: T
): [T, Dispatch<SetStateAction<T>>] {
// Initialize state from sessionStorage or use default value
const [value, setValue] = useState<T>(() => {
// SSR safety: sessionStorage only available in browser
if (typeof window === 'undefined') {
return defaultValue;
}
try {
const stored = sessionStorage.getItem(`library_filter_${key}`);
if (stored === null) {
return defaultValue;
}
return JSON.parse(stored) as T;
} catch (error) {
console.warn(`Failed to parse sessionStorage value for library_filter_${key}:`, error);
return defaultValue;
}
});
// Persist to sessionStorage whenever value changes
useEffect(() => {
if (typeof window === 'undefined') return;
try {
sessionStorage.setItem(`library_filter_${key}`, JSON.stringify(value));
} catch (error) {
console.warn(`Failed to save to sessionStorage for library_filter_${key}:`, error);
}
}, [key, value]);
return [value, setValue];
}
/**
* Clear all library filters from sessionStorage.
* Useful for "Clear Filters" button or when switching libraries.
*/
export function clearLibraryFilters(): void {
if (typeof window === 'undefined') return;
try {
// Get all sessionStorage keys
const keys = Object.keys(sessionStorage);
// Remove only library filter keys
keys.forEach(key => {
if (key.startsWith('library_filter_')) {
sessionStorage.removeItem(key);
}
});
} catch (error) {
console.warn('Failed to clear library filters from sessionStorage:', error);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@ http {
server { server {
listen 80; listen 80;
client_max_body_size 600M; client_max_body_size 2048M; # 2GB for large backup uploads
# Frontend routes # Frontend routes
location / { location / {
@@ -55,8 +55,8 @@ http {
proxy_connect_timeout 900s; proxy_connect_timeout 900s;
proxy_send_timeout 900s; proxy_send_timeout 900s;
proxy_read_timeout 900s; proxy_read_timeout 900s;
# Large upload settings # Large upload settings (2GB for backups)
client_max_body_size 600M; client_max_body_size 2048M;
proxy_request_buffering off; proxy_request_buffering off;
proxy_max_temp_file_size 0; proxy_max_temp_file_size 0;
} }