file size limits, keep active filters in session
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
68
frontend/src/hooks/useLibraryFilters.ts
Normal file
68
frontend/src/hooks/useLibraryFilters.ts
Normal 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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user