696 lines
30 KiB
Python
696 lines
30 KiB
Python
import shutil
|
|
import os
|
|
import re
|
|
import base64
|
|
import json
|
|
import hashlib
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
from aiohttp import web
|
|
from typing import Any
|
|
from io import BytesIO
|
|
from PIL import Image
|
|
|
|
from .constants import (
|
|
APP_CONFIGS, APP_VERSION, EXTENSION_NODE_MAP_PATH,
|
|
CUSTOM_NODES_DIR, FLOWMSG, logger, FLOWS_PATH, WEBROOT, CORE_PATH,
|
|
SAFE_FOLDER_NAME_REGEX, ALLOWED_EXTENSIONS, CUSTOM_THEMES_DIR, FLOWS_CONFIG_FILE
|
|
)
|
|
|
|
DATA_DIR = Path(__file__).parent / "data"
|
|
PREVIEWS_REGISTRY_DIR = DATA_DIR / "model_previews_registry"
|
|
PREVIEWS_IMAGES_DIR = DATA_DIR / "model_previews"
|
|
|
|
def ensure_data_folders():
|
|
try:
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
PREVIEWS_REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
|
|
PREVIEWS_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Could not create data dirs: {e}")
|
|
|
|
def pathToKey(model_path: str) -> str:
|
|
return model_path.replace('\\', '/')
|
|
|
|
def get_filename_only(model_path: str) -> str:
|
|
fwd = pathToKey(model_path)
|
|
return os.path.basename(fwd)
|
|
|
|
def get_preview_id(model_path: str) -> str:
|
|
filename = get_filename_only(model_path)
|
|
h = hashlib.sha1(filename.encode('utf-8')).hexdigest()
|
|
return h[:16]
|
|
|
|
def get_preview_paths(preview_id: str):
|
|
sub1 = preview_id[0]
|
|
sub2 = preview_id[:2]
|
|
registry_json = PREVIEWS_REGISTRY_DIR / sub1 / sub2 / f"{preview_id}.json"
|
|
image_folder = PREVIEWS_IMAGES_DIR / sub1 / sub2 / preview_id
|
|
return registry_json, image_folder
|
|
|
|
async def set_model_preview_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
ensure_data_folders()
|
|
data = await request.json()
|
|
rawPath = data.get("modelPath")
|
|
base64_data = data.get("base64Data")
|
|
if not rawPath or not base64_data:
|
|
return web.Response(status=400, text="Missing 'modelPath' or 'base64Data'")
|
|
|
|
pid = get_preview_id(rawPath)
|
|
registry_json, image_folder = get_preview_paths(pid)
|
|
|
|
match = re.match(r"data:(image/\w+);base64,(.+)", base64_data)
|
|
if not match:
|
|
return web.Response(status=400, text="Invalid data URL format")
|
|
|
|
mime_type = match.group(1)
|
|
encoded = match.group(2)
|
|
try:
|
|
raw_image = base64.b64decode(encoded)
|
|
except:
|
|
return web.Response(status=400, text="Error decoding base64 image")
|
|
|
|
registry_json.parent.mkdir(parents=True, exist_ok=True)
|
|
image_folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
full_path = image_folder / "full.jpg"
|
|
with full_path.open("wb") as f:
|
|
f.write(raw_image)
|
|
|
|
thumb_path = image_folder / "thumbnail.jpg"
|
|
img = Image.open(BytesIO(raw_image))
|
|
w_percent = 128.0 / float(img.size[0])
|
|
h_new = int(float(img.size[1]) * w_percent)
|
|
img = img.convert("RGB").resize((128, h_new), Image.Resampling.LANCZOS)
|
|
img.save(thumb_path, format="JPEG")
|
|
|
|
reg_data = {
|
|
"modelPath": rawPath,
|
|
"previewId": pid,
|
|
"timestamp": int(os.path.getmtime(full_path)),
|
|
"mime_type": mime_type
|
|
}
|
|
with registry_json.open("w", encoding="utf-8") as jf:
|
|
json.dump(reg_data, jf, indent=2)
|
|
|
|
return web.json_response({"status": "success", "previewId": pid})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error in set_model_preview_handler: {e}")
|
|
return web.Response(status=500, text=str(e))
|
|
|
|
async def clear_model_preview_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
ensure_data_folders()
|
|
rawPath = request.query.get("modelPath", None)
|
|
if not rawPath:
|
|
return web.Response(status=400, text="Missing 'modelPath'")
|
|
|
|
pid = get_preview_id(rawPath)
|
|
registry_json, image_folder = get_preview_paths(pid)
|
|
|
|
if registry_json.exists():
|
|
registry_json.unlink()
|
|
if image_folder.exists() and image_folder.is_dir():
|
|
shutil.rmtree(image_folder)
|
|
|
|
return web.json_response({"status": "success", "previewId": pid})
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error in clear_model_preview_handler: {e}")
|
|
return web.Response(status=500, text=str(e))
|
|
|
|
async def list_model_previews_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
ensure_data_folders()
|
|
result_map = {}
|
|
|
|
if request.method == 'POST':
|
|
data = await request.json()
|
|
raw_paths = data.get('paths', [])
|
|
if not isinstance(raw_paths, list):
|
|
return web.Response(status=400, text="Invalid JSON: 'paths' must be an array")
|
|
|
|
for rp in raw_paths:
|
|
rp = urllib.parse.unquote(rp).strip()
|
|
if not rp:
|
|
continue
|
|
pid = get_preview_id(rp)
|
|
_, image_folder = get_preview_paths(pid)
|
|
thumb = image_folder / "thumbnail.jpg"
|
|
if thumb.exists():
|
|
with thumb.open("rb") as tf:
|
|
b = tf.read()
|
|
b64 = base64.b64encode(b).decode("utf-8")
|
|
data_url = f"data:image/jpeg;base64,{b64}"
|
|
result_map[rp] = data_url
|
|
|
|
return web.json_response(result_map)
|
|
|
|
paths_param = request.rel_url.query.get('paths', None)
|
|
if paths_param:
|
|
raw_split = paths_param.split(',')
|
|
for rp in raw_split:
|
|
rp = rp.strip()
|
|
if not rp:
|
|
continue
|
|
rp = urllib.parse.unquote(rp)
|
|
pid = get_preview_id(rp)
|
|
_, image_folder = get_preview_paths(pid)
|
|
thumb = image_folder / "thumbnail.jpg"
|
|
if thumb.exists():
|
|
with thumb.open("rb") as tf:
|
|
b = tf.read()
|
|
b64 = base64.b64encode(b).decode("utf-8")
|
|
result_map[rp] = f"data:image/jpeg;base64,{b64}"
|
|
return web.json_response(result_map)
|
|
|
|
for root, dirs, files in os.walk(PREVIEWS_REGISTRY_DIR):
|
|
for filename in files:
|
|
if filename.endswith(".json"):
|
|
regp = Path(root) / filename
|
|
try:
|
|
with regp.open("r", encoding="utf-8") as f:
|
|
reg_data = json.load(f)
|
|
mp = reg_data.get("modelPath")
|
|
pid = reg_data.get("previewId")
|
|
if not mp or not pid:
|
|
continue
|
|
|
|
_, folder = get_preview_paths(pid)
|
|
thumb = folder / "thumbnail.jpg"
|
|
if thumb.exists():
|
|
with thumb.open("rb") as tf:
|
|
b = tf.read()
|
|
b64 = base64.b64encode(b).decode("utf-8")
|
|
result_map[mp] = f"data:image/jpeg;base64,{b64}"
|
|
except Exception as ex:
|
|
logger.error(f"{FLOWMSG}: Error reading registry {regp}: {ex}")
|
|
continue
|
|
|
|
return web.json_response(result_map)
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error in list_model_previews_handler: {e}")
|
|
return web.Response(status=500, text=str(e))
|
|
|
|
async def get_model_preview_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
ensure_data_folders()
|
|
rawPath = request.query.get("modelPath", None)
|
|
if not rawPath:
|
|
return web.Response(status=400, text="Missing 'modelPath'")
|
|
|
|
pid = get_preview_id(rawPath)
|
|
_, image_folder = get_preview_paths(pid)
|
|
|
|
thumb = image_folder / "thumbnail.jpg"
|
|
if thumb.exists():
|
|
with thumb.open("rb") as tf:
|
|
b = tf.read()
|
|
b64 = base64.b64encode(b).decode("utf-8")
|
|
data_url = f"data:image/jpeg;base64,{b64}"
|
|
return web.json_response({rawPath: data_url})
|
|
else:
|
|
return web.Response(status=404, text="Preview not found")
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error in get_model_preview_handler: {e}")
|
|
return web.Response(status=500, text=str(e))
|
|
|
|
async def apps_handler(request: web.Request) -> web.Response:
|
|
return web.json_response(APP_CONFIGS)
|
|
|
|
async def flow_version_handler(request: web.Request) -> web.Response:
|
|
return web.json_response({'version': APP_VERSION})
|
|
|
|
async def extension_node_map_handler(request: web.Request) -> web.Response:
|
|
if EXTENSION_NODE_MAP_PATH.exists():
|
|
with EXTENSION_NODE_MAP_PATH.open('r') as f:
|
|
extension_node_map = json.load(f)
|
|
return web.json_response(extension_node_map)
|
|
else:
|
|
logger.error(f"{FLOWMSG}: extension-node-map.json not found at {EXTENSION_NODE_MAP_PATH}")
|
|
return web.Response(status=404, text="extension-node-map.json not found")
|
|
|
|
async def installed_custom_nodes_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
installed_nodes = [item.name for item in CUSTOM_NODES_DIR.iterdir() if item.is_dir()]
|
|
return web.json_response({'installedNodes': installed_nodes})
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error fetching installed custom nodes: {e}")
|
|
return web.Response(status=500, text="Internal Server Error")
|
|
|
|
async def install_package_handler(request: web.Request) -> web.Response:
|
|
data = await request.json()
|
|
package_url = data.get('packageUrl')
|
|
if not package_url:
|
|
return web.Response(status=400, text="Missing 'packageUrl' in request body")
|
|
|
|
package_name = package_url.rstrip('/').split('/')[-1]
|
|
install_path = CUSTOM_NODES_DIR / package_name
|
|
|
|
if install_path.exists():
|
|
return web.json_response({'status': 'already_installed', 'message': f"Custom node '{package_name}' is already installed."})
|
|
|
|
try:
|
|
subprocess.check_call(['git', 'clone', package_url, str(install_path)])
|
|
logger.info(f"{FLOWMSG}: Custom node '{package_name}' cloned successfully.")
|
|
requirements_file = install_path / 'requirements.txt'
|
|
if requirements_file.exists():
|
|
try:
|
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', str(requirements_file)])
|
|
logger.info(f"{FLOWMSG}: Requirements for '{package_name}' installed successfully.")
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"{FLOWMSG}: Failed to install requirements for '{package_name}': {e}")
|
|
shutil.rmtree(install_path)
|
|
return web.json_response({
|
|
'status': 'error',
|
|
'message': f"Failed to install requirements for '{package_name}'. The package has been removed. Please try installing manually."
|
|
}, status=500)
|
|
else:
|
|
logger.info(f"{FLOWMSG}: No requirements.txt found for '{package_name}'.")
|
|
|
|
return web.json_response({'status': 'success', 'message': f"Custom node '{package_name}' installed successfully."})
|
|
except subprocess.CalledProcessError as e:
|
|
if install_path.exists():
|
|
shutil.rmtree(install_path)
|
|
logger.error(f"{FLOWMSG}: Failed to install package '{package_name}': {e}")
|
|
return web.json_response({'status': 'error', 'message': f"Failed to install custom node '{package_name}': {e}"}, status=500)
|
|
except Exception as e:
|
|
if install_path.exists():
|
|
shutil.rmtree(install_path)
|
|
logger.error(f"{FLOWMSG}: An unexpected error occurred while installing '{package_name}': {e}")
|
|
return web.json_response({'status': 'error', 'message': f"An unexpected error occurred while installing '{package_name}': {e}"}, status=500)
|
|
|
|
async def update_package_handler(request: web.Request) -> web.Response:
|
|
data = await request.json()
|
|
package_url = data.get('packageUrl')
|
|
if not package_url:
|
|
return web.Response(status=400, text="Missing 'packageUrl' in request body")
|
|
|
|
package_name = package_url.rstrip('/').split('/')[-1]
|
|
install_path = CUSTOM_NODES_DIR / package_name
|
|
|
|
if not install_path.exists():
|
|
return web.json_response({'status': 'not_installed', 'message': f"Package '{package_name}' is not installed."})
|
|
|
|
try:
|
|
result = subprocess.run(['git', '-C', str(install_path), 'pull'], capture_output=True, text=True)
|
|
if result.returncode == 0:
|
|
return web.json_response({'status': 'success', 'message': f"Package '{package_name}' updated successfully."})
|
|
else:
|
|
logger.error(f"{FLOWMSG}: Failed to update package '{package_name}':\n{result.stderr}")
|
|
return web.json_response({'status': 'error', 'message': f"Failed to update package '{package_name}': {result.stderr}"}, status=500)
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: An error occurred while updating package '{package_name}': {e}")
|
|
return web.json_response({'status': 'error', 'message': f"An error occurred while updating package '{package_name}': {e}"}, status=500)
|
|
|
|
async def uninstall_package_handler(request: web.Request) -> web.Response:
|
|
data = await request.json()
|
|
package_url = data.get('packageUrl')
|
|
if not package_url:
|
|
logger.warning(f"{FLOWMSG}: Uninstall request received with missing 'packageUrl'.")
|
|
return web.Response(status=400, text="Missing 'packageUrl' in request body")
|
|
|
|
package_name = package_url.rstrip('/').split('/')[-1]
|
|
install_path = CUSTOM_NODES_DIR / package_name
|
|
|
|
if not install_path.exists():
|
|
logger.info(f"{FLOWMSG}: Attempt to uninstall non-existent package '{package_name}'.")
|
|
return web.json_response({'status': 'not_installed', 'message': f"Custom node '{package_name}' is not installed."})
|
|
|
|
try:
|
|
logger.info(f"{FLOWMSG}: Uninstalling custom node '{package_name}'...")
|
|
shutil.rmtree(install_path, onerror=remove_readonly)
|
|
logger.info(f"{FLOWMSG}: Custom node '{package_name}' uninstalled successfully.")
|
|
return web.json_response({'status': 'success', 'message': f"Custom node '{package_name}' uninstalled successfully."})
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: An error occurred while uninstalling '{package_name}': {e}")
|
|
return web.json_response({'status': 'error', 'message': f"An error occurred while uninstalling custom node '{package_name}': {e}"}, status=500)
|
|
|
|
async def list_themes_handler(request: web.Request) -> web.Response:
|
|
themes_dir = CUSTOM_THEMES_DIR
|
|
try:
|
|
if not themes_dir.exists():
|
|
logger.warning(f"Custom themes directory does not exist: {themes_dir}")
|
|
return web.json_response([], status=200)
|
|
|
|
css_files = [file.name for file in themes_dir.iterdir() if file.is_file() and allowed_file(file.name)]
|
|
return web.json_response(css_files)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error listing theme files: {e}")
|
|
return web.json_response({'error': 'Failed to list theme files.'}, status=500)
|
|
|
|
async def get_theme_css_handler(request: web.Request) -> web.Response:
|
|
filename = request.match_info.get('filename')
|
|
|
|
if not allowed_file(filename):
|
|
logger.warning(f"Attempt to access disallowed file type: {filename}")
|
|
raise web.HTTPNotFound()
|
|
|
|
file_path = CUSTOM_THEMES_DIR / filename
|
|
|
|
if not file_path.exists() or not file_path.is_file():
|
|
logger.warning(f"CSS file not found: {file_path}")
|
|
raise web.HTTPNotFound()
|
|
|
|
try:
|
|
return web.FileResponse(path=file_path)
|
|
except Exception as e:
|
|
logger.error(f"Error serving CSS file '{filename}': {e}")
|
|
raise web.HTTPInternalServerError(text="Internal Server Error")
|
|
|
|
async def preview_flow_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
reader = await request.multipart()
|
|
flow_config = None
|
|
wf_file = None
|
|
thumbnail_data = None
|
|
thumbnail_extension = None
|
|
|
|
while True:
|
|
part = await reader.next()
|
|
if part is None:
|
|
break
|
|
|
|
if part.name == 'flowConfig':
|
|
flow_config_content = await part.read(decode=True)
|
|
try:
|
|
flow_config = json.loads(flow_config_content)
|
|
except json.JSONDecodeError:
|
|
return web.Response(status=400, text="Invalid JSON format in 'flowConfig'")
|
|
|
|
flow_url = flow_config.get('url', None)
|
|
if not flow_url:
|
|
return web.Response(status=400, text="Missing 'url' in 'flowConfig'")
|
|
|
|
elif part.name == 'wf':
|
|
wf_file = await part.read(decode=False)
|
|
|
|
elif part.name == 'thumbnail':
|
|
thumbnail_str = await part.text()
|
|
match = re.match(r'data:(image/\w+);base64,(.+)', thumbnail_str)
|
|
if match:
|
|
mime_type = match.group(1)
|
|
base64_data = match.group(2)
|
|
thumbnail_extension = mime_type.split('/')[1]
|
|
|
|
try:
|
|
thumbnail_data = base64.b64decode(base64_data)
|
|
except base64.binascii.Error:
|
|
return web.Response(status=400, text="Invalid Base64 encoding in 'thumbnail'")
|
|
else:
|
|
return web.Response(status=400, text="Invalid data URL format for 'thumbnail'")
|
|
|
|
if not flow_config or not wf_file:
|
|
return web.Response(status=400, text="Missing 'flowConfig' or 'wf' in request")
|
|
|
|
flow_id = "linker"
|
|
if not flow_id:
|
|
return web.Response(status=400, text="Missing 'id' in request body")
|
|
|
|
if not SAFE_FOLDER_NAME_REGEX.match(flow_id):
|
|
return web.Response(status=400, text="Invalid 'flow_id'. Only letters, numbers, dashes, and underscores are allowed.")
|
|
|
|
flow_path = WEBROOT / flow_id
|
|
if not flow_path.exists():
|
|
return web.Response(status=404, text=f"Flow directory '{flow_id}' not found")
|
|
|
|
config_path = flow_path / FLOWS_CONFIG_FILE
|
|
with config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
if wf_file:
|
|
wf_json_path = flow_path / 'wf.json'
|
|
with wf_json_path.open('wb') as f:
|
|
f.write(wf_file)
|
|
|
|
if thumbnail_data and thumbnail_extension:
|
|
thumbnail_filename = f"thumbnail.{thumbnail_extension}"
|
|
thumbnail_path = flow_path / thumbnail_filename
|
|
with thumbnail_path.open('wb') as f:
|
|
f.write(thumbnail_data)
|
|
flow_config['thumbnail'] = thumbnail_filename
|
|
with config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
logger.info(f"Thumbnail saved as '{thumbnail_filename}' in flow '{flow_id}'")
|
|
|
|
return web.json_response({
|
|
'status': 'success',
|
|
'message': f"Configuration for previewing flow '{flow_id}' saved successfully.",
|
|
'thumbnail': f"thumbnail.{thumbnail_extension}" if thumbnail_extension else None
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error saving configuration: {e}")
|
|
return web.Response(status=500, text=f"{FLOWMSG}: Error saving configuration: {str(e)}")
|
|
|
|
async def reset_preview_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
flow_id = "linker"
|
|
flow_path = WEBROOT / flow_id
|
|
|
|
if not flow_path.exists():
|
|
return web.Response(status=404, text=f"Flow directory '{flow_id}' not found")
|
|
|
|
defwf_path = flow_path / 'defwf.json'
|
|
wf_path = flow_path / 'wf.json'
|
|
def_flow_config_path = flow_path / 'defFlowConfig.json'
|
|
flow_config_path = flow_path / FLOWS_CONFIG_FILE
|
|
|
|
if not defwf_path.exists():
|
|
return web.Response(status=404, text=f"Default workflow file 'defwf.json' not found in '{flow_id}'")
|
|
|
|
if not def_flow_config_path.exists():
|
|
return web.Response(status=404, text=f"Default flow configuration file 'defFlowConfig.json' not found in '{flow_id}'")
|
|
|
|
shutil.copy2(defwf_path, wf_path)
|
|
shutil.copy2(def_flow_config_path, flow_config_path)
|
|
|
|
logger.info(f"{FLOWMSG}: Preview reset successfully for flow '{flow_id}'.")
|
|
return web.json_response({
|
|
'status': 'success',
|
|
'message': f"Preview has been reset successfully for flow '{flow_id}'."
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error resetting preview: {e}")
|
|
return web.Response(status=500, text=f"{FLOWMSG}: Error resetting preview: {str(e)}")
|
|
|
|
async def create_flow_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
reader = await request.multipart()
|
|
flow_config = None
|
|
wf_file = None
|
|
flow_url = None
|
|
thumbnail_data = None
|
|
|
|
while True:
|
|
part = await reader.next()
|
|
if part is None:
|
|
break
|
|
|
|
if part.name == 'flowConfig':
|
|
flow_config_content = await part.read(decode=True)
|
|
try:
|
|
flow_config = json.loads(flow_config_content)
|
|
except json.JSONDecodeError:
|
|
return web.Response(status=400, text="Invalid JSON format in 'flowConfig'")
|
|
|
|
flow_url = flow_config.get('url', None)
|
|
if not flow_url:
|
|
return web.Response(status=400, text="Missing 'url' in 'flowConfig'")
|
|
|
|
elif part.name == 'wf':
|
|
wf_file = await part.read(decode=False)
|
|
|
|
elif part.name == 'thumbnail':
|
|
thumbnail_str = await part.text()
|
|
match = re.match(r'data:(image/\w+);base64,(.+)', thumbnail_str)
|
|
if match:
|
|
mime_type = match.group(1)
|
|
base64_data = match.group(2)
|
|
try:
|
|
thumbnail_bytes = base64.b64decode(base64_data)
|
|
image = Image.open(BytesIO(thumbnail_bytes))
|
|
if image.mode in ("RGBA", "P"):
|
|
image = image.convert("RGB")
|
|
width_percent = (468 / float(image.size[0]))
|
|
new_height = int((float(image.size[1]) * float(width_percent)))
|
|
image = image.resize((468, new_height), Image.Resampling.LANCZOS)
|
|
buffered = BytesIO()
|
|
image.save(buffered, format="JPEG")
|
|
thumbnail_data = buffered.getvalue()
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error processing thumbnail: {e}")
|
|
return web.Response(status=400, text="Invalid image data in 'thumbnail'")
|
|
else:
|
|
return web.Response(status=400, text="Invalid data URL format for 'thumbnail'")
|
|
|
|
if not flow_config or not wf_file:
|
|
return web.Response(status=400, text="Missing 'flowConfig' or 'wf' in request")
|
|
|
|
if not SAFE_FOLDER_NAME_REGEX.match(flow_url):
|
|
return web.Response(status=400, text="Invalid 'url' in 'flowConfig'. Only letters, numbers, dashes, and underscores are allowed.")
|
|
|
|
flow_folder = FLOWS_PATH / flow_url
|
|
if flow_folder.exists():
|
|
return web.Response(status=400, text=f"Flow with url '{flow_url}' already exists")
|
|
|
|
flow_folder.mkdir(parents=True, exist_ok=False)
|
|
|
|
flow_config_path = flow_folder / FLOWS_CONFIG_FILE
|
|
with flow_config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
wf_json_path = flow_folder / 'wf.json'
|
|
with wf_json_path.open('wb') as f:
|
|
f.write(wf_file)
|
|
|
|
if thumbnail_data:
|
|
media_folder = flow_folder / 'media'
|
|
media_folder.mkdir(exist_ok=True)
|
|
thumbnail_filename = "thumbnail.jpg"
|
|
thumbnail_path = media_folder / thumbnail_filename
|
|
with thumbnail_path.open('wb') as f:
|
|
f.write(thumbnail_data)
|
|
|
|
with flow_config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
logger.info(f"Thumbnail saved as '{thumbnail_filename}' in flow '{flow_url}'")
|
|
|
|
index_template_path = CORE_PATH / 'templates' / 'index.html'
|
|
if not index_template_path.exists():
|
|
return web.Response(status=500, text="Template 'index.html' not found")
|
|
|
|
index_destination_path = flow_folder / 'index.html'
|
|
shutil.copy2(index_template_path, index_destination_path)
|
|
|
|
logger.info(f"{FLOWMSG}: Flow '{flow_url}' created successfully.")
|
|
return web.json_response({'status': 'success', 'message': f"Flow '{flow_url}' created successfully."})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error creating flow: {e}")
|
|
return web.Response(status=500, text=f"{FLOWMSG}: Error creating flow: {str(e)}")
|
|
|
|
async def update_flow_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
reader = await request.multipart()
|
|
flow_config = None
|
|
wf_file = None
|
|
flow_url = None
|
|
thumbnail_data = None
|
|
|
|
while True:
|
|
part = await reader.next()
|
|
if part is None:
|
|
break
|
|
|
|
if part.name == 'flowConfig':
|
|
flow_config_content = await part.read(decode=True)
|
|
try:
|
|
flow_config = json.loads(flow_config_content)
|
|
except json.JSONDecodeError:
|
|
return web.Response(status=400, text="Invalid JSON format in 'flowConfig'")
|
|
|
|
flow_url = flow_config.get('url', None)
|
|
if not flow_url:
|
|
return web.Response(status=400, text="Missing 'url' in 'flowConfig'")
|
|
|
|
elif part.name == 'wf':
|
|
wf_file = await part.read(decode=False)
|
|
|
|
elif part.name == 'thumbnail':
|
|
thumbnail_str = await part.text()
|
|
match = re.match(r'data:(image/\w+);base64,(.+)', thumbnail_str)
|
|
if match:
|
|
mime_type = match.group(1)
|
|
base64_data = match.group(2)
|
|
try:
|
|
thumbnail_bytes = base64.b64decode(base64_data)
|
|
image = Image.open(BytesIO(thumbnail_bytes))
|
|
if image.mode in ("RGBA", "P"):
|
|
image = image.convert("RGB")
|
|
width_percent = (468 / float(image.size[0]))
|
|
new_height = int((float(image.size[1]) * float(width_percent)))
|
|
image = image.resize((468, new_height), Image.Resampling.LANCZOS)
|
|
buffered = BytesIO()
|
|
image.save(buffered, format="JPEG")
|
|
thumbnail_data = buffered.getvalue()
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error processing thumbnail: {e}")
|
|
return web.Response(status=400, text="Invalid image data in 'thumbnail'")
|
|
else:
|
|
return web.Response(status=400, text="Invalid data URL format for 'thumbnail'")
|
|
|
|
if not flow_config:
|
|
return web.Response(status=400, text="Missing 'flowConfig' in request")
|
|
|
|
if not SAFE_FOLDER_NAME_REGEX.match(flow_url):
|
|
return web.Response(status=400, text="Invalid 'url' in 'flowConfig'. Only letters, numbers, dashes, and underscores are allowed.")
|
|
|
|
flow_folder = FLOWS_PATH / flow_url
|
|
if not flow_folder.exists():
|
|
return web.Response(status=400, text=f"Flow with url '{flow_url}' does not exist")
|
|
|
|
flow_config_path = flow_folder / FLOWS_CONFIG_FILE
|
|
with flow_config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
if wf_file:
|
|
wf_json_path = flow_folder / 'wf.json'
|
|
with wf_json_path.open('wb') as f:
|
|
f.write(wf_file)
|
|
|
|
if thumbnail_data:
|
|
media_folder = flow_folder / 'media'
|
|
media_folder.mkdir(exist_ok=True)
|
|
thumbnail_filename = "thumbnail.jpg"
|
|
thumbnail_path = media_folder / thumbnail_filename
|
|
with thumbnail_path.open('wb') as f:
|
|
f.write(thumbnail_data)
|
|
|
|
with flow_config_path.open('w', encoding='utf-8') as f:
|
|
json.dump(flow_config, f, indent=2)
|
|
|
|
logger.info(f"Thumbnail updated as '{thumbnail_filename}' in flow '{flow_url}'")
|
|
|
|
logger.info(f"{FLOWMSG}: Flow '{flow_url}' updated successfully.")
|
|
return web.json_response({'status': 'success', 'message': f"Flow '{flow_url}' updated successfully."})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error updating flow: {e}")
|
|
return web.Response(status=500, text=f"{FLOWMSG}: Error updating flow: {str(e)}")
|
|
|
|
async def delete_flow_handler(request: web.Request) -> web.Response:
|
|
try:
|
|
flow_url = request.query.get('url', None)
|
|
if not flow_url:
|
|
return web.Response(status=400, text="Missing 'url' parameter")
|
|
|
|
if not SAFE_FOLDER_NAME_REGEX.match(flow_url):
|
|
return web.Response(status=400, text="Invalid 'url' parameter.")
|
|
|
|
flow_folder = FLOWS_PATH / flow_url
|
|
if not flow_folder.exists():
|
|
return web.Response(status=400, text=f"Flow with url '{flow_url}' does not exist")
|
|
|
|
shutil.rmtree(flow_folder)
|
|
|
|
logger.info(f"{FLOWMSG}: Flow '{flow_url}' deleted successfully.")
|
|
return web.json_response({'status': 'success', 'message': f"Flow '{flow_url}' deleted successfully."})
|
|
|
|
except Exception as e:
|
|
logger.error(f"{FLOWMSG}: Error deleting flow: {e}")
|
|
return web.Response(status=500, text=f"{FLOWMSG}: Error deleting flow: {str(e)}")
|
|
|
|
def allowed_file(filename: str) -> bool:
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
|
|
|
def remove_readonly(func, path, excinfo):
|
|
os.chmod(path, stat.S_IWRITE)
|
|
func(path) |