From 0a4019a4da99e219f461c1a0a1988e69ebda7d8d Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Wed, 14 Jan 2026 16:41:10 +0100 Subject: [PATCH 1/3] refactor(bmap): move bmap parser to utils By moving it to the utils, we make it reusable from both the client, as well as from the webserver. Signed-off-by: Felix Moessbauer --- mtda/client.py | 34 ++-------------------------------- mtda/utils.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/mtda/client.py b/mtda/client.py index 1d44c240..0471d235 100644 --- a/mtda/client.py +++ b/mtda/client.py @@ -20,7 +20,7 @@ import zstandard as zstd from mtda.main import MultiTenantDeviceAccess -from mtda.utils import Compression +from mtda.utils import Compression, BmapUtils import mtda.constants as CONSTS # Pyro @@ -216,7 +216,7 @@ def storage_write_image(self, path): bmap = ET.fromstring(bmap) print(f"Discovered bmap file '{bmap_path}'") - bmapDict = self.parseBmap(bmap, bmap_path) + bmapDict = BmapUtils.parseBmap(bmap, bmap_path) self._impl.storage_bmap_dict(bmapDict) image_size = bmapDict['ImageSize'] break @@ -244,36 +244,6 @@ def storage_write_image(self, path): self.storage_close() self._impl.storage_bmap_dict(None) - def parseBmap(self, bmap, bmap_path): - try: - bmapDict = {} - bmapDict["BlockSize"] = int( - bmap.find("BlockSize").text.strip()) - bmapDict["BlocksCount"] = int( - bmap.find("BlocksCount").text.strip()) - bmapDict["MappedBlocksCount"] = int( - bmap.find("MappedBlocksCount").text.strip()) - bmapDict["ImageSize"] = int( - bmap.find("ImageSize").text.strip()) - bmapDict["ChecksumType"] = \ - bmap.find("ChecksumType").text.strip() - bmapDict["BmapFileChecksum"] = \ - bmap.find("BmapFileChecksum").text.strip() - bmapDict["BlockMap"] = [] - for child in bmap.find("BlockMap").findall("Range"): - range = child.text.strip().split("-") - first = range[0] - last = range[0] if len(range) == 1 else range[1] - bmapDict["BlockMap"].append({ - "first": int(first), - "last": int(last), - "chksum": child.attrib["chksum"] - }) - except Exception: - print(f"Error parsing '{bmap_path}', probably not a bmap 2.0 file") - return None - return bmapDict - def start(self): return self._agent.start() diff --git a/mtda/utils.py b/mtda/utils.py index 933fdca2..2e11a90c 100644 --- a/mtda/utils.py +++ b/mtda/utils.py @@ -18,6 +18,38 @@ import mtda.constants as CONSTS +class BmapUtils: + def parseBmap(bmap, bmap_path): + try: + bmapDict = {} + bmapDict["BlockSize"] = int( + bmap.find("BlockSize").text.strip()) + bmapDict["BlocksCount"] = int( + bmap.find("BlocksCount").text.strip()) + bmapDict["MappedBlocksCount"] = int( + bmap.find("MappedBlocksCount").text.strip()) + bmapDict["ImageSize"] = int( + bmap.find("ImageSize").text.strip()) + bmapDict["ChecksumType"] = \ + bmap.find("ChecksumType").text.strip() + bmapDict["BmapFileChecksum"] = \ + bmap.find("BmapFileChecksum").text.strip() + bmapDict["BlockMap"] = [] + for child in bmap.find("BlockMap").findall("Range"): + range = child.text.strip().split("-") + first = range[0] + last = range[0] if len(range) == 1 else range[1] + bmapDict["BlockMap"].append({ + "first": int(first), + "last": int(last), + "chksum": child.attrib["chksum"] + }) + except Exception: + print(f"Error parsing '{bmap_path}', probably not a bmap 2.0 file") + return None + return bmapDict + + class Compression: def from_extension(path): if path.endswith(".bz2"): From e8fe30acfae7c62a9522c8753ab30cfa492e1c69 Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Wed, 14 Jan 2026 17:33:19 +0100 Subject: [PATCH 2/3] fix(www-storage-write): port to remote call interface The StorageOpenHandler still required the mtda instance as second argument. Since the introduction of the Pyro proxy, this interface does not exist anymore and needs to be ported to the new remote_call proxy. This apparently was forgotten for the storage_open API, hence these requests (like storage-write) always failed. Fixes: 7e13aac9 ("fix(www): create a Pyro proxy from the worker ...") Signed-off-by: Felix Moessbauer --- mtda-www | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/mtda-www b/mtda-www index 4360cc74..e94c4d67 100755 --- a/mtda-www +++ b/mtda-www @@ -719,7 +719,15 @@ class StorageOpenHandler(RemoteCallHandler): } }}} - async def _handle_remote_request(self, mtda): + async def _handle_remote_request(self): + @remote_call + def storage_compression(mtda, compr): + return mtda.storage_compression(compr) + + @remote_call + def storage_open(mtda, size, session): + return mtda.storage_open(size, session=session) + compr = CONSTS.IMAGE.RAW.value file = self.get_argument('file') try: @@ -729,11 +737,11 @@ class StorageOpenHandler(RemoteCallHandler): if file: logger.debug(f'file to be uploaded: {file}') compr = Compression.from_extension(file) - await self.blocking_call(mtda.storage_compression, compr) + await self.blocking_call(storage_compression, compr) sid = self.get_argument('session') zmq_socket = await self.blocking_call( - mtda.storage_open, size, session=sid + storage_open, size, session=sid ) self.application.settings['sockets'][sid] = zmq_socket return 204, None From ac11d90a5fdd76df0313261544b3f3b01d38a71f Mon Sep 17 00:00:00 2001 From: Felix Moessbauer Date: Wed, 14 Jan 2026 17:38:40 +0100 Subject: [PATCH 3/3] feat(www): allow to flash image with bmap file The mtda-cli already supports to write the image using a bmap file. By that, writing images with large holes is speedup a lot. Further, the image data ranges are checksumed. We now implement the same feature for the web UI as well. Signed-off-by: Felix Moessbauer --- mtda-www | 61 ++++++++++++++++++++++++++++++++++++++- mtda/templates/index.html | 51 ++++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/mtda-www b/mtda-www index e94c4d67..a828be0a 100755 --- a/mtda-www +++ b/mtda-www @@ -26,7 +26,7 @@ import logging import functools from mtda.client import Client -from mtda.utils import Compression +from mtda.utils import BmapUtils, Compression import mtda.constants as CONSTS from mtda.console.remote import RemoteConsole from mtda.console.screen import ScreenOutput @@ -121,6 +121,9 @@ class RemoteCallHandler(BaseHandler): finally: self.finish() + async def post(self): + await self.get() + async def blocking_call(self, func, *args, **kwargs): """ Shorthand to run a blocking function in the application's @@ -681,6 +684,60 @@ class PowerToggleHandler(RemoteCallHandler): status = await self.blocking_call(target_toggle, session=sid) return 200, {"status": status} +# --------------------------------------------------------------------------- +# /storage-set-bmap +# --------------------------------------------------------------------------- + + +class StorageBmapHandler(RemoteCallHandler): + spec = {"post": { + "summary": "Set the bmap file corresponding to the current file", + "requestBody": { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "filename": { + "type": "string", + "format": "binary" + }, + "session": { + "type": "string" + } + }, + "required": ["filename", "session"] + } + } + } + }, + "responses": { + "204": { + "description": "bmap file set" + }, + "400": { + "description": "Invalid input parameters" + } + }}} + + async def _handle_remote_request(self): + @remote_call + def storage_bmap_dict(mtda, bmap, session): + mtda.storage_bmap_dict(bmap, session=session) + + import xml.etree.ElementTree as ET + + sid = self.get_argument('session') + file_info = self.request.files.get('filename')[0] + file_content = file_info['body'].decode('utf-8') + filename = file_info['filename'] + + bmap_xml = ET.fromstring(file_content) + bmap = BmapUtils.parseBmap(bmap_xml, filename) + await self.blocking_call(storage_bmap_dict, bmap, session=sid) + return 204, None + # --------------------------------------------------------------------------- # /storage-open # --------------------------------------------------------------------------- @@ -1174,6 +1231,7 @@ class OpenAPIHandler(RemoteCallHandler): "/keyboard-input": KeyboardInputHandler.spec, "/mouse-move": MouseEventHandler.spec, "/power-toggle": PowerToggleHandler.spec, + "/storage-set-bmap": StorageBmapHandler.spec, "/storage-open": StorageOpenHandler.spec, "/storage-close": StorageCloseHandler.spec, "/storage-commit": StorageCommitHandler.spec, @@ -1288,6 +1346,7 @@ class Service: (r"/keyboard-input", KeyboardInputHandler), (r"/mouse-move", MouseEventHandler), (r"/power-toggle", PowerToggleHandler), + (r"/storage-set-bmap", StorageBmapHandler), (r"/storage-close", StorageCloseHandler), (r"/storage-commit", StorageCommitHandler), (r"/storage-open", StorageOpenHandler), diff --git a/mtda/templates/index.html b/mtda/templates/index.html index f85bad35..d15b7654 100644 --- a/mtda/templates/index.html +++ b/mtda/templates/index.html @@ -461,7 +461,7 @@ }); } uploadWindow = new WinBox("Upload", { - html: "
Drag and drop your file here
", + html: "
Drag and drop your files here (e.g. image + bmap)
", class: ["no-full", "no-max", "modern"], width: "600px", height: "340px", @@ -634,17 +634,35 @@ const upload = document.getElementById('upload'); const CHUNK_SIZE = 512 * 1024; let selectedFile = null; + let selectedBmapFile = null; let maySend = false; - $(function() { - $('#upload').bind('click', function() { - $.getJSON('./storage-open', { + async function storage_set_bmap() { + const formData = new FormData(); + formData.append('filename', selectedBmapFile); + formData.append('session', localStorage.getItem('session')); + + await fetch('./storage-set-bmap', { + method: 'POST', + body: formData + }); + } + + async function storage_open() { + await new Promise(done => $.getJSON('./storage-open', { file: selectedFile.name, size: selectedFile.size, session: localStorage.getItem('session') - }, function(data) { - // do nothing - }); + }, function (data) { done(); })); + } + + $(function() { + $('#upload').bind('click', function() { + if (selectedBmapFile) { + storage_set_bmap().then(storage_open()); + } else { + storage_open(); + } return false; }); }); @@ -843,11 +861,26 @@ dropzone.addEventListener('drop', (event) => { const files = event.dataTransfer.files; - if (files.length) { - selectedFile = files[0]; + selectedFile = null; + selectedBmapFile = null; + for (const f of files) { + if (f.name.endsWith('.bmap')) + selectedBmapFile = f; + else + selectedFile = f; + } + if (selectedFile && selectedBmapFile) { + console.log(`will upload ${selectedFile.name} with bmap ${selectedBmapFile.name}`); + dropzone.textContent = `${selectedFile.name} (+ bmap)`; + upload.disabled = false; + } else if (selectedFile) { console.log('will upload '+selectedFile.name); dropzone.textContent = `${selectedFile.name}`; upload.disabled = false; + } else if (selectedBmapFile) { + console.log('only bmap file provided '+selectedBmapFile.name); + dropzone.textContent = `Only bmap file ${selectedBmapFile.name} provided`; + upload.disabled = true; } });