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
|
* Sync S3 files with database - now uses background job system
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -141,6 +141,42 @@ export class S3Service {
|
|||||||
return files;
|
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
|
* Generate a presigned URL for secure file access
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
Input,
|
Input,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Select,
|
||||||
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';
|
||||||
@ -33,6 +34,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
|||||||
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
|
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
|
||||||
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 [markForScan, setMarkForScan] = useState<boolean>(true);
|
const [markForScan, setMarkForScan] = useState<boolean>(true);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@ -120,6 +122,26 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
|||||||
multiple: true,
|
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 = () => {
|
const resetUploads = () => {
|
||||||
setUploadProgress([]);
|
setUploadProgress([]);
|
||||||
};
|
};
|
||||||
@ -129,7 +151,15 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
|
|||||||
<HStack>
|
<HStack>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<Text color="gray.300" fontSize="sm" mb={1}>Target S3 folder</Text>
|
<Text color="gray.300" fontSize="sm" mb={1}>Target S3 folder</Text>
|
||||||
|
{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" />
|
<Input value={targetFolder} onChange={(e) => setTargetFolder(e.target.value)} placeholder="e.g. Prep/ToScan" bg="gray.800" borderColor="gray.700" />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Checkbox isChecked={markForScan} onChange={(e) => setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end">
|
<Checkbox isChecked={markForScan} onChange={(e) => setMarkForScan(e.target.checked)} colorScheme="blue" alignSelf="end">
|
||||||
Add to "To Be Scanned"
|
Add to "To Be Scanned"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user