feat(upload): improve folder browser UX and performance - add loading state, fix chevron visibility, implement backend caching with 5min TTL, add cache invalidation on file operations
This commit is contained in:
parent
10e38fae4d
commit
7557eddeb4
@ -95,6 +95,9 @@ router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
|
||||
await musicFile.save();
|
||||
|
||||
// Invalidate folder cache since we added a new file
|
||||
invalidateFolderCache();
|
||||
|
||||
// Optionally add to a special playlist for scanning
|
||||
if (markForScan) {
|
||||
try {
|
||||
@ -191,6 +194,9 @@ router.post('/batch-upload', upload.array('files', 10), async (req, res) => {
|
||||
|
||||
await musicFile.save();
|
||||
results.push({ success: true, musicFile });
|
||||
|
||||
// Invalidate folder cache since we added a new file
|
||||
invalidateFolderCache();
|
||||
} catch (error) {
|
||||
console.error(`Error uploading ${file.originalname}:`, error);
|
||||
results.push({ success: false, fileName: file.originalname, error: (error as Error).message });
|
||||
@ -223,17 +229,51 @@ router.get('/files', async (req, res) => {
|
||||
/**
|
||||
* List folders in the S3 bucket for folder selection
|
||||
*/
|
||||
// Cache for folder listing to improve performance
|
||||
let folderCache: { folders: string[]; timestamp: number } | null = null;
|
||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Function to invalidate folder cache
|
||||
const invalidateFolderCache = () => {
|
||||
folderCache = null;
|
||||
};
|
||||
|
||||
router.get('/folders', async (req, res) => {
|
||||
try {
|
||||
// Check if we have a valid cached result
|
||||
if (folderCache && (Date.now() - folderCache.timestamp) < CACHE_DURATION) {
|
||||
return res.json({ folders: folderCache.folders });
|
||||
}
|
||||
|
||||
const folders = await s3Service.listAllFolders('');
|
||||
// Include root option as empty string
|
||||
res.json({ folders: ['', ...folders] });
|
||||
const result = ['', ...folders];
|
||||
|
||||
// Cache the result
|
||||
folderCache = {
|
||||
folders: result,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
res.json({ folders: result });
|
||||
} catch (error) {
|
||||
console.error('Error fetching S3 folders:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch S3 folders' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Force refresh folder cache
|
||||
*/
|
||||
router.post('/folders/refresh', async (req, res) => {
|
||||
try {
|
||||
invalidateFolderCache();
|
||||
res.json({ message: 'Folder cache refreshed successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error refreshing folder cache:', error);
|
||||
res.status(500).json({ error: 'Failed to refresh folder cache' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sync S3 files with database - now uses background job system
|
||||
*/
|
||||
@ -256,6 +296,9 @@ router.post('/sync-s3', async (req, res) => {
|
||||
status: 'started'
|
||||
});
|
||||
|
||||
// Invalidate folder cache since sync might change folder structure
|
||||
invalidateFolderCache();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error starting S3 sync job:', error);
|
||||
res.status(500).json({ error: 'Failed to start S3 sync job' });
|
||||
@ -396,6 +439,9 @@ router.delete('/:id', async (req, res) => {
|
||||
// Delete from database
|
||||
await MusicFile.findByIdAndDelete(req.params.id);
|
||||
|
||||
// Invalidate folder cache since we removed a file
|
||||
invalidateFolderCache();
|
||||
|
||||
res.json({ message: 'Music file deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
|
||||
@ -135,6 +135,9 @@ const FolderBrowser: React.FC<FolderBrowserProps> = ({
|
||||
toggleFolder(node.path);
|
||||
}}
|
||||
aria-label={expanded ? 'Collapse folder' : 'Expand folder'}
|
||||
color="gray.300"
|
||||
_hover={{ bg: 'gray.700', color: 'white' }}
|
||||
_active={{ bg: 'gray.600' }}
|
||||
/>
|
||||
) : (
|
||||
<Box w="32px" />
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
Icon,
|
||||
Input,
|
||||
Checkbox,
|
||||
Spinner,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi';
|
||||
@ -35,6 +36,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [targetFolder, setTargetFolder] = useState<string>('uploads');
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const [isLoadingFolders, setIsLoadingFolders] = useState(true);
|
||||
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
||||
const toast = useToast();
|
||||
|
||||
@ -126,6 +128,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setIsLoadingFolders(true);
|
||||
const res = await fetch('/api/music/folders');
|
||||
if (!res.ok) throw new Error('Failed to load folders');
|
||||
const data = await res.json();
|
||||
@ -138,6 +141,8 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore, keep text input fallback
|
||||
} finally {
|
||||
setIsLoadingFolders(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
@ -150,7 +155,19 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
<VStack spacing={4} align="stretch" w="full">
|
||||
<Box>
|
||||
<Text color="gray.300" fontSize="sm" mb={3}>Target S3 folder</Text>
|
||||
{folders.length > 0 ? (
|
||||
{isLoadingFolders ? (
|
||||
<Box
|
||||
p={6}
|
||||
textAlign="center"
|
||||
bg="gray.800"
|
||||
border="1px solid"
|
||||
borderColor="gray.700"
|
||||
borderRadius="md"
|
||||
>
|
||||
<Spinner size="md" color="blue.400" mb={3} />
|
||||
<Text color="gray.400" fontSize="sm">Loading folder structure...</Text>
|
||||
</Box>
|
||||
) : folders.length > 0 ? (
|
||||
<FolderBrowser
|
||||
folders={folders}
|
||||
selectedFolder={targetFolder}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user