From 762ae0730a23f5fc1722c3230eeef8922e873b34 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 17:02:58 +0200 Subject: [PATCH] feat(upload): add S3 folder dropdown (backend /api/music/folders) and use selected folder for uploads; keep text input as fallback --- packages/backend/src/routes/music.ts | 13 +++++++ packages/backend/src/services/s3Service.ts | 36 +++++++++++++++++++ .../frontend/src/components/MusicUpload.tsx | 32 ++++++++++++++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/routes/music.ts b/packages/backend/src/routes/music.ts index dce125e..cea66ef 100644 --- a/packages/backend/src/routes/music.ts +++ b/packages/backend/src/routes/music.ts @@ -220,6 +220,19 @@ router.get('/files', async (req, res) => { } }); +/** + * List folders in the S3 bucket for folder selection + */ +router.get('/folders', async (req, res) => { + try { + const folders = await s3Service.listAllFolders(''); + res.json({ folders }); + } catch (error) { + console.error('Error fetching S3 folders:', error); + res.status(500).json({ error: 'Failed to fetch S3 folders' }); + } +}); + /** * Sync S3 files with database - now uses background job system */ diff --git a/packages/backend/src/services/s3Service.ts b/packages/backend/src/services/s3Service.ts index 17a5c52..0e980b5 100644 --- a/packages/backend/src/services/s3Service.ts +++ b/packages/backend/src/services/s3Service.ts @@ -141,6 +141,42 @@ export class S3Service { return files; } + /** + * List all folders (prefixes) in the bucket. Recursively collects nested prefixes. + */ + async listAllFolders(prefix: string = ''): Promise { + const folders = new Set(); + const queue: string[] = [prefix]; + + while (queue.length > 0) { + const currentPrefix = queue.shift() || ''; + let continuationToken: string | undefined; + + do { + const command = new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: currentPrefix, + Delimiter: '/', + ContinuationToken: continuationToken, + }); + const response = await this.client.send(command); + const common = (response.CommonPrefixes || []).map(cp => cp.Prefix).filter(Boolean) as string[]; + for (const p of common) { + // Normalize: strip trailing slash + const normalized = p.replace(/\/+$/, ''); + if (!folders.has(normalized)) { + folders.add(normalized); + // Continue deeper + queue.push(p); + } + } + continuationToken = response.NextContinuationToken; + } while (continuationToken); + } + + return Array.from(folders).sort(); + } + /** * Generate a presigned URL for secure file access */ diff --git a/packages/frontend/src/components/MusicUpload.tsx b/packages/frontend/src/components/MusicUpload.tsx index 8fa924a..c9f91b1 100644 --- a/packages/frontend/src/components/MusicUpload.tsx +++ b/packages/frontend/src/components/MusicUpload.tsx @@ -14,6 +14,7 @@ import { Icon, Input, Checkbox, + Select, useToast, } from '@chakra-ui/react'; import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi'; @@ -33,6 +34,7 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => const [uploadProgress, setUploadProgress] = useState([]); const [isUploading, setIsUploading] = useState(false); const [targetFolder, setTargetFolder] = useState('uploads'); + const [folders, setFolders] = useState([]); const [markForScan, setMarkForScan] = useState(true); const toast = useToast(); @@ -120,6 +122,26 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => multiple: true, }); + // Load folders for dropdown + React.useEffect(() => { + (async () => { + try { + const res = await fetch('/api/music/folders'); + if (!res.ok) throw new Error('Failed to load folders'); + const data = await res.json(); + const items = Array.isArray(data.folders) ? data.folders : []; + setFolders(items); + if (items.length > 0) { + // default to first folder if uploads not present + const defaultChoice = items.find((f: string) => f.toLowerCase().includes('upload')) || items[0]; + setTargetFolder(defaultChoice.replace(/^\/+/, '')); + } + } catch (e) { + // ignore, keep text input fallback + } + })(); + }, []); + const resetUploads = () => { setUploadProgress([]); }; @@ -129,7 +151,15 @@ export const MusicUpload: React.FC = ({ onUploadComplete }) => Target S3 folder - setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" /> + {folders.length > 0 ? ( + + ) : ( + setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" /> + )} setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end"> Add to "To Be Scanned"