feat(upload): add S3 folder dropdown (backend /api/music/folders) and use selected folder for uploads; keep text input as fallback
This commit is contained in:
parent
aae57ec55f
commit
762ae0730a
@ -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
|
||||
*/
|
||||
|
||||
@ -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<string[]> {
|
||||
const folders = new Set<string>();
|
||||
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
|
||||
*/
|
||||
|
||||
@ -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<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [targetFolder, setTargetFolder] = useState<string>('uploads');
|
||||
const [folders, setFolders] = useState<string[]>([]);
|
||||
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
||||
const toast = useToast();
|
||||
|
||||
@ -120,6 +122,26 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ 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<MusicUploadProps> = ({ onUploadComplete }) =>
|
||||
<HStack>
|
||||
<Box flex={1}>
|
||||
<Text color="gray.300" fontSize="sm" mb={1}>Target S3 folder</Text>
|
||||
<Input value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" />
|
||||
{folders.length > 0 ? (
|
||||
<Select value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} bg="gray.800" borderColor="gray.700">
|
||||
{folders.map((f) => (
|
||||
<option key={f} value={f}>{f}</option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Input value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" />
|
||||
)}
|
||||
</Box>
|
||||
<Checkbox isChecked={markForScan} onChange={(e) => setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end">
|
||||
Add to "To Be Scanned"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user