diff --git a/backend/app/api/frames.py b/backend/app/api/frames.py index 2e985123..bf5b87b1 100644 --- a/backend/app/api/frames.py +++ b/backend/app/api/frames.py @@ -423,6 +423,41 @@ async def api_frame_assets_upload( return {"path": path_without_combined, "size": len(contents), "mtime": int(datetime.now().timestamp())} +@api_with_auth.post("/frames/{id:int}/assets/mkdir") +async def api_frame_assets_mkdir( + id: int, + path: str = Form(..., description="Folder to make"), + db: Session = Depends(get_db), redis: Redis = Depends(get_redis) +): + frame = db.get(Frame, id) + if frame is None: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Frame not found") + if not path: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Path parameter is required") + if "*" in path: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid character * in path") + assets_path = frame.assets_path or "/srv/assets" + combined_path = os.path.normpath(os.path.join(assets_path, path)) + if not combined_path.startswith(os.path.normpath(assets_path) + '/'): + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid asset path") + + # TODO: stream and reuse connections + ssh = await get_ssh_connection(db, redis, frame) + try: + await exec_command( + db, redis, frame, ssh, + f"mkdir -p {shlex.quote(combined_path)}", + ) + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + finally: + await remove_ssh_connection(db, redis, ssh, frame) + + path_without_combined = os.path.relpath(combined_path, assets_path) + + return {"path": path_without_combined, "mtime": int(datetime.now().timestamp())} + @api_with_auth.post("/frames/{id:int}/clear_build_cache") async def api_frame_clear_build_cache(id: int, redis: Redis = Depends(get_redis), db: Session = Depends(get_db)): frame = db.get(Frame, id) diff --git a/frontend/src/scenes/frame/panels/Assets/Assets.tsx b/frontend/src/scenes/frame/panels/Assets/Assets.tsx index 69daf2d4..974308b1 100644 --- a/frontend/src/scenes/frame/panels/Assets/Assets.tsx +++ b/frontend/src/scenes/frame/panels/Assets/Assets.tsx @@ -28,17 +28,21 @@ interface AssetNode { children: Record } +interface AssetUtils { + openAsset: (path: string) => void + uploadAssets: (path: string) => void + mkdir: (path: string) => void +} + /** A recursive component that renders a folder or a file */ function TreeNode({ node, frameId, - openAsset, - uploadAssets, + assetUtils, }: { node: AssetNode frameId: number - openAsset: (path: string) => void - uploadAssets: (path: string) => void + assetUtils: AssetUtils }): JSX.Element { const [expanded, setExpanded] = useState(node.path === '') const [isDownloading, setIsDownloading] = useState(false) @@ -60,7 +64,17 @@ function TreeNode({ { label: 'Upload files', icon: , - onClick: () => uploadAssets(node.path), + onClick: () => assetUtils.uploadAssets(node.path), + }, + { + label: 'New folder', + icon: , + onClick: () => { + const newFolder = prompt('Enter the name of the new folder') + if (newFolder) { + assetUtils.mkdir(node.path ? node.path + '/' + newFolder : newFolder) + } + }, }, ]} /> @@ -68,13 +82,7 @@ function TreeNode({ {expanded && (
{Object.values(node.children).map((child) => ( - + ))}
)} @@ -85,7 +93,7 @@ function TreeNode({ return (
- openAsset(node.path)}> + assetUtils.openAsset(node.path)}> {node.name}
@@ -132,7 +140,7 @@ export function Assets(): JSX.Element { const { frame } = useValues(frameLogic) const { openLogs } = useActions(panelsLogic) const { assetsLoading, assetTree } = useValues(assetsLogic({ frameId: frame.id })) - const { syncAssets, uploadAssets } = useActions(assetsLogic({ frameId: frame.id })) + const { syncAssets, uploadAssets, mkdir } = useActions(assetsLogic({ frameId: frame.id })) const { openAsset } = useActions(panelsLogic({ frameId: frame.id })) return ( @@ -157,7 +165,11 @@ export function Assets(): JSX.Element {
Loading assets...
) : (
- +
)}
diff --git a/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts b/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts index 206e1f5b..aad9d69b 100644 --- a/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts +++ b/frontend/src/scenes/frame/panels/Assets/assetsLogic.ts @@ -69,6 +69,9 @@ export const assetsLogic = kea([ filesToUpload: (files: string[]) => ({ files }), uploadFailure: (path: string) => ({ path }), syncAssets: true, + mkdir: (path: string) => ({ path }), + mkdirComplete: (path: string) => ({ path }), + mkdirFailure: (path: string) => ({ path }), }), loaders(({ props }) => ({ assets: [ @@ -156,6 +159,20 @@ export const assetsLogic = kea([ } input.click() }, + mkdir: async ({ path }) => { + try { + const formData = new FormData() + formData.append('path', path) + const response = await apiFetch(`/api/frames/${props.frameId}/assets/mkdir`, { + method: 'POST', + body: formData, + }) + await response.json() + actions.mkdirComplete(path) + } catch (error) { + actions.mkdirFailure(path) + } + }, })), reducers({ assets: { @@ -181,6 +198,9 @@ export const assetsLogic = kea([ }, uploadFailure: (state, { path }) => state.map((asset) => (asset.path === path ? { ...asset, size: -2, mtime: -2 } : asset)), + mkdirComplete: (state, { path }) => { + return [...state, { path: path + '/.', size: 0, mtime: Date.now() }] + }, }, }), afterMount(({ actions }) => {