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();
|
await musicFile.save();
|
||||||
|
|
||||||
|
// Invalidate folder cache since we added a new file
|
||||||
|
invalidateFolderCache();
|
||||||
|
|
||||||
// Optionally add to a special playlist for scanning
|
// Optionally add to a special playlist for scanning
|
||||||
if (markForScan) {
|
if (markForScan) {
|
||||||
try {
|
try {
|
||||||
@ -191,6 +194,9 @@ router.post('/batch-upload', upload.array('files', 10), async (req, res) => {
|
|||||||
|
|
||||||
await musicFile.save();
|
await musicFile.save();
|
||||||
results.push({ success: true, musicFile });
|
results.push({ success: true, musicFile });
|
||||||
|
|
||||||
|
// Invalidate folder cache since we added a new file
|
||||||
|
invalidateFolderCache();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error uploading ${file.originalname}:`, error);
|
console.error(`Error uploading ${file.originalname}:`, error);
|
||||||
results.push({ success: false, fileName: file.originalname, error: (error as Error).message });
|
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
|
* 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) => {
|
router.get('/folders', async (req, res) => {
|
||||||
try {
|
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('');
|
const folders = await s3Service.listAllFolders('');
|
||||||
// Include root option as empty string
|
const result = ['', ...folders];
|
||||||
res.json({ folders: ['', ...folders] });
|
|
||||||
|
// Cache the result
|
||||||
|
folderCache = {
|
||||||
|
folders: result,
|
||||||
|
timestamp: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({ folders: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching S3 folders:', error);
|
console.error('Error fetching S3 folders:', error);
|
||||||
res.status(500).json({ error: 'Failed to fetch S3 folders' });
|
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
|
* Sync S3 files with database - now uses background job system
|
||||||
*/
|
*/
|
||||||
@ -256,6 +296,9 @@ router.post('/sync-s3', async (req, res) => {
|
|||||||
status: 'started'
|
status: 'started'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate folder cache since sync might change folder structure
|
||||||
|
invalidateFolderCache();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error starting S3 sync job:', error);
|
console.error('❌ Error starting S3 sync job:', error);
|
||||||
res.status(500).json({ error: 'Failed to start S3 sync job' });
|
res.status(500).json({ error: 'Failed to start S3 sync job' });
|
||||||
@ -396,6 +439,9 @@ router.delete('/:id', async (req, res) => {
|
|||||||
// Delete from database
|
// Delete from database
|
||||||
await MusicFile.findByIdAndDelete(req.params.id);
|
await MusicFile.findByIdAndDelete(req.params.id);
|
||||||
|
|
||||||
|
// Invalidate folder cache since we removed a file
|
||||||
|
invalidateFolderCache();
|
||||||
|
|
||||||
res.json({ message: 'Music file deleted successfully' });
|
res.json({ message: 'Music file deleted successfully' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete error:', error);
|
console.error('Delete error:', error);
|
||||||
|
|||||||
@ -135,6 +135,9 @@ const FolderBrowser: React.FC<FolderBrowserProps> = ({
|
|||||||
toggleFolder(node.path);
|
toggleFolder(node.path);
|
||||||
}}
|
}}
|
||||||
aria-label={expanded ? 'Collapse folder' : 'Expand folder'}
|
aria-label={expanded ? 'Collapse folder' : 'Expand folder'}
|
||||||
|
color="gray.300"
|
||||||
|
_hover={{ bg: 'gray.700', color: 'white' }}
|
||||||
|
_active={{ bg: 'gray.600' }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box w="32px" />
|
<Box w="32px" />
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Spinner,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi';
|
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 [isUploading, setIsUploading] = useState(false);
|
||||||
const [targetFolder, setTargetFolder] = useState<string>('uploads');
|
const [targetFolder, setTargetFolder] = useState<string>('uploads');
|
||||||
const [folders, setFolders] = useState<string[]>([]);
|
const [folders, setFolders] = useState<string[]>([]);
|
||||||
|
const [isLoadingFolders, setIsLoadingFolders] = useState(true);
|
||||||
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@ -126,6 +128,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsLoadingFolders(true);
|
||||||
const res = await fetch('/api/music/folders');
|
const res = await fetch('/api/music/folders');
|
||||||
if (!res.ok) throw new Error('Failed to load folders');
|
if (!res.ok) throw new Error('Failed to load folders');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -138,6 +141,8 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore, keep text input fallback
|
// 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">
|
<VStack spacing={4} align="stretch" w="full">
|
||||||
<Box>
|
<Box>
|
||||||
<Text color="gray.300" fontSize="sm" mb={3}>Target S3 folder</Text>
|
<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
|
<FolderBrowser
|
||||||
folders={folders}
|
folders={folders}
|
||||||
selectedFolder={targetFolder}
|
selectedFolder={targetFolder}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user