From c05064e6e8a83d9a5a95553f4cd37c297dbaf377 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 20:30:39 -0500 Subject: [PATCH 1/9] Adding rudimentary file browser. --- htdocs/css/files.css | 29 +++++++++++++++++++ htdocs/css/openwebrx.css | 4 +-- htdocs/files.html | 18 ++++++++++++ htdocs/files.js | 17 +++++++++++ htdocs/openwebrx.js | 18 ++++++++++++ owrx/controllers/file.py | 62 ++++++++++++++++++++++++++++++++++++++++ owrx/http.py | 3 ++ 7 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 htdocs/css/files.css create mode 100644 htdocs/files.html create mode 100644 htdocs/files.js create mode 100644 owrx/controllers/file.py diff --git a/htdocs/css/files.css b/htdocs/css/files.css new file mode 100644 index 00000000..b9b77d78 --- /dev/null +++ b/htdocs/css/files.css @@ -0,0 +1,29 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +html, body { + height: unset; +} + +body { + margin-bottom: 5rem; +} + +hr { + background: #444; +} + +h1 { + margin: 1em 0; + text-align: center; +} + +table { + border-collapse: separate; + border-spacing: 15px; +} + +td { + text-align: center; + border: 3px dotted; +} diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index 6fb6e2ed..a55a109d 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -158,8 +158,8 @@ input[type=range]:disabled { { height: 100%; position: relative; - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } #openwebrx-scale-container diff --git a/htdocs/files.html b/htdocs/files.html new file mode 100644 index 00000000..0125d0f4 --- /dev/null +++ b/htdocs/files.html @@ -0,0 +1,18 @@ + + OpenWebRX+ Received Files + + + + + + + + + ${header} +
+

OpenWebRX+ Received Files

+ + ${rows} +
+
+ diff --git a/htdocs/files.js b/htdocs/files.js new file mode 100644 index 00000000..a663729f --- /dev/null +++ b/htdocs/files.js @@ -0,0 +1,17 @@ +function saveCanvas(canvas, name) { + var a = document.createElement('a'); + + canvas.toBlob(function(blob) { + a.href = window.URL.createObjectURL(blob); + }, 'image/png'); + + a.style = 'display: none'; + a.download = name; + document.body.appendChild(a); + a.click(); + + setTimeout(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(a.href); + }, 0); +} diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index 825a2a85..aa23ffce 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -93,6 +93,24 @@ function toggleRecording() { } +function saveCanvas(canvas, name) { + var a = document.createElement('a'); + + canvas.toBlob(function(blob) { + a.href = window.URL.createObjectURL(blob); + }, 'image/png'); + + a.style = 'display: none'; + a.download = name; + document.body.appendChild(a); + a.click(); + + setTimeout(function() { + document.body.removeChild(a); + window.URL.revokeObjectURL(a.href); + }, 0); +} + function zoomInOneStep() { zoom_set(zoom_level + 1); } diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py new file mode 100644 index 00000000..0589693d --- /dev/null +++ b/owrx/controllers/file.py @@ -0,0 +1,62 @@ +from owrx.controllers.template import WebpageController +from owrx.controllers.assets import ModificationAwareController +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +from owrx.controllers.settings import SettingsBreadcrumb +from owrx.config.core import CoreConfig + +from datetime import datetime, timezone +import mimetypes +import os +import re + +class FileController(ModificationAwareController): + def getModified(self, file): + return datetime.fromtimestamp(os.path.getmtime(file), timezone.utc) + + def openFile(self, file): + return open(file, "rb") + + def serve_file(self, file, content_type=None): + try: + modified = self.getModified(file) + + if not self.wasModified(file): + self.send_response("", code=304) + return + + f = self.openFile(file) + data = f.read() + f.close() + + if content_type is None: + (content_type, encoding) = mimetypes.guess_type(file) + self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) + except FileNotFoundError: + self.send_response("file '%s' not found" % file, code=404) + + def indexAction(self): + filename = self.request.matches.group(1) + self.serve_file("/tmp/" + filename) + + +class FilesController(WebpageController): + def template_variables(self): + files = [f for f in os.listdir('/tmp') if re.match(r'SSTV-[0-9]+-[0-9]+\.bmp', f)] + rows = "" + + for i in range(len(files)): + # Start a row + if i % 3 == 0: + rows += '\n' + # Print out individual tiles + rows += ('' % (files[i], files[i])) + ('

