pynode-flow 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
pynode/api/services.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""MQTT service management API."""
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, jsonify
|
|
4
|
+
|
|
5
|
+
from pynode.api.helpers import _INVALID_BODY_ERROR, _get_json_body, _json_error
|
|
6
|
+
|
|
7
|
+
services_bp = Blueprint('services', __name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@services_bp.route('/api/services/mqtt', methods=['GET'])
|
|
11
|
+
def list_mqtt_services():
|
|
12
|
+
"""List all MQTT services."""
|
|
13
|
+
try:
|
|
14
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
15
|
+
services = mqtt_manager.list_services()
|
|
16
|
+
return jsonify({'success': True, 'services': services})
|
|
17
|
+
except Exception as e:
|
|
18
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@services_bp.route('/api/services/mqtt', methods=['POST'])
|
|
22
|
+
def create_mqtt_service():
|
|
23
|
+
"""Create a new MQTT service."""
|
|
24
|
+
try:
|
|
25
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
26
|
+
data = _get_json_body()
|
|
27
|
+
if data is None:
|
|
28
|
+
return _json_error(_INVALID_BODY_ERROR, 400)
|
|
29
|
+
|
|
30
|
+
# Validate required fields
|
|
31
|
+
if not data.get('name'):
|
|
32
|
+
return jsonify({'success': False, 'error': 'Service name is required'}), 400
|
|
33
|
+
if not data.get('broker'):
|
|
34
|
+
return jsonify({'success': False, 'error': 'Broker address is required'}), 400
|
|
35
|
+
|
|
36
|
+
service = mqtt_manager.create_service(data)
|
|
37
|
+
return jsonify({
|
|
38
|
+
'success': True,
|
|
39
|
+
'service': {
|
|
40
|
+
'id': service.id,
|
|
41
|
+
'name': service.name,
|
|
42
|
+
'broker': service.broker,
|
|
43
|
+
'port': service.port
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
except Exception as e:
|
|
47
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@services_bp.route('/api/services/mqtt/<service_id>', methods=['GET'])
|
|
51
|
+
def get_mqtt_service(service_id):
|
|
52
|
+
"""Get a specific MQTT service by ID."""
|
|
53
|
+
try:
|
|
54
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
55
|
+
service = mqtt_manager.get_service(service_id)
|
|
56
|
+
if not service:
|
|
57
|
+
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
58
|
+
|
|
59
|
+
return jsonify({
|
|
60
|
+
'success': True,
|
|
61
|
+
'service': service.to_dict()
|
|
62
|
+
})
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@services_bp.route('/api/services/mqtt/<service_id>', methods=['PUT'])
|
|
68
|
+
def update_mqtt_service(service_id):
|
|
69
|
+
"""Update an existing MQTT service."""
|
|
70
|
+
try:
|
|
71
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
72
|
+
data = _get_json_body()
|
|
73
|
+
if data is None:
|
|
74
|
+
return _json_error(_INVALID_BODY_ERROR, 400)
|
|
75
|
+
|
|
76
|
+
service = mqtt_manager.update_service(service_id, data)
|
|
77
|
+
if not service:
|
|
78
|
+
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
79
|
+
|
|
80
|
+
return jsonify({
|
|
81
|
+
'success': True,
|
|
82
|
+
'service': service.to_dict()
|
|
83
|
+
})
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@services_bp.route('/api/services/mqtt/<service_id>', methods=['DELETE'])
|
|
89
|
+
def delete_mqtt_service(service_id):
|
|
90
|
+
"""Delete an MQTT service."""
|
|
91
|
+
try:
|
|
92
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
93
|
+
|
|
94
|
+
if not mqtt_manager.delete_service(service_id):
|
|
95
|
+
return jsonify({
|
|
96
|
+
'success': False,
|
|
97
|
+
'error': 'Service not found or still in use'
|
|
98
|
+
}), 400
|
|
99
|
+
|
|
100
|
+
return jsonify({'success': True})
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@services_bp.route('/api/services/mqtt/<service_id>/test', methods=['POST'])
|
|
106
|
+
def test_mqtt_service(service_id):
|
|
107
|
+
"""Test connection to an MQTT service."""
|
|
108
|
+
try:
|
|
109
|
+
from pynode.nodes.MQTTNode.mqtt_service import mqtt_manager
|
|
110
|
+
|
|
111
|
+
service = mqtt_manager.get_service(service_id)
|
|
112
|
+
if not service:
|
|
113
|
+
return jsonify({'success': False, 'error': 'Service not found'}), 404
|
|
114
|
+
|
|
115
|
+
# Try to connect
|
|
116
|
+
connected = service.connect()
|
|
117
|
+
|
|
118
|
+
return jsonify({
|
|
119
|
+
'success': True,
|
|
120
|
+
'connected': connected,
|
|
121
|
+
'status': 'connected' if connected else 'failed'
|
|
122
|
+
})
|
|
123
|
+
except Exception as e:
|
|
124
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|
pynode/api/sse.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Server-Sent Events debug stream.
|
|
2
|
+
|
|
3
|
+
The broadcast worker itself lives on the WorkflowManager
|
|
4
|
+
(``manager.start_debug_broadcast()`` / ``manager.stop_debug_broadcast()``) so
|
|
5
|
+
each app instance has its own worker thread and client queues.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import queue
|
|
11
|
+
|
|
12
|
+
from flask import Blueprint, Response, stream_with_context
|
|
13
|
+
|
|
14
|
+
from pynode.api.helpers import _get_manager
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
sse_bp = Blueprint('sse', __name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@sse_bp.route('/api/debug/stream')
|
|
22
|
+
def debug_stream():
|
|
23
|
+
"""Server-Sent Events stream for debug messages."""
|
|
24
|
+
manager = _get_manager()
|
|
25
|
+
|
|
26
|
+
def generate():
|
|
27
|
+
# Create a queue for this client
|
|
28
|
+
q = queue.Queue(maxsize=100)
|
|
29
|
+
client_id = id(q)
|
|
30
|
+
with manager.clients_lock:
|
|
31
|
+
manager.debug_message_queues[client_id] = q
|
|
32
|
+
|
|
33
|
+
# Start broadcast thread if not running
|
|
34
|
+
manager.start_debug_broadcast()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
yield 'data: {"type": "connected"}\n\n'
|
|
38
|
+
|
|
39
|
+
while True:
|
|
40
|
+
try:
|
|
41
|
+
# Wait for data from broadcast thread
|
|
42
|
+
data = q.get(timeout=1.0)
|
|
43
|
+
yield f'data: {json.dumps(data)}\n\n'
|
|
44
|
+
except queue.Empty:
|
|
45
|
+
# Send keepalive
|
|
46
|
+
yield 'data: {"type": "keepalive"}\n\n'
|
|
47
|
+
|
|
48
|
+
except GeneratorExit:
|
|
49
|
+
# Client disconnected
|
|
50
|
+
with manager.clients_lock:
|
|
51
|
+
manager.debug_message_queues.pop(client_id, None)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
# Log error and close connection
|
|
54
|
+
logger.error(f"SSE Error: {e}")
|
|
55
|
+
with manager.clients_lock:
|
|
56
|
+
manager.debug_message_queues.pop(client_id, None)
|
|
57
|
+
|
|
58
|
+
return Response(stream_with_context(generate()), mimetype='text/event-stream')
|
pynode/api/uploads.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""File upload API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from flask import Blueprint, jsonify, request
|
|
6
|
+
|
|
7
|
+
from pynode.api.helpers import _get_manager
|
|
8
|
+
|
|
9
|
+
uploads_bp = Blueprint('uploads', __name__)
|
|
10
|
+
|
|
11
|
+
# File upload configuration: uploads may only land in these subdirectories
|
|
12
|
+
# of the manager's upload_base_dir. Anything else is rejected.
|
|
13
|
+
ALLOWED_UPLOAD_SUBDIRS = ('models', 'uploads')
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@uploads_bp.route('/api/upload/file', methods=['POST'])
|
|
17
|
+
def upload_file():
|
|
18
|
+
"""Upload a file (model, video, etc.) and save to the server."""
|
|
19
|
+
try:
|
|
20
|
+
if 'file' not in request.files:
|
|
21
|
+
return jsonify({'success': False, 'error': 'No file provided'}), 400
|
|
22
|
+
|
|
23
|
+
file = request.files['file']
|
|
24
|
+
if not file.filename or file.filename == '':
|
|
25
|
+
return jsonify({'success': False, 'error': 'No file selected'}), 400
|
|
26
|
+
|
|
27
|
+
# Determine upload directory from optional 'directory' field, default to models.
|
|
28
|
+
# Only allowlisted subdirectory names are accepted (no separators, no
|
|
29
|
+
# traversal) and the resolved path must stay inside the upload base dir.
|
|
30
|
+
upload_subdir = request.form.get('directory', 'models')
|
|
31
|
+
normalized_subdir = os.path.normpath(upload_subdir).replace('\\', '/')
|
|
32
|
+
if normalized_subdir not in ALLOWED_UPLOAD_SUBDIRS:
|
|
33
|
+
return jsonify({
|
|
34
|
+
'success': False,
|
|
35
|
+
'error': f"Invalid upload directory. Allowed: {', '.join(ALLOWED_UPLOAD_SUBDIRS)}"
|
|
36
|
+
}), 400
|
|
37
|
+
|
|
38
|
+
base_dir = os.path.realpath(_get_manager().upload_base_dir)
|
|
39
|
+
upload_dir = os.path.realpath(os.path.join(base_dir, normalized_subdir))
|
|
40
|
+
if upload_dir != base_dir and not upload_dir.startswith(base_dir + os.sep):
|
|
41
|
+
return jsonify({
|
|
42
|
+
'success': False,
|
|
43
|
+
'error': 'Invalid upload directory: outside allowed base'
|
|
44
|
+
}), 400
|
|
45
|
+
os.makedirs(upload_dir, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
# Save the file
|
|
48
|
+
filename = os.path.basename(file.filename)
|
|
49
|
+
file_path = os.path.join(upload_dir, filename)
|
|
50
|
+
file.save(file_path)
|
|
51
|
+
|
|
52
|
+
return jsonify({
|
|
53
|
+
'success': True,
|
|
54
|
+
'model_path': file_path,
|
|
55
|
+
'file_path': file_path,
|
|
56
|
+
'filename': filename
|
|
57
|
+
})
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return jsonify({'success': False, 'error': str(e)}), 500
|