Skip to content

Commit

Permalink
Asset browser (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra authored Jul 23, 2024
1 parent e93bd9f commit c77cfb6
Show file tree
Hide file tree
Showing 33 changed files with 302 additions and 55 deletions.
94 changes: 93 additions & 1 deletion backend/app/api/frames.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import io
import json
import shlex

import requests
import os

from http import HTTPStatus
from flask import jsonify, request, Response
from flask import jsonify, request, Response, send_file
from flask_login import login_required
from . import api
from app import redis
from app.models.frame import Frame, new_frame, delete_frame, update_frame
from app.models.log import new_log as log
from app.models.metrics import Metrics
from app.codegen.scene_nim import write_scene_nim
from app.utils.ssh_utils import get_ssh_connection, exec_command, remove_ssh_connection
from scp import SCPClient
from tempfile import NamedTemporaryFile


@api.route("/frames", methods=["GET"])
Expand Down Expand Up @@ -127,6 +135,90 @@ def api_frame_scene_source(id: int, scene: str):
return jsonify({'source': write_scene_nim(frame, scene_json)})
return jsonify({'error': f'Scene {scene} not found'}), HTTPStatus.NOT_FOUND

@api.route('/frames/<int:id>/assets', methods=['GET'])
@login_required
def api_frame_get_assets(id: int):
frame = Frame.query.get_or_404(id)
assets_path = frame.assets_path or "/srv/assets"
ssh = get_ssh_connection(frame)
command = f"find {assets_path} -type f -exec stat --format='%s %Y %n' {{}} +"
output = []
exec_command(frame, ssh, command, output, log_output=False)
remove_ssh_connection(ssh)

assets = []
for line in output:
parts = line.split(' ', 2)
size, mtime, path = parts
assets.append({
'path': path.strip(),
'size': int(size.strip()),
'mtime': int(mtime.strip()),
})

assets.sort(key=lambda x: x['path'])
return jsonify(assets=assets)

@api.route('/frames/<int:id>/asset', methods=['GET'])
@login_required
def api_frame_get_asset(id: int):
frame = Frame.query.get_or_404(id)
assets_path = frame.assets_path or "/srv/assets"
path = request.args.get('path')
mode = request.args.get('mode', 'download') # Default mode is 'download'
filename = request.args.get('filename', os.path.basename(path))

if not path:
return jsonify({'error': 'Path parameter is required'}), HTTPStatus.BAD_REQUEST

# Normalize and validate the path
normalized_path = os.path.normpath(os.path.join(assets_path, path))
if not normalized_path.startswith(os.path.normpath(assets_path)):
return jsonify({'error': 'Invalid asset path'}), HTTPStatus.BAD_REQUEST

try:
ssh = get_ssh_connection(frame)
try:
escaped_path = shlex.quote(normalized_path)
# Check if the asset exists and get its MD5 hash
command = f"md5sum {escaped_path}"
log(frame.id, "stdinfo", f"> {command}")
stdin, stdout, stderr = ssh.exec_command(command)
md5sum_output = stdout.read().decode().strip()
if not md5sum_output:
return jsonify({'error': 'Asset not found'}), HTTPStatus.NOT_FOUND

md5sum = md5sum_output.split()[0]
cache_key = f'asset:{md5sum}'

cached_asset = redis.get(cache_key)
if cached_asset:
return send_file(
io.BytesIO(cached_asset),
download_name=filename,
as_attachment=(mode == 'download'),
mimetype='image/png' if mode == 'image' else 'application/octet-stream'
)

# Download the file to a temporary file
with NamedTemporaryFile(delete=True) as temp_file:
with SCPClient(ssh.get_transport()) as scp:
scp.get(normalized_path, temp_file.name)
temp_file.seek(0)
asset_content = temp_file.read()
redis.set(cache_key, asset_content, ex=86400 * 30) # Cache for 30 days
return send_file(
io.BytesIO(asset_content),
download_name=filename,
as_attachment=(mode == 'download'),
mimetype='image/png' if mode == 'image' else 'application/octet-stream'
)
finally:
remove_ssh_connection(ssh)
except Exception as e:
return jsonify({'error': 'Internal Server Error', 'message': str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR


@api.route('/frames/<int:id>/reset', methods=['POST'])
@login_required
def api_frame_reset_event(id: int):
Expand Down
8 changes: 5 additions & 3 deletions backend/app/utils/ssh_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,19 @@ def get_ssh_connection(frame: Frame) -> SSHClient:
return ssh


def exec_command(frame: Frame, ssh: SSHClient, command: str, output: Optional[list[str]] = None, raise_on_error = True) -> int:
def exec_command(frame: Frame, ssh: SSHClient, command: str, output: Optional[list[str]] = None, raise_on_error = True, log_output = True) -> int:
log(frame.id, "stdout", f"> {command}")
_stdin, stdout, stderr = ssh.exec_command(command)
exit_status = None
while exit_status is None:
while line := stdout.readline():
log(frame.id, "stdout", line)
if log_output:
log(frame.id, "stdout", line)
if output is not None:
output.append(line)
while line := stderr.readline():
log(frame.id, "stderr", line)
if log_output:
log(frame.id, "stderr", line)
if output is not None:
output.append(line)

Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataDownloadImage.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataDownloadUrl.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataLocalImage.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataNewImage.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataNewImageNext.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataQR.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_dataResize.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_logicIfElse.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_logicSetAsState.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderColorFlow.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderColorImage.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderColorSplit.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderGradientSplit.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderImage.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderImageBlend.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderImageMask.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderOpacity.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderSplitData.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderSplitFlow.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderSplitLoop.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderTextOverflow.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderTextPosition.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderTextRich.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderTextRichOver.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
2 changes: 1 addition & 1 deletion e2e/generated/scene_renderTextSplit.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# This file is autogenerated

{.warning[UnusedImport]: off.}
import pixie, json, times, strformat, strutils, sequtils, options
import pixie, json, times, strformat, strutils, sequtils, options, algorithm

import frameos/types
import frameos/channels
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/scenes/frame/panels/Assets/Asset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useValues } from 'kea'
import { frameLogic } from '../../frameLogic'
import { useState } from 'react'

interface AssetProps {
path: string
}

export function Asset({ path }: AssetProps) {
const { frame } = useValues(frameLogic)
const isImage = path.endsWith('.png') || path.endsWith('.jpg') || path.endsWith('.jpeg') || path.endsWith('.gif')
const [isLoading, setIsLoading] = useState(true)

return (
<div className="w-full">
{isImage ? (
<>
<img
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
className="max-w-full"
src={`/api/frames/${frame.id}/asset?path=${encodeURIComponent(path)}`}
alt={path}
/>
{isLoading ? <div>Loading...</div> : null}
</>
) : (
<>{path}</>
)}
</div>
)
}
Loading

0 comments on commit c77cfb6

Please sign in to comment.