%s

\n' % files[i]) + # Finish a row + if i % 3 == 2: + rows += '\n' + + variables = super().template_variables() + variables["rows"] = rows + return variables + + def indexAction(self): + self.serve_template("files.html", **self.template_variables()) diff --git a/owrx/http.py b/owrx/http.py index 347bacff..ebb35b70 100644 --- a/owrx/http.py +++ b/owrx/http.py @@ -1,6 +1,7 @@ from owrx.controllers.status import StatusController from owrx.controllers.template import IndexController, MapController, PolicyController from owrx.controllers.feature import FeatureController +from owrx.controllers.file import FilesController, FileController from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, CompiledAssetsController from owrx.controllers.websocket import WebSocketController from owrx.controllers.api import ApiController @@ -96,6 +97,8 @@ class Router(object): StaticRoute("/map", MapController), StaticRoute("/policy", PolicyController), StaticRoute("/features", FeatureController), + StaticRoute("/files", FilesController), + RegexRoute("^/files/(SSTV-[0-9]+-[0-9]+\.bmp)$", FileController), StaticRoute("/api/features", ApiController), StaticRoute("/metrics", MetricsController, options={"action": "prometheusAction"}), StaticRoute("/metrics.json", MetricsController), From 3d8bfac74dd33e0d8350faf9faffd9bb88349961 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 20:46:59 -0500 Subject: [PATCH 2/9] File browser almost works now. --- htdocs/files.html | 1 - htdocs/files.js | 17 ----------------- owrx/controllers/file.py | 13 +++++++++++-- 3 files changed, 11 insertions(+), 20 deletions(-) delete mode 100644 htdocs/files.js diff --git a/htdocs/files.html b/htdocs/files.html index 0125d0f4..f277b8ce 100644 --- a/htdocs/files.html +++ b/htdocs/files.html @@ -6,7 +6,6 @@ - ${header}
diff --git a/htdocs/files.js b/htdocs/files.js deleted file mode 100644 index a663729f..00000000 --- a/htdocs/files.js +++ /dev/null @@ -1,17 +0,0 @@ -function saveCanvas(canvas, name) { - var a = document.createElement('a'); - - canvas.toBlob(function(blob) { - a.href = window.URL.createObjectURL(blob); - }, 'image/png'); - - a.style = 'display: none'; - a.download = name; - document.body.appendChild(a); - a.click(); - - setTimeout(function() { - document.body.removeChild(a); - window.URL.revokeObjectURL(a.href); - }, 0); -} diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py index 0589693d..36028e04 100644 --- a/owrx/controllers/file.py +++ b/owrx/controllers/file.py @@ -35,8 +35,9 @@ class FileController(ModificationAwareController): self.send_response("file '%s' not found" % file, code=404) def indexAction(self): + tmpDir = CoreConfig().get_temporary_directory() filename = self.request.matches.group(1) - self.serve_file("/tmp/" + filename) + self.serve_file("%s/%s" % (tmpDir, filename)) class FilesController(WebpageController): @@ -49,11 +50,19 @@ class FilesController(WebpageController): if i % 3 == 0: rows += '\n' # Print out individual tiles - rows += ('' % (files[i], files[i])) + ('

%s

\n' % files[i]) + rows += ('' + + ('' % (files[i], files[i])) + + ('' % (files[i], files[i])) + + ('

%s

' % files[i]) + + '
\n') # Finish a row if i % 3 == 2: rows += '\n' + # Finish final row + if len(files) > 0 and len(files) % 3 != 0: + rows += '\n' + variables = super().template_variables() variables["rows"] = rows return variables From 4501c3700fc609e06ce3fbd44c774a24b4072c1b Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 20:54:26 -0500 Subject: [PATCH 3/9] Added top link to files. --- htdocs/include/header.include.html | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html index ea9de175..0a22ae72 100644 --- a/htdocs/include/header.include.html +++ b/htdocs/include/header.include.html @@ -11,6 +11,7 @@

Log

Receiver

Map
+
Files

Settings
From 6f701e02eaeac700a65144c54318e3176b79fcba Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 21:08:38 -0500 Subject: [PATCH 4/9] Table centered now. --- htdocs/css/files.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/htdocs/css/files.css b/htdocs/css/files.css index b9b77d78..58ad79b4 100644 --- a/htdocs/css/files.css +++ b/htdocs/css/files.css @@ -27,3 +27,8 @@ td { text-align: center; border: 3px dotted; } + +img { + width: 100%; +} + From 7a56366cbf62da1da1b6d6a94a23156adb3f28f6 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 22:52:30 -0500 Subject: [PATCH 5/9] Refactoring. --- htdocs/css/files.css | 1 - owrx/controllers/file.py | 57 ++++++++++++++++------------------------ 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/htdocs/css/files.css b/htdocs/css/files.css index 58ad79b4..3d921a49 100644 --- a/htdocs/css/files.css +++ b/htdocs/css/files.css @@ -31,4 +31,3 @@ td { img { width: 100%; } - diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py index 36028e04..4ec64191 100644 --- a/owrx/controllers/file.py +++ b/owrx/controllers/file.py @@ -1,48 +1,35 @@ from owrx.controllers.template import WebpageController -from owrx.controllers.assets import ModificationAwareController -from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin -from owrx.controllers.settings import SettingsBreadcrumb +from owrx.controllers.assets import AssetsController from owrx.config.core import CoreConfig -from datetime import datetime, timezone -import mimetypes import os import re -class FileController(ModificationAwareController): - def getModified(self, file): - return datetime.fromtimestamp(os.path.getmtime(file), timezone.utc) - - def openFile(self, file): - return open(file, "rb") - - def serve_file(self, file, content_type=None): - try: - modified = self.getModified(file) - - if not self.wasModified(file): - self.send_response("", code=304) - return - - f = self.openFile(file) - data = f.read() - f.close() - - if content_type is None: - (content_type, encoding) = mimetypes.guess_type(file) - self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) - except FileNotFoundError: - self.send_response("file '%s' not found" % file, code=404) - - def indexAction(self): - tmpDir = CoreConfig().get_temporary_directory() - filename = self.request.matches.group(1) - self.serve_file("%s/%s" % (tmpDir, filename)) +class FileController(AssetsController): + def getFilePath(self, file): + return CoreConfig().get_temporary_directory() + "/" + file class FilesController(WebpageController): + # Get list of files to work on, sorted in reverse alphabetic + # order (so that newer files appear first) + def getFileList(self): + dir = CoreConfig().get_temporary_directory() + files = [f for f in os.listdir(dir) if re.match(r'SSTV-[0-9]+-[0-9]+\.bmp', f)] + return sorted(files, reverse=True) + + # Delete all files except for newest ones + def cleanFiles(self, keepN): + dir = CoreConfig().get_temporary_directory() + files = self.getFileList() + for f in files[keepN:]: + try: + os.unlink(dir + "/" + f) + except: + pass + def template_variables(self): - files = [f for f in os.listdir('/tmp') if re.match(r'SSTV-[0-9]+-[0-9]+\.bmp', f)] + files = self.getFileList() rows = "" for i in range(len(files)): From 89bc51b1f90e459f1b712f944750a42ca164bdc6 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 23:05:11 -0500 Subject: [PATCH 6/9] Adding limit to the number of stored files. --- owrx/config/defaults.py | 1 + owrx/controllers/file.py | 2 +- owrx/controllers/settings/general.py | 10 ++++++++-- owrx/details.py | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index e028e4db..e6685e86 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -162,6 +162,7 @@ defaultConfig = PropertyLayer( callsign_url="https://www.qrzcq.com/call/{}", usage_policy_url="policy", session_timeout=0, + keep_files=20, decoding_queue_workers=2, decoding_queue_length=10, wsjt_decoding_depth=3, diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py index 4ec64191..fcb5819c 100644 --- a/owrx/controllers/file.py +++ b/owrx/controllers/file.py @@ -25,7 +25,7 @@ class FilesController(WebpageController): for f in files[keepN:]: try: os.unlink(dir + "/" + f) - except: + except Exception: pass def template_variables(self): diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py index fa674166..a96eea1c 100644 --- a/owrx/controllers/settings/general.py +++ b/owrx/controllers/settings/general.py @@ -70,18 +70,24 @@ class GeneralSettingsController(SettingsFormController): NumberInput( "max_clients", "Maximum number of clients", + infotext="Number of people who can connect at the same time.", + ), + NumberInput( + "keep_files", + "Maximum number of files", + infotext="Number of received images and other files to keep.", ), NumberInput( "session_timeout", "Session timeout", - infotext="User session timeout in seconds (0 to disable timeout).", + infotext="Client session timeout in seconds (0 to disable timeout).", append="secs", ), TextInput( "usage_policy_url", "Usage policy URL", infotext="Specifies web page describing receiver usage policy " - + "and shown when the user session times out.", + + "and shown when a client session times out.", ), ), Section( diff --git a/owrx/details.py b/owrx/details.py index 5d979b97..ba672029 100644 --- a/owrx/details.py +++ b/owrx/details.py @@ -19,7 +19,8 @@ class ReceiverDetails(PropertyFilter): "photo_title", "photo_desc", "usage_policy_url", - "session_timeout", + "session_timeout", + "keep_files", ) ) From 75960d06ac120662a545765da8549e1399e62355 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Tue, 21 Feb 2023 23:21:46 -0500 Subject: [PATCH 7/9] Adding stored file management. --- owrx/config/core.py | 19 +++++++++++++++++++ owrx/controllers/file.py | 22 +--------------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/owrx/config/core.py b/owrx/config/core.py index e22f004a..afa7e745 100644 --- a/owrx/config/core.py +++ b/owrx/config/core.py @@ -1,6 +1,7 @@ from owrx.config import ConfigError from configparser import ConfigParser import os +import re from glob import glob @@ -46,6 +47,23 @@ class CoreConfig(object): if not os.access(dir, os.W_OK): raise ConfigError(key, "{dir} is not writable".format(dir=dir)) + # Get list of stored files, sorted in reverse alphabetic order + # (so that newer files appear first) + def getStoredFiles(self): + dir = self.get_temporary_directory() + files = [f for f in os.listdir(dir) if re.match(r"SSTV-[0-9]+-[0-9]+\.bmp", f)] + return sorted(files, reverse=True) + + # Delete all stored files except for newest ones + def cleanStoredFiles(self, keepN): + dir = self.get_temporary_directory() + files = self.getFileList() + for f in files[keepN:]: + try: + os.unlink(dir + "/" + f) + except Exception: + pass + def get_web_port(self): return self.web_port @@ -57,3 +75,4 @@ class CoreConfig(object): def get_aprs_symbols_path(self): return self.aprs_symbols_path + diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py index fcb5819c..becbef96 100644 --- a/owrx/controllers/file.py +++ b/owrx/controllers/file.py @@ -2,34 +2,14 @@ from owrx.controllers.template import WebpageController from owrx.controllers.assets import AssetsController from owrx.config.core import CoreConfig -import os -import re - class FileController(AssetsController): def getFilePath(self, file): return CoreConfig().get_temporary_directory() + "/" + file class FilesController(WebpageController): - # Get list of files to work on, sorted in reverse alphabetic - # order (so that newer files appear first) - def getFileList(self): - dir = CoreConfig().get_temporary_directory() - files = [f for f in os.listdir(dir) if re.match(r'SSTV-[0-9]+-[0-9]+\.bmp', f)] - return sorted(files, reverse=True) - - # Delete all files except for newest ones - def cleanFiles(self, keepN): - dir = CoreConfig().get_temporary_directory() - files = self.getFileList() - for f in files[keepN:]: - try: - os.unlink(dir + "/" + f) - except Exception: - pass - def template_variables(self): - files = self.getFileList() + files = CoreConfig().getStoredFiles() rows = "" for i in range(len(files)): From b643bf83faeffa4e145c7418bbcf6502908e8a57 Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Wed, 22 Feb 2023 12:24:22 -0500 Subject: [PATCH 8/9] Storage cleanup works now. --- owrx/config/core.py | 18 +++++++++++++----- owrx/controllers/file.py | 2 +- owrx/sstv.py | 10 +++++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/owrx/config/core.py b/owrx/config/core.py index afa7e745..1134d86c 100644 --- a/owrx/config/core.py +++ b/owrx/config/core.py @@ -4,6 +4,9 @@ import os import re from glob import glob +import logging + +logger = logging.getLogger(__name__) class CoreConfig(object): defaults = { @@ -47,6 +50,11 @@ class CoreConfig(object): if not os.access(dir, os.W_OK): raise ConfigError(key, "{dir} is not writable".format(dir=dir)) + # Get complete path to a stored file from its filename by + # adding folder name + def getStoredFilePath(self, filename): + return self.get_temporary_directory() + "/" + filename + # Get list of stored files, sorted in reverse alphabetic order # (so that newer files appear first) def getStoredFiles(self): @@ -56,13 +64,13 @@ class CoreConfig(object): # Delete all stored files except for newest ones def cleanStoredFiles(self, keepN): - dir = self.get_temporary_directory() - files = self.getFileList() + files = self.getStoredFiles() for f in files[keepN:]: + logger.debug("Deleting stored file '%s'." % f) try: - os.unlink(dir + "/" + f) - except Exception: - pass + os.unlink(self.getStoredFilePath(f)) + except Exception as exptn: + logger.debug(str(exptn)) def get_web_port(self): return self.web_port diff --git a/owrx/controllers/file.py b/owrx/controllers/file.py index becbef96..c29653d0 100644 --- a/owrx/controllers/file.py +++ b/owrx/controllers/file.py @@ -4,7 +4,7 @@ from owrx.config.core import CoreConfig class FileController(AssetsController): def getFilePath(self, file): - return CoreConfig().get_temporary_directory() + "/" + file + return CoreConfig().getStoredFilePath(file) class FilesController(WebpageController): diff --git a/owrx/sstv.py b/owrx/sstv.py index b7db9ab5..5da7d422 100644 --- a/owrx/sstv.py +++ b/owrx/sstv.py @@ -1,4 +1,5 @@ from owrx.config.core import CoreConfig +from owrx.config import Config from csdr.module import ThreadModule from pycsdr.types import Format from datetime import datetime @@ -80,7 +81,14 @@ class SstvParser(ThreadModule): if self.height==0 or self.line Date: Wed, 22 Feb 2023 16:35:21 -0500 Subject: [PATCH 9/9] Saving SSTV images from the message window works now. --- htdocs/lib/MessagePanel.js | 10 ++++++---- htdocs/openwebrx.js | 16 +++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/htdocs/lib/MessagePanel.js b/htdocs/lib/MessagePanel.js index a7a18c7d..8bbd70c4 100644 --- a/htdocs/lib/MessagePanel.js +++ b/htdocs/lib/MessagePanel.js @@ -297,10 +297,12 @@ SstvMessagePanel.prototype.pushMessage = function(msg) { // $b.scrollTop($b[0].scrollHeight); } else if(msg.width>0 && msg.height>0 && !msg.hasOwnProperty('line')) { - var h = '' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + - ' ' + msg.sstvMode + '
'; - var c = ''; + var h = '
' + msg.timestamp + ' ' + msg.width + 'x' + msg.height + + ' ' + msg.sstvMode + '
'; + var c = '
' + + '
'; // Append a new canvas $b.append($('' + h + c + '')); $b.scrollTop($b[0].scrollHeight); diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index aa23ffce..d9122836 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -93,18 +93,20 @@ function toggleRecording() { } -function saveCanvas(canvas, name) { +function saveCanvas(canvas) { + // Get canvas by its ID + var c = document.getElementById(canvas); + if (c == null) return; + + // Create and click a link to the canvas data URL var a = document.createElement('a'); - - canvas.toBlob(function(blob) { - a.href = window.URL.createObjectURL(blob); - }, 'image/png'); - + a.href = c.toDataURL('image/png'); a.style = 'display: none'; - a.download = name; + a.download = canvas + ".png"; document.body.appendChild(a); a.click(); + // Get rid of the canvas data URL setTimeout(function() { document.body.removeChild(a); window.URL.revokeObjectURL(a.href);