From 74e1709c4c94f702ea486e716e8c887ca4117a13 Mon Sep 17 00:00:00 2001 From: Jan Grewe <jan@faked.org> Date: Mon, 7 Oct 2024 03:48:54 +0200 Subject: [PATCH] implement file upload --- README.md | 5 ++ TODO.txt | 5 ++ main.py | 108 ++++++++++++++++++++++++++++++++++++++-- requirements.txt | 1 + web/index.html | 31 ++++++++++++ web/js/chitui.js | 127 ++++++++++++++++++++++++++++++++++++++--------- web/js/sdcp.js | 1 + 7 files changed, 250 insertions(+), 28 deletions(-) create mode 100644 TODO.txt diff --git a/README.md b/README.md index 6c3cc2c..187e7c5 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,8 @@ As ChitUI needs to broadcast UDP messages on your network segment, running ChitU docker build -t chitui:latest . docker run --rm --name chitui --net=host chitui:latest ``` + +## Configuration +Configuration is done via environment variables: +* `PORT` to set the HTTP port of the web interface (default: `54780`) +* `DEBUG` to enable debug logging, log colorization and code reloading (default: `False`) diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..7dd7e55 --- /dev/null +++ b/TODO.txt @@ -0,0 +1,5 @@ +* upload progress should use to-printer upload +* file deletion +* manual adding of printers +* camera frame/stream +* print job control diff --git a/main.py b/main.py index 7372e72..09d34ca 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ -from flask import Flask, send_file +from flask import Flask, Response, request, jsonify +from werkzeug.utils import secure_filename from flask_socketio import SocketIO from threading import Thread from loguru import logger @@ -8,16 +9,23 @@ import os import websocket import time import sys +import requests +import hashlib +import uuid debug = False log_level = "INFO" -if os.environ.get("DEBUG") is not None and os.environ.get("DEBUG"): +if os.environ.get("DEBUG") is not None: debug = True log_level = "DEBUG" +logger.remove() logger.add(sys.stdout, colorize=debug, level=log_level) port = 54780 +if os.environ.get("PORT") is not None: + port = os.environ.get("PORT") + discovery_timeout = 1 app = Flask(__name__, static_url_path='', @@ -26,12 +34,105 @@ socketio = SocketIO(app) websockets = {} printers = {} +UPLOAD_FOLDER = '/tmp' +ALLOWED_EXTENSIONS = {'ctb', 'goo'} +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + @app.route("/") def web_index(): return app.send_static_file('index.html') +@app.route('/upload', methods=['GET', 'POST']) +def upload_file(): + if request.method == 'POST': + if 'file' not in request.files: + logger.error("No 'file' parameter in request.") + return Response('{"upload": "error", "msg": "Malformed request - no file."}', status=400, mimetype="application/json") + file = request.files['file'] + if file.filename == '': + logger.error('No file selected to be uploaded.') + return Response('{"upload": "error", "msg": "No file selected."}', status=400, mimetype="application/json") + form_data = request.form.to_dict() + if 'printer' not in form_data or form_data['printer'] == "": + logger.error("No 'printer' parameter in request.") + return Response('{"upload": "error", "msg": "Malformed request - no printer."}', status=400, mimetype="application/json") + printer = printers[form_data['printer']] + if file and not allowed_file(file.filename): + logger.error("Invalid filetype.") + return Response('{"upload": "error", "msg": "Invalid filetype."}', status=400, mimetype="application/json") + + filename = secure_filename(file.filename) + filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(filepath) + logger.debug( + "File '{f}' received, uploading to printer '{p}'...", f=filename, p=printer['name']) + upload_file(printer['ip'], filepath) + return Response('{"upload": "success", "msg": "File uploaded"}', status=200, mimetype="application/json") + else: + return Response("u r doin it rong", status=405, mimetype='text/plain') + + +def allowed_file(filename): + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + + +def upload_file(printer_ip, filepath): + part_size = 1048576 + filename = os.path.basename(filepath) + md5_hash = hashlib.md5() + with open(filepath, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + md5_hash.update(byte_block) + file_stats = os.stat(filepath) + post_data = { + 'S-File-MD5': md5_hash.hexdigest(), + 'Check': 1, + 'Offset': 0, + 'Uuid': uuid.uuid4(), + 'TotalSize': file_stats.st_size, + } + url = 'http://{ip}:3030/uploadFile/upload'.format(ip=printer_ip) + num_parts = (int)(file_stats.st_size / part_size) + logger.debug("Uploaded file will be split into {} parts", num_parts) + i = 0 + while i <= num_parts: + offset = i * part_size + with open(filepath, 'rb') as f: + f.seek(offset) + file_part = f.read(part_size) + logger.debug("Uploading part {}/{} (offset: {})", i, num_parts, offset) + if not upload_file_part(url, post_data, filename, file_part, offset): + logger.error("Uploading file to printer failed.") + break + logger.debug("Part {}/{} uploaded.", i, num_parts, offset) + i += 1 + os.remove(filepath) + return True + + +def upload_file_part(url, post_data, file_name, file_part, offset): + post_data['Offset'] = offset + post_files = {'File': (file_name, file_part)} + #post_files = {'File': file_part} + response = requests.post(url, data=post_data, files=post_files) + status = json.loads(response.text) + if status['success']: + return True + logger.error(json.loads(response.text)) + return False + + +def read_in_chunks(file, chunk_size=126976): + while True: + data = file.read(chunk_size) + if not data: + break + yield data + + @socketio.on('connect') def sio_handle_connect(auth): logger.info('Client connected') @@ -178,9 +279,8 @@ def ws_msg_handler(ws, msg): def main(): printers = discover_printers() - if len(printers) > 0: + if printers: connect_printers(printers) - logger.info("Setting up connections: done.") socketio.emit('printers', printers) else: logger.error("No printers discovered.") diff --git a/requirements.txt b/requirements.txt index 2b66ca5..c0c13ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ flask==3.0.3 flask-socketio==5.4.1 websocket-client==1.8.0 +requests==2.32.3 loguru==0.7.2 diff --git a/web/index.html b/web/index.html index a788f29..4512270 100644 --- a/web/index.html +++ b/web/index.html @@ -105,7 +105,38 @@ <div class="tab-content" id="navPanes"></div> </div> + <div class="card mt-3"> + <div class="card-header"> + File Upload + </div> + <div class="card-body"> + <div class=""> + <form enctype="multipart/form-data" method="post" id="formUpload"> + <!-- <label for="formFile" class="form-label">Default file input example</label> --> + <input id="uploadFile" class="form-control" type="file" name="file"> + <input id="uploadPrinter" type="hidden" name="printer" value=""> + <div class="progress mt-3" role="progressbar" aria-label="Example with label" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"> + <div id="progressUpload" class="progress-bar" style="width: 0%">0%</div> + </div> + <button id="btnUpload" type="button" class="btn btn-secondary mt-3">Upload</button> + </form> + </div> + </div> + </div> + + <div id="toastUpload" class="toast position-fixed top-0 end-0 m-3" role="alert" aria-live="assertive" aria-atomic="true"> + <div class="toast-header bg-body-secondary"> + <i class="bi bi-file-earmark-arrow-up"></i> + <strong class="me-auto">File Upload</strong> + <small>11 mins ago</small> + <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> + </div> + <div class="toast-body bg-body-tertiary">Success!</div> + </div> + + </div> + </main> <template id="tmplPrintersListItem"> diff --git a/web/js/chitui.js b/web/js/chitui.js index 95f3814..07e9090 100644 --- a/web/js/chitui.js +++ b/web/js/chitui.js @@ -2,6 +2,7 @@ const socket = io(); var websockets = [] var printers = {} + socket.on("connect", () => { console.log('socket.io connected: ' + socket.id); setServerStatus(true) @@ -48,45 +49,61 @@ socket.on("printer_notice", (data) => { socket.on("printer_status", (data) => { //console.log(JSON.stringify(data)) - var fields = {} + if (!printers[data.MainboardID].hasOwnProperty('status')) { + printers[data.MainboardID]['status'] = {} + } var filter = ['CurrentStatus', 'PrintScreen', 'ReleaseFilm', 'TempOfUVLED', 'TimeLapseStatus', 'PrintInfo'] $.each(data.Status, function (key, val) { if (filter.includes(key)) { - fields[key] = val + if (val.length == 1) { + val = val[0] + } + printers[data.MainboardID]['status'][key] = val } }) - printers[data.MainboardID]['status'] = fields + printer_status = printers[data.MainboardID]['status'] + // update file list on status change from UNKNOWN_8 to Idle + if (typeof printer_status['PreviousStatus'] !== undefined + && printer_status['PreviousStatus'] == SDCP_MACHINE_STATUS_UNKNOWN_8 + && printer_status['CurrentStatus'] == SDCP_MACHINE_STATUS_IDLE) { + socket.emit("printer_files", { id: data.MainboardID, url: '/local' }) + } + printers[data.MainboardID]['status']['PreviousStatus'] = printer_status['CurrentStatus'] updatePrinterStatus(data) }); socket.on("printer_attributes", (data) => { //console.log(JSON.stringify(data)) - var fields = {} + if (!printers[data.MainboardID].hasOwnProperty('attributes')) { + printers[data.MainboardID]['attributes'] = {} + } var filter = ['Resolution', 'XYZsize', 'NumberOfVideoStreamConnected', 'MaximumVideoStreamAllowed', 'UsbDiskStatus', 'Capabilities', 'SupportFileType', 'DevicesStatus', 'ReleaseFilmMax', 'CameraStatus', 'RemainingMemory', 'TLPNoCapPos', 'TLPStartCapPos', 'TLPInterLayers'] $.each(data.Attributes, function (key, val) { if (filter.includes(key)) { - fields[key] = val + printers[data.MainboardID]['attributes'][key] = val } }) - printers[data.MainboardID]['attributes'] = fields //updatePrinterAttributes(data) }); function handle_printer_files(id, data) { - if (printers[id]['files'] == undefined) { - printers[id]['files'] = [] + files = [] + if (printers[id]['files'] !== undefined) { + files = printers[id]['files'] } $.each(data, function (i, f) { if (f.type === 0) { getPrinterFiles(id, f.name) } else { - printers[id]['files'].push(f.name) - createTable('Files', [f.name]) + if (!files.includes(f.name)) { + files.push(f.name) + } } }) + printers[id]['files'] = files + createTable('Files', files) } - function addPrinters(printers) { $.each(printers, function (id, printer) { var template = $("#tmplPrintersListItem").html() @@ -121,7 +138,7 @@ function showPrinter(id) { createTable('Status', p.status, true) createTable('Attributes', p.attributes) createTable('Print', p.status.PrintInfo) - + // only get files once if (printers[id]['files'] == undefined) { getPrinterFiles(id, '/local') @@ -129,27 +146,29 @@ function showPrinter(id) { getPrinterFiles(id, '/usb') } } + + $('#uploadPrinter').val(id) } function createTable(name, data, active = false) { - if ($('#tab-'+name).length == 0) { + if ($('#tab-' + name).length == 0) { var tTab = $("#tmplNavTab").html() var tab = $(tTab) - tab.find('button').attr('id', 'tab-'+name) - tab.find('button').attr('data-bs-target', '#tab'+name) + tab.find('button').attr('id', 'tab-' + name) + tab.find('button').attr('data-bs-target', '#tab' + name) tab.find('button').text(name) - if(active) { + if (active) { tab.find('button').addClass('active') } $('#navTabs').append(tab) } - if ($('#tab'+name).length == 0) { + if ($('#tab' + name).length == 0) { var tPane = $("#tmplNavPane").html() var pane = $(tPane) - pane.attr('id', 'tab'+name) - pane.find('tbody').attr('id', 'table'+name) - if(active) { + pane.attr('id', 'tab' + name) + pane.find('tbody').attr('id', 'table' + name) + if (active) { pane.addClass('active') } $('#navPanes').append(pane) @@ -158,7 +177,8 @@ function createTable(name, data, active = false) { } function fillTable(table, data) { - var t = $('#table'+table) + var t = $('#table' + table) + t.empty() $.each(data, function (key, val) { if (typeof val === 'object') { val = JSON.stringify(val) @@ -192,9 +212,13 @@ function updatePrinterStatus(data) { updatePrinterStatusIcon(data.MainboardID, "info", true) break case SDCP_MACHINE_STATUS_DEVICES_TESTING: - info.text("Device Self-Test") + info.text("Devices Self-Test") updatePrinterStatusIcon(data.MainboardID, "warning", true) break + case SDCP_MACHINE_STATUS_UNKNOWN_8: + info.text("UNKNOWN STATUS") + updatePrinterStatusIcon(data.MainboardID, "info", true) + break default: break } @@ -226,18 +250,73 @@ function setServerStatus(online) { } } -$('.serverStatus').on("mouseenter", function (e) { +$('#btnUpload').on('click', function () { + uploadFile() +}); + +function uploadFile() { + var req = $.ajax({ + url: '/upload', + type: 'POST', + data: new FormData($('#formUpload')[0]), + // Tell jQuery not to process data or worry about content-type + // You *must* include these options! + cache: false, + contentType: false, + processData: false, + // Custom XMLHttpRequest + xhr: function () { + var myXhr = $.ajaxSettings.xhr(); + if (myXhr.upload) { + // For handling the progress of the upload + myXhr.upload.addEventListener('progress', function (e) { + if (e.lengthComputable) { + var percent = Math.floor(e.loaded / e.total * 100); + $('#progressUpload').text(percent + '%').css('width', percent + '%'); + if (percent == 100) { + $('#progressUpload').addClass('progress-bar-striped progress-bar-animated') + } + } + }, false); + } + return myXhr; + } + }) + req.done(function (data) { + $('#uploadFile').val('') + $("#toastUpload").show() + setTimeout(function () { + $("#toastUpload").hide() + }, 3000) + }) + req.fail(function (data) { + alert(data.responseJSON.msg) + }) + req.always(function () { + setTimeout(function () { + $('#progressUpload').text('0%').css('width', '0%').removeClass('progress-bar-striped progress-bar-animated'); + }, 1000) + }) +} + +$('.serverStatus').on('mouseenter', function (e) { if ($(this).hasClass('bi-cloud-check-fill')) { $(this).removeClass('bi-cloud-check-fill').addClass('bi-cloud-plus text-primary') } }); -$('.serverStatus').on("mouseleave", function (e) { + +$('.serverStatus').on('mouseleave', function (e) { $(this).removeClass('bi-cloud-plus text-primary').addClass('bi-cloud-check-fill') }); + $('.serverStatus').on('click', function (e) { socket.emit("printers", "{}") }); +$('#toastUpload .btn-close').on('click', function (e) { + $("#toastUpload").hide() +}); + /* global bootstrap: false */ (() => { 'use strict' diff --git a/web/js/sdcp.js b/web/js/sdcp.js index e4a2802..e07d675 100644 --- a/web/js/sdcp.js +++ b/web/js/sdcp.js @@ -4,6 +4,7 @@ const SDCP_MACHINE_STATUS_PRINTING = 1 // Executing print task const SDCP_MACHINE_STATUS_FILE_TRANSFERRING = 2 // File transfer in progress const SDCP_MACHINE_STATUS_EXPOSURE_TESTING = 3 // Exposure test in progress const SDCP_MACHINE_STATUS_DEVICES_TESTING = 4 //Device self-check in progress +const SDCP_MACHINE_STATUS_UNKNOWN_8 = 8 // UNKNOWN, is sent after file transfer // PRINT_STATUS const SDCP_PRINT_STATUS_IDLE = 0 // Idle const SDCP_PRINT_STATUS_HOMING = 1 // Resetting -- GitLab