diff --git a/.gitignore b/.gitignore index b4cc8ea..465d3eb 100644 --- a/.gitignore +++ b/.gitignore @@ -133,4 +133,4 @@ dmypy.json deb_dist .DS_Store install/windows/Dangerzone.wxs -test-docs/sample-safe.pdf \ No newline at end of file +test_docs/sample-safe.pdf \ No newline at end of file diff --git a/BUILD.md b/BUILD.md index 8126674..c68b2da 100644 --- a/BUILD.md +++ b/BUILD.md @@ -5,7 +5,7 @@ Install dependencies: ```sh -sudo apt install -y dh-python python3 python3-stdeb python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtwidgets python3-appdirs python3-click python3-xdg python3-requests python3-termcolor +sudo apt install -y dh-python python3 python3-stdeb python3-pyside2.qtcore python3-pyside2.qtgui python3-pyside2.qtwidgets python3-appdirs python3-click python3-xdg python3-requests python3-colorama ``` You also need docker, either by installing the [Docker snap package](https://snapcraft.io/docker), installing the `docker.io` package, or by installing `docker-ce` by following [these instructions for Ubuntu](https://docs.docker.com/install/linux/docker-ce/ubuntu/) or [for Debian](https://docs.docker.com/install/linux/docker-ce/debian/). @@ -27,7 +27,7 @@ Create a .deb: Install dependencies: ```sh -sudo dnf install -y rpm-build python3 python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-termcolor +sudo dnf install -y rpm-build python3 python3-pyside2 python3-appdirs python3-click python3-pyxdg python3-requests python3-colorama ``` You also need docker, either by installing the `docker` package, or by installing `docker-ce` by following [these instructions](https://docs.docker.com/install/linux/docker-ce/fedora/). @@ -61,7 +61,14 @@ poetry install Run from source tree: ``` -poetry run dangerzone +# start a shell in the virtual environment +poetry shell + +# run the CLI +./dev_scripts/dangerzone-cli --help + +# run the GUI +./dev_scripts/dangerzone ``` To create an app bundle, use the `build_app.py` script: diff --git a/RELEASE.md b/RELEASE.md index b2022d8..b745021 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -7,7 +7,7 @@ This section documents the release process. Unless you're a dangerzone developer Before making a release, all of these should be complete: * Update `version` in `pyproject.toml` -* Update `dangerzone_version` in `dangerzone/__init__.py` +* Update in `share/version.txt` * Update version and download links in `README.md` * CHANGELOG.md should be updated to include a list of all major changes since the last release * There must be a PGP-signed git tag for the version, e.g. for dangerzone 0.1.0, the tag must be `v0.1.0` diff --git a/dangerzone/__init__.py b/dangerzone/__init__.py index 8edcba8..d89750d 100644 --- a/dangerzone/__init__.py +++ b/dangerzone/__init__.py @@ -1,13 +1,14 @@ import os import sys -dangerzone_version = "0.1.5" +# Depending on the filename, decide if we want to run: +# dangerzone, dangerzone-cli, or dangerzone-container -# This is a hack for Windows and Mac to be able to run dangerzone-container, even though -# PyInstaller builds a single binary basename = os.path.basename(sys.argv[0]) + if basename == "dangerzone-container" or basename == "dangerzone-container.exe": from .container import container_main as main +elif basename == "dangerzone-cli" or basename == "dangerzone-cli.exe": + from .cli import cli_main as main else: - # If the binary isn't "dangerzone-contatiner", then launch the GUI from .gui import gui_main as main diff --git a/dangerzone/cli.py b/dangerzone/cli.py new file mode 100644 index 0000000..f94ef14 --- /dev/null +++ b/dangerzone/cli.py @@ -0,0 +1,205 @@ +import os +import shutil +import click +from colorama import Fore, Back, Style + +from .global_common import GlobalCommon +from .common import Common + + +def print_header(s): + click.echo("") + click.echo(Style.BRIGHT + Fore.LIGHTWHITE_EX + s) + + +def exec_container(global_common, args): + output = "" + + with global_common.exec_dangerzone_container(args) as p: + for line in p.stdout: + output += line.decode() + + # Hack to add colors to the command executing + if line.startswith(b"\xe2\x80\xa3 "): + print( + Fore.WHITE + "\u2023 " + Fore.LIGHTCYAN_EX + line.decode()[2:], + end="", + ) + else: + print(" " + line.decode(), end="") + + stderr = p.stderr.read().decode() + if len(stderr) > 0: + print("") + for line in stderr.strip().split("\n"): + print(" " + Style.DIM + line) + + if p.returncode != 0: + click.echo(f"Return code: {p.returncode}") + if p.returncode == 126 or p.returncode == 127: + click.echo(f"Authorization failed") + + return p.returncode, output, stderr + + +@click.command() +@click.option("--custom-container", help="Use a custom container") +@click.option("--safe-pdf-filename", help="Default is filename ending with -safe.pdf") +@click.option("--ocr-lang", help="Language to OCR, defaults to none") +@click.option( + "--skip-update", + is_flag=True, + help="Don't update flmcode/dangerzone container", +) +@click.argument("filename", required=True) +def cli_main(custom_container, safe_pdf_filename, ocr_lang, skip_update, filename): + global_common = GlobalCommon() + common = Common() + + global_common.display_banner() + + # Validate filename + valid = True + try: + with open(os.path.abspath(filename), "rb") as f: + pass + except: + valid = False + + if not valid: + click.echo("Invalid filename") + return + + common.document_filename = os.path.abspath(filename) + + # Validate safe PDF output filename + if safe_pdf_filename: + valid = True + if not safe_pdf_filename.endswith(".pdf"): + click.echo("Safe PDF filename must end in '.pdf'") + return + + try: + with open(os.path.abspath(safe_pdf_filename), "wb") as f: + pass + except: + valid = False + + if not valid: + click.echo("Safe PDF filename is not writable") + return + + common.save_filename = os.path.abspath(safe_pdf_filename) + + else: + common.save_filename = ( + f"{os.path.splitext(common.document_filename)[0]}-safe.pdf" + ) + try: + with open(common.save_filename, "wb") as f: + pass + except: + click.echo( + f"Output filename {common.save_filename} is not writable, use --safe-pdf-filename" + ) + return + + # Validate OCR language + if ocr_lang: + valid = False + for lang in global_common.ocr_languages: + if global_common.ocr_languages[lang] == ocr_lang: + valid = True + break + if not valid: + click.echo("Invalid OCR language code. Valid language codes:") + for lang in global_common.ocr_languages: + click.echo(f"{global_common.ocr_languages[lang]}: {lang}") + return + + # Validate custom container + if custom_container: + success, error_message = global_common.container_exists(custom_container) + if not success: + click.echo(error_message) + return + + global_common.custom_container = custom_container + else: + if skip_update: + # Make sure flmcode/dangerzone exists + success, error_message = global_common.container_exists( + "flmcode/dangerzone" + ) + if not success: + click.echo( + "You don't have the flmcode/dangerzone container so you can't use --skip-update" + ) + return + + # Pull the latest image + if not skip_update: + print_header("Pulling container image (this might take a few minutes)") + returncode, _, _ = exec_container(global_common, ["pull"]) + if returncode != 0: + return + + # Convert to pixels + print_header("Converting document to pixels") + returncode, output, _ = exec_container( + global_common, + [ + "documenttopixels", + "--document-filename", + common.document_filename, + "--pixel-dir", + common.pixel_dir.name, + "--container-name", + global_common.get_container_name(), + ], + ) + + if returncode != 0: + return + + success, error_message = global_common.validate_convert_to_pixel_output( + common, output + ) + if not success: + click.echo(error_message) + return + + # Convert to PDF + print_header("Converting pixels to safe PDF") + + if ocr_lang: + ocr = "1" + else: + ocr = "0" + ocr_lang = "" + + returncode, _, _ = exec_container( + global_common, + [ + "pixelstopdf", + "--pixel-dir", + common.pixel_dir.name, + "--safe-dir", + common.safe_dir.name, + "--container-name", + global_common.get_container_name(), + "--ocr", + ocr, + "--ocr-lang", + ocr_lang, + ], + ) + + if returncode != 0: + return + + # Save the safe PDF + source_filename = f"{common.safe_dir.name}/safe-output-compressed.pdf" + shutil.move(source_filename, common.save_filename) + print_header("Safe PDF created successfully") + click.echo(common.save_filename) diff --git a/dangerzone/common.py b/dangerzone/common.py index d50a51f..d8630d6 100644 --- a/dangerzone/common.py +++ b/dangerzone/common.py @@ -1,4 +1,5 @@ import os +import stat import platform import tempfile @@ -30,6 +31,20 @@ class Common(object): prefix=os.path.join(cache_dir, "safe-") ) + # Make the folders world-readable to ensure that the container has permission + # to access it even if it's owned by root or someone else + permissions = ( + stat.S_IRUSR + | stat.S_IWUSR + | stat.S_IXUSR + | stat.S_IRGRP + | stat.S_IXGRP + | stat.S_IROTH + | stat.S_IXOTH + ) + os.chmod(self.pixel_dir.name, permissions) + os.chmod(self.safe_dir.name, permissions) + # Name of input and out files self.document_filename = None self.save_filename = None diff --git a/dangerzone/container.py b/dangerzone/container.py index 3a745fd..a0e1a5a 100644 --- a/dangerzone/container.py +++ b/dangerzone/container.py @@ -3,7 +3,6 @@ import platform import subprocess import sys import pipes -import getpass import shutil # What is the container runtime for this platform? @@ -26,7 +25,7 @@ def exec_container(args): args = [container_runtime] + args args_str = " ".join(pipes.quote(s) for s in args) - sys.stdout.write(f"Executing: {args_str}\n\n") + print("\u2023 " + args_str) # ‣ sys.stdout.flush() with subprocess.Popen( diff --git a/dangerzone/global_common.py b/dangerzone/global_common.py index 0f74a6e..e19680d 100644 --- a/dangerzone/global_common.py +++ b/dangerzone/global_common.py @@ -1,26 +1,14 @@ import sys import os import inspect -import tempfile import appdirs import platform import subprocess -import shlex import pipes -from PySide2 import QtCore, QtGui, QtWidgets - -if platform.system() == "Darwin": - import CoreServices - import LaunchServices - import plistlib - -elif platform.system() == "Linux": - import grp - import getpass - from xdg.DesktopEntry import DesktopEntry +import colorama +from colorama import Fore, Back, Style from .settings import Settings -from .docker_installer import is_docker_ready class GlobalCommon(object): @@ -28,18 +16,13 @@ class GlobalCommon(object): The GlobalCommon class is a singleton of shared functionality throughout the app """ - def __init__(self, app): - # Qt app - self.app = app + def __init__(self): + # Version + with open(self.get_resource_path("version.txt")) as f: + self.version = f.read().strip() - # Name of input file - self.document_filename = None - - # Name of output file - self.save_filename = None - - # Preload font - self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + # Initialize terminal colors + colorama.init(autoreset=True) # App data folder self.appdata_path = appdirs.user_config_dir("dangerzone") @@ -50,9 +33,6 @@ class GlobalCommon(object): # dangerzone-container path self.dz_container_path = self.get_dangerzone_container_path() - # Preload list of PDF viewers on computer - self.pdf_viewers = self._find_pdf_viewers() - # Languages supported by tesseract self.ocr_languages = { "Afrikaans": "ar", @@ -220,6 +200,181 @@ class GlobalCommon(object): # Load settings self.settings = Settings(self) + def display_banner(self): + """ + Raw ASCII art example: + ╭──────────────────────────╮ + │ ▄██▄ │ + │ ██████ │ + │ ███▀▀▀██ │ + │ ███ ████ │ + │ ███ ██████ │ + │ ███ ▀▀▀▀████ │ + │ ███████ ▄██████ │ + │ ███████ ▄█████████ │ + │ ████████████████████ │ + │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ + │ │ + │ Dangerzone v0.1.5 │ + │ https://dangerzone.rocks │ + ╰──────────────────────────╯ + """ + + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╭──────────────────────────╮") + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▄██▄ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███▀▀▀██ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███ ▀▀▀▀████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄██████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ███████ ▄█████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ████████████████████ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Fore.LIGHTYELLOW_EX + + Style.NORMAL + + " ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Back.BLACK + Fore.YELLOW + Style.DIM + "│ │") + left_spaces = (15 - len(self.version) - 1) // 2 + right_spaces = left_spaces + if left_spaces + len(self.version) + 1 + right_spaces < 15: + right_spaces += 1 + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + + Style.BRIGHT + + f"{' '*left_spaces}Dangerzone v{self.version}{' '*right_spaces}" + + Fore.YELLOW + + Style.DIM + + "│" + ) + print( + Back.BLACK + + Fore.YELLOW + + Style.DIM + + "│" + + Style.RESET_ALL + + Back.BLACK + + Fore.LIGHTWHITE_EX + + " https://dangerzone.rocks " + + Fore.YELLOW + + Style.DIM + + "│" + ) + print(Back.BLACK + Fore.YELLOW + Style.DIM + "╰──────────────────────────╯") + def get_container_name(self): if self.custom_container: return self.custom_container @@ -287,7 +442,7 @@ class GlobalCommon(object): # Execute dangerzone-container args_str = " ".join(pipes.quote(s) for s in args) - print(f"Executing: {args_str}") + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ return subprocess.Popen( args, startupinfo=self.get_subprocess_startupinfo(), @@ -295,187 +450,6 @@ class GlobalCommon(object): stderr=subprocess.PIPE, ) - def get_window_icon(self): - if platform.system() == "Windows": - path = self.get_resource_path("dangerzone.ico") - else: - path = self.get_resource_path("icon.png") - return QtGui.QIcon(path) - - def open_pdf_viewer(self, filename): - if self.settings.get("open_app") in self.pdf_viewers: - if platform.system() == "Darwin": - # Get the PDF reader bundle command - bundle_identifier = self.pdf_viewers[self.settings.get("open_app")] - args = ["open", "-b", bundle_identifier, filename] - - # Run - print(f"Executing: {' '.join(args)}") - subprocess.run(args) - - elif platform.system() == "Linux": - # Get the PDF reader command - args = shlex.split(self.pdf_viewers[self.settings.get("open_app")]) - # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open - for i in range(len(args)): - if ( - args[i] == "%f" - or args[i] == "%F" - or args[i] == "%u" - or args[i] == "%U" - ): - args[i] = filename - - # Open as a background process - print(f"Executing: {' '.join(args)}") - subprocess.Popen(args) - - def _find_pdf_viewers(self): - pdf_viewers = {} - - if platform.system() == "Darwin": - # Get all installed apps that can open PDFs - bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( - "com.adobe.pdf", CoreServices.kLSRolesAll - ) - for bundle_identifier in bundle_identifiers: - # Get the filesystem path of the app - res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( - bundle_identifier, None - ) - if res[0] is None: - continue - app_url = res[0][0] - app_path = str(app_url.path()) - - # Load its plist file - plist_path = os.path.join(app_path, "Contents/Info.plist") - - # Skip if there's not an Info.plist - if not os.path.exists(plist_path): - continue - - with open(plist_path, "rb") as f: - plist_data = f.read() - - plist_dict = plistlib.loads(plist_data) - - if ( - plist_dict.get("CFBundleName") - and plist_dict["CFBundleName"] != "Dangerzone" - ): - pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier - - elif platform.system() == "Linux": - # Find all .desktop files - for search_path in [ - "/usr/share/applications", - "/usr/local/share/applications", - os.path.expanduser("~/.local/share/applications"), - ]: - try: - for filename in os.listdir(search_path): - full_filename = os.path.join(search_path, filename) - if os.path.splitext(filename)[1] == ".desktop": - - # See which ones can open PDFs - desktop_entry = DesktopEntry(full_filename) - if ( - "application/pdf" in desktop_entry.getMimeTypes() - and desktop_entry.getName() != "dangerzone" - ): - pdf_viewers[ - desktop_entry.getName() - ] = desktop_entry.getExec() - - except FileNotFoundError: - pass - - return pdf_viewers - - def ensure_docker_group_preference(self): - # If the user prefers typing their password - if self.settings.get("linux_prefers_typing_password") == True: - return True - - # Get the docker group - try: - groupinfo = grp.getgrnam("docker") - except: - # Ignore if group is not found - return True - - # See if the user is in the group - username = getpass.getuser() - if username not in groupinfo.gr_mem: - # User is not in the docker group, ask if they prefer typing their password - message = "Dangerzone requires Docker

In order to use Docker, your user must be in the 'docker' group or you'll need to type your password each time you run dangerzone.

Adding your user to the 'docker' group is more convenient but less secure, and will require just typing your password once. Which do you prefer?" - return_code = Alert( - self, - message, - ok_text="I'll type my password each time", - extra_button_text="Add my user to the 'docker' group", - ).launch() - if return_code == QtWidgets.QDialog.Accepted: - # Prefers typing password - self.settings.set("linux_prefers_typing_password", True) - self.settings.save() - return True - elif return_code == 2: - # Prefers being in the docker group - self.settings.set("linux_prefers_typing_password", False) - self.settings.save() - - # Add user to the docker group - p = subprocess.run( - [ - "/usr/bin/pkexec", - "/usr/sbin/usermod", - "-a", - "-G", - "docker", - username, - ] - ) - if p.returncode == 0: - message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." - Alert(self, message).launch() - else: - message = "Failed to add your user to the 'docker' group, quitting." - Alert(self, message).launch() - - return False - else: - # Cancel - return False - - return True - - def ensure_docker_service_is_started(self): - if not is_docker_ready(self): - message = "Dangerzone requires Docker

Docker should be installed, but it looks like it's not running in the background.

Click Ok to try starting the docker service. You will have to type your login password." - if Alert(self, message).launch() == QtWidgets.QDialog.Accepted: - p = subprocess.run( - [ - "/usr/bin/pkexec", - self.get_resource_path("enable_docker_service.sh"), - ] - ) - if p.returncode == 0: - # Make sure docker is now ready - if is_docker_ready(self): - return True - else: - message = "Restarting docker appeared to work, but the service still isn't responding, quitting." - Alert(self, message).launch() - else: - message = "Failed to start the docker service, quitting." - Alert(self, message).launch() - - return False - - return True - def get_subprocess_startupinfo(self): if platform.system() == "Windows": startupinfo = subprocess.STARTUPINFO() @@ -484,70 +458,99 @@ class GlobalCommon(object): else: return None + def container_exists(self, container_name): + """ + Check if container_name is a valid container. Returns a tuple like: + (success (boolean), error_message (str)) + """ + # Do we have this container? + with self.exec_dangerzone_container( + ["ls", "--container-name", container_name] + ) as p: + stdout_data, _ = p.communicate() + lines = stdout_data.split(b"\n") + if b"\u2023 " in lines[0]: # ‣ + stdout_data = b"\n".join(lines[1:]) -class Alert(QtWidgets.QDialog): - def __init__(self, common, message, ok_text="Ok", extra_button_text=None): - super(Alert, self).__init__() - self.common = common + # The user canceled, or permission denied + if p.returncode == 126 or p.returncode == 127: + return False, "Authorization failed" + return + elif p.returncode != 0: + return False, "Container error" + return - self.setWindowTitle("dangerzone") - self.setWindowIcon(self.common.get_window_icon()) - self.setModal(True) + # Check the output + if container_name.encode() not in stdout_data: + return False, f"Container '{container_name}' not found" - flags = ( - QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.WindowTitleHint - | QtCore.Qt.WindowSystemMenuHint - | QtCore.Qt.WindowCloseButtonHint - | QtCore.Qt.WindowStaysOnTopHint - ) - self.setWindowFlags(flags) + return True, True - logo = QtWidgets.QLabel() - logo.setPixmap( - QtGui.QPixmap.fromImage( - QtGui.QImage(self.common.get_resource_path("icon.png")) + def validate_convert_to_pixel_output(self, common, output): + """ + Take the output from the convert to pixels tasks and validate it. Returns + a tuple like: (success (boolean), error_message (str)) + """ + max_image_width = 10000 + max_image_height = 10000 + + # Did we hit an error? + for line in output.split("\n"): + if ( + "failed:" in line + or "The document format is not supported" in line + or "Error" in line + ): + return False, output + + # How many pages was that? + num_pages = None + for line in output.split("\n"): + if line.startswith("Document has "): + num_pages = line.split(" ")[2] + break + if not num_pages or not num_pages.isdigit() or int(num_pages) <= 0: + return False, "Invalid number of pages returned" + num_pages = int(num_pages) + + # Make sure we have the files we expect + expected_filenames = [] + for i in range(1, num_pages + 1): + expected_filenames += [ + f"page-{i}.rgb", + f"page-{i}.width", + f"page-{i}.height", + ] + expected_filenames.sort() + actual_filenames = os.listdir(common.pixel_dir.name) + actual_filenames.sort() + + if expected_filenames != actual_filenames: + return ( + False, + f"We expected these files:\n{expected_filenames}\n\nBut we got these files:\n{actual_filenames}", ) - ) - label = QtWidgets.QLabel() - label.setText(message) - label.setWordWrap(True) + # Make sure the files are the correct sizes + for i in range(1, num_pages + 1): + with open(f"{common.pixel_dir.name}/page-{i}.width") as f: + w_str = f.read().strip() + with open(f"{common.pixel_dir.name}/page-{i}.height") as f: + h_str = f.read().strip() + w = int(w_str) + h = int(h_str) + if ( + not w_str.isdigit() + or not h_str.isdigit() + or w <= 0 + or w > max_image_width + or h <= 0 + or h > max_image_height + ): + return False, f"Page {i} has invalid geometry" - message_layout = QtWidgets.QHBoxLayout() - message_layout.addWidget(logo) - message_layout.addSpacing(10) - message_layout.addWidget(label, stretch=1) + # Make sure the RGB file is the correct size + if os.path.getsize(f"{common.pixel_dir.name}/page-{i}.rgb") != w * h * 3: + return False, f"Page {i} has an invalid RGB file size" - ok_button = QtWidgets.QPushButton(ok_text) - ok_button.clicked.connect(self.clicked_ok) - if extra_button_text: - extra_button = QtWidgets.QPushButton(extra_button_text) - extra_button.clicked.connect(self.clicked_extra) - cancel_button = QtWidgets.QPushButton("Cancel") - cancel_button.clicked.connect(self.clicked_cancel) - - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.addStretch() - buttons_layout.addWidget(ok_button) - if extra_button_text: - buttons_layout.addWidget(extra_button) - buttons_layout.addWidget(cancel_button) - - layout = QtWidgets.QVBoxLayout() - layout.addLayout(message_layout) - layout.addSpacing(10) - layout.addLayout(buttons_layout) - self.setLayout(layout) - - def clicked_ok(self): - self.done(QtWidgets.QDialog.Accepted) - - def clicked_extra(self): - self.done(2) - - def clicked_cancel(self): - self.done(QtWidgets.QDialog.Rejected) - - def launch(self): - return self.exec_() + return True, True diff --git a/dangerzone/gui.py b/dangerzone/gui/__init__.py similarity index 77% rename from dangerzone/gui.py rename to dangerzone/gui/__init__.py index 4fe1107..2e79083 100644 --- a/dangerzone/gui.py +++ b/dangerzone/gui/__init__.py @@ -3,21 +3,18 @@ import sys import signal import platform import click -import time import uuid -import subprocess from PySide2 import QtCore, QtWidgets -from .global_common import GlobalCommon +from .common import GuiCommon from .main_window import MainWindow from .docker_installer import ( is_docker_installed, is_docker_ready, - launch_docker_windows, DockerInstaller, AuthorizationFailed, ) -from .container import container_runtime +from ..global_common import GlobalCommon # For some reason, Dangerzone segfaults if I inherit from QApplication directly, so instead @@ -59,28 +56,17 @@ def gui_main(custom_container, filename): app_wrapper = ApplicationWrapper() app = app_wrapper.app - # GlobalCommon object - global_common = GlobalCommon(app) + # Common objects + global_common = GlobalCommon() + gui_common = GuiCommon(app, global_common) + + global_common.display_banner() if custom_container: - # Do we have this container? - with global_common.exec_dangerzone_container( - ["ls", "--container-name", custom_container] - ) as p: - stdout_data, stderr_data = p.communicate() - - # The user canceled, or permission denied - if p.returncode == 126 or p.returncode == 127: - click.echo("Authorization failed") - return - elif p.returncode != 0: - click.echo("Container error") - return - - # Check the output - if custom_container.encode() not in stdout_data: - click.echo(f"Container '{custom_container}' not found") - return + success, error_message = global_common.container_exists(custom_container) + if not success: + click.echo(error_message) + return global_common.custom_container = custom_container @@ -89,10 +75,10 @@ def gui_main(custom_container, filename): # If we're using Linux and docker, see if we need to add the user to the docker group or if the user prefers typing their password if platform.system() == "Linux": - if not global_common.ensure_docker_group_preference(): + if not gui_common.ensure_docker_group_preference(): return try: - if not global_common.ensure_docker_service_is_started(): + if not gui_common.ensure_docker_service_is_started(): click.echo("Failed to start docker service") return except AuthorizationFailed: @@ -101,10 +87,10 @@ def gui_main(custom_container, filename): # See if we need to install Docker... if (platform.system() == "Darwin" or platform.system() == "Windows") and ( - not is_docker_installed(global_common) or not is_docker_ready(global_common) + not is_docker_installed() or not is_docker_ready(global_common) ): click.echo("Docker is either not installed or not running") - docker_installer = DockerInstaller(global_common) + docker_installer = DockerInstaller(gui_common) docker_installer.start() return @@ -124,7 +110,7 @@ def gui_main(custom_container, filename): window = windows[list(windows.keys())[0]] else: window_id = uuid.uuid4().hex - window = MainWindow(global_common, window_id) + window = MainWindow(global_common, gui_common, window_id) window.delete_window.connect(delete_window) windows[window_id] = window diff --git a/dangerzone/gui/common.py b/dangerzone/gui/common.py new file mode 100644 index 0000000..cbf29bf --- /dev/null +++ b/dangerzone/gui/common.py @@ -0,0 +1,303 @@ +import os +import platform +import subprocess +import shlex +import pipes +from PySide2 import QtCore, QtGui, QtWidgets +from colorama import Fore + +if platform.system() == "Darwin": + import CoreServices + import LaunchServices + import plistlib + +elif platform.system() == "Linux": + import grp + import getpass + from xdg.DesktopEntry import DesktopEntry + +from .docker_installer import is_docker_ready +from ..settings import Settings + + +class GuiCommon(object): + """ + The GuiCommon class is a singleton of shared functionality for the GUI + """ + + def __init__(self, app, global_common): + # Qt app + self.app = app + + # Global common singleton + self.global_common = global_common + + # Preload font + self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) + + # Preload list of PDF viewers on computer + self.pdf_viewers = self._find_pdf_viewers() + + def get_window_icon(self): + if platform.system() == "Windows": + path = self.global_common.get_resource_path("dangerzone.ico") + else: + path = self.global_common.get_resource_path("icon.png") + return QtGui.QIcon(path) + + def open_pdf_viewer(self, filename): + if self.global_common.settings.get("open_app") in self.pdf_viewers: + if platform.system() == "Darwin": + # Get the PDF reader bundle command + bundle_identifier = self.pdf_viewers[ + self.global_common.settings.get("open_app") + ] + args = ["open", "-b", bundle_identifier, filename] + + # Run + args_str = " ".join(pipes.quote(s) for s in args) + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ + subprocess.run(args) + + elif platform.system() == "Linux": + # Get the PDF reader command + args = shlex.split( + self.pdf_viewers[self.global_common.settings.get("open_app")] + ) + # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open + for i in range(len(args)): + if ( + args[i] == "%f" + or args[i] == "%F" + or args[i] == "%u" + or args[i] == "%U" + ): + args[i] = filename + + # Open as a background process + args_str = " ".join(pipes.quote(s) for s in args) + print(Fore.YELLOW + "\u2023 " + Fore.CYAN + args_str) # ‣ + subprocess.Popen(args) + + def _find_pdf_viewers(self): + pdf_viewers = {} + + if platform.system() == "Darwin": + # Get all installed apps that can open PDFs + bundle_identifiers = LaunchServices.LSCopyAllRoleHandlersForContentType( + "com.adobe.pdf", CoreServices.kLSRolesAll + ) + for bundle_identifier in bundle_identifiers: + # Get the filesystem path of the app + res = LaunchServices.LSCopyApplicationURLsForBundleIdentifier( + bundle_identifier, None + ) + if res[0] is None: + continue + app_url = res[0][0] + app_path = str(app_url.path()) + + # Load its plist file + plist_path = os.path.join(app_path, "Contents/Info.plist") + + # Skip if there's not an Info.plist + if not os.path.exists(plist_path): + continue + + with open(plist_path, "rb") as f: + plist_data = f.read() + + plist_dict = plistlib.loads(plist_data) + + if ( + plist_dict.get("CFBundleName") + and plist_dict["CFBundleName"] != "Dangerzone" + ): + pdf_viewers[plist_dict["CFBundleName"]] = bundle_identifier + + elif platform.system() == "Linux": + # Find all .desktop files + for search_path in [ + "/usr/share/applications", + "/usr/local/share/applications", + os.path.expanduser("~/.local/share/applications"), + ]: + try: + for filename in os.listdir(search_path): + full_filename = os.path.join(search_path, filename) + if os.path.splitext(filename)[1] == ".desktop": + + # See which ones can open PDFs + desktop_entry = DesktopEntry(full_filename) + if ( + "application/pdf" in desktop_entry.getMimeTypes() + and desktop_entry.getName() != "dangerzone" + ): + pdf_viewers[ + desktop_entry.getName() + ] = desktop_entry.getExec() + + except FileNotFoundError: + pass + + return pdf_viewers + + def ensure_docker_group_preference(self): + # If the user prefers typing their password + if self.global_common.settings.get("linux_prefers_typing_password") == True: + return True + + # Get the docker group + try: + groupinfo = grp.getgrnam("docker") + except: + # Ignore if group is not found + return True + + # See if the user is in the group + username = getpass.getuser() + if username not in groupinfo.gr_mem: + # User is not in the docker group, ask if they prefer typing their password + message = "Dangerzone requires Docker

In order to use Docker, your user must be in the 'docker' group or you'll need to type your password each time you run dangerzone.

Adding your user to the 'docker' group is more convenient but less secure, and will require just typing your password once. Which do you prefer?" + return_code = Alert( + self, + self.global_common, + message, + ok_text="I'll type my password each time", + extra_button_text="Add my user to the 'docker' group", + ).launch() + if return_code == QtWidgets.QDialog.Accepted: + # Prefers typing password + self.global_common.settings.set("linux_prefers_typing_password", True) + self.global_common.settings.save() + return True + elif return_code == 2: + # Prefers being in the docker group + self.global_common.settings.set("linux_prefers_typing_password", False) + self.global_common.settings.save() + + # Add user to the docker group + p = subprocess.run( + [ + "/usr/bin/pkexec", + "/usr/sbin/usermod", + "-a", + "-G", + "docker", + username, + ] + ) + if p.returncode == 0: + message = "Great! Now you must log out of your computer and log back in, and then you can use Dangerzone." + Alert(self, self.global_common, message).launch() + else: + message = "Failed to add your user to the 'docker' group, quitting." + Alert(self, self.global_common, message).launch() + + return False + else: + # Cancel + return False + + return True + + def ensure_docker_service_is_started(self): + if not is_docker_ready(self.global_common): + message = "Dangerzone requires Docker

Docker should be installed, but it looks like it's not running in the background.

Click Ok to try starting the docker service. You will have to type your login password." + if ( + Alert(self, self.global_common, message).launch() + == QtWidgets.QDialog.Accepted + ): + p = subprocess.run( + [ + "/usr/bin/pkexec", + self.global_common.get_resource_path( + "enable_docker_service.sh" + ), + ] + ) + if p.returncode == 0: + # Make sure docker is now ready + if is_docker_ready(self.global_common): + return True + else: + message = "Restarting docker appeared to work, but the service still isn't responding, quitting." + Alert(self, self.global_common, message).launch() + else: + message = "Failed to start the docker service, quitting." + Alert(self, self.global_common, message).launch() + + return False + + return True + + +class Alert(QtWidgets.QDialog): + def __init__( + self, gui_common, global_common, message, ok_text="Ok", extra_button_text=None + ): + super(Alert, self).__init__() + self.global_common = global_common + self.gui_common = gui_common + + self.setWindowTitle("dangerzone") + self.setWindowIcon(self.gui_common.get_window_icon()) + self.setModal(True) + + flags = ( + QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowSystemMenuHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + self.setWindowFlags(flags) + + logo = QtWidgets.QLabel() + logo.setPixmap( + QtGui.QPixmap.fromImage( + QtGui.QImage(self.global_common.get_resource_path("icon.png")) + ) + ) + + label = QtWidgets.QLabel() + label.setText(message) + label.setWordWrap(True) + + message_layout = QtWidgets.QHBoxLayout() + message_layout.addWidget(logo) + message_layout.addSpacing(10) + message_layout.addWidget(label, stretch=1) + + ok_button = QtWidgets.QPushButton(ok_text) + ok_button.clicked.connect(self.clicked_ok) + if extra_button_text: + extra_button = QtWidgets.QPushButton(extra_button_text) + extra_button.clicked.connect(self.clicked_extra) + cancel_button = QtWidgets.QPushButton("Cancel") + cancel_button.clicked.connect(self.clicked_cancel) + + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addStretch() + buttons_layout.addWidget(ok_button) + if extra_button_text: + buttons_layout.addWidget(extra_button) + buttons_layout.addWidget(cancel_button) + + layout = QtWidgets.QVBoxLayout() + layout.addLayout(message_layout) + layout.addSpacing(10) + layout.addLayout(buttons_layout) + self.setLayout(layout) + + def clicked_ok(self): + self.done(QtWidgets.QDialog.Accepted) + + def clicked_extra(self): + self.done(2) + + def clicked_cancel(self): + self.done(QtWidgets.QDialog.Rejected) + + def launch(self): + return self.exec_() diff --git a/dangerzone/doc_selection_widget.py b/dangerzone/gui/doc_selection_widget.py similarity index 100% rename from dangerzone/doc_selection_widget.py rename to dangerzone/gui/doc_selection_widget.py diff --git a/dangerzone/docker_installer.py b/dangerzone/gui/docker_installer.py similarity index 97% rename from dangerzone/docker_installer.py rename to dangerzone/gui/docker_installer.py index 6def8ee..150b2f2 100644 --- a/dangerzone/docker_installer.py +++ b/dangerzone/gui/docker_installer.py @@ -8,14 +8,14 @@ import time import platform from PySide2 import QtCore, QtGui, QtWidgets -from .container import container_runtime +from ..container import container_runtime class AuthorizationFailed(Exception): pass -def is_docker_installed(global_common): +def is_docker_installed(): if platform.system() == "Darwin": # Does the docker binary exist? if os.path.isdir("/Applications/Docker.app") and os.path.exists( @@ -55,12 +55,11 @@ def launch_docker_windows(global_common): class DockerInstaller(QtWidgets.QDialog): - def __init__(self, global_common): + def __init__(self, gui_common): super(DockerInstaller, self).__init__() - self.global_common = global_common self.setWindowTitle("dangerzone") - self.setWindowIcon(self.global_common.get_window_icon()) + self.setWindowIcon(gui_common.get_window_icon()) # self.setMinimumHeight(170) label = QtWidgets.QLabel() diff --git a/dangerzone/main_window.py b/dangerzone/gui/main_window.py similarity index 84% rename from dangerzone/main_window.py rename to dangerzone/gui/main_window.py index 209af08..4477f6c 100644 --- a/dangerzone/main_window.py +++ b/dangerzone/gui/main_window.py @@ -6,20 +6,21 @@ from PySide2 import QtCore, QtGui, QtWidgets from .doc_selection_widget import DocSelectionWidget from .settings_widget import SettingsWidget from .tasks_widget import TasksWidget -from .common import Common +from ..common import Common class MainWindow(QtWidgets.QMainWindow): delete_window = QtCore.Signal(str) - def __init__(self, global_common, window_id): + def __init__(self, global_common, gui_common, window_id): super(MainWindow, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.window_id = window_id self.common = Common() self.setWindowTitle("dangerzone") - self.setWindowIcon(self.global_common.get_window_icon()) + self.setWindowIcon(self.gui_common.get_window_icon()) self.setMinimumWidth(600) self.setMinimumHeight(400) @@ -32,7 +33,7 @@ class MainWindow(QtWidgets.QMainWindow): ) ) header_label = QtWidgets.QLabel("dangerzone") - header_label.setFont(self.global_common.fixed_font) + header_label.setFont(self.gui_common.fixed_font) header_label.setStyleSheet("QLabel { font-weight: bold; font-size: 50px; }") header_layout = QtWidgets.QHBoxLayout() header_layout.addStretch() @@ -47,7 +48,9 @@ class MainWindow(QtWidgets.QMainWindow): self.doc_selection_widget.show() # Settings - self.settings_widget = SettingsWidget(self.global_common, self.common) + self.settings_widget = SettingsWidget( + self.global_common, self.gui_common, self.common + ) self.doc_selection_widget.document_selected.connect( self.settings_widget.document_selected ) @@ -59,7 +62,9 @@ class MainWindow(QtWidgets.QMainWindow): ) # Tasks - self.tasks_widget = TasksWidget(self.global_common, self.common) + self.tasks_widget = TasksWidget( + self.global_common, self.gui_common, self.common + ) self.tasks_widget.close_window.connect(self.close) self.doc_selection_widget.document_selected.connect( self.tasks_widget.document_selected @@ -93,4 +98,4 @@ class MainWindow(QtWidgets.QMainWindow): self.delete_window.emit(self.window_id) if platform.system() != "Darwin": - self.global_common.app.quit() + self.gui_common.app.quit() diff --git a/dangerzone/settings_widget.py b/dangerzone/gui/settings_widget.py similarity index 97% rename from dangerzone/settings_widget.py rename to dangerzone/gui/settings_widget.py index 4150653..759818f 100644 --- a/dangerzone/settings_widget.py +++ b/dangerzone/gui/settings_widget.py @@ -1,5 +1,4 @@ import os -import subprocess import platform from PySide2 import QtCore, QtGui, QtWidgets @@ -8,9 +7,10 @@ class SettingsWidget(QtWidgets.QWidget): start_clicked = QtCore.Signal() close_window = QtCore.Signal() - def __init__(self, global_common, common): + def __init__(self, global_common, gui_common, common): super(SettingsWidget, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.common = common # Dangerous document label @@ -49,8 +49,8 @@ class SettingsWidget(QtWidgets.QWidget): ) self.open_checkbox.clicked.connect(self.update_ui) self.open_combobox = QtWidgets.QComboBox() - for k in self.global_common.pdf_viewers: - self.open_combobox.addItem(k, self.global_common.pdf_viewers[k]) + for k in self.gui_common.pdf_viewers: + self.open_combobox.addItem(k, self.gui_common.pdf_viewers[k]) open_layout = QtWidgets.QHBoxLayout() open_layout.addWidget(self.open_checkbox) open_layout.addWidget(self.open_combobox) diff --git a/dangerzone/tasks.py b/dangerzone/gui/tasks.py similarity index 53% rename from dangerzone/tasks.py rename to dangerzone/gui/tasks.py index bf950a3..5b97f34 100644 --- a/dangerzone/tasks.py +++ b/dangerzone/gui/tasks.py @@ -1,10 +1,5 @@ -import subprocess -import time -import os -import pipes -import platform from PySide2 import QtCore, QtWidgets, QtGui -from termcolor import cprint +from colorama import Style, Fore class TaskBase(QtCore.QThread): @@ -23,11 +18,23 @@ class TaskBase(QtCore.QThread): with self.global_common.exec_dangerzone_container(args) as p: for line in p.stdout: output += line.decode() - print(line.decode(), end="") + + if line.startswith(b"\xe2\x80\xa3 "): + print( + Fore.WHITE + "\u2023 " + Fore.LIGHTCYAN_EX + line.decode()[2:], + end="", + ) + else: + print(" " + line.decode(), end="") + self.update_details.emit(output) stderr = p.stderr.read().decode() - cprint(stderr, attrs=["dark"]) + if len(stderr) > 0: + print("") + for line in stderr.strip().split("\n"): + print(" " + Style.DIM + line) + self.update_details.emit(output) if p.returncode == 126 or p.returncode == 127: @@ -65,10 +72,6 @@ class ConvertToPixels(TaskBase): self.global_common = global_common self.common = common - self.max_image_width = 10000 - self.max_image_height = 10000 - self.max_image_size = self.max_image_width * self.max_image_height * 3 - def run(self): self.update_label.emit("Converting document to pixels") args = [ @@ -80,76 +83,17 @@ class ConvertToPixels(TaskBase): "--container-name", self.global_common.get_container_name(), ] - returncode, output, stderr = self.exec_container(args) + returncode, output, _ = self.exec_container(args) if returncode != 0: return - # Did we hit an error? - for line in output.split("\n"): - if ( - "failed:" in line - or "The document format is not supported" in line - or "Error" in line - ): - self.task_failed.emit(output) - return - - # How many pages was that? - num_pages = None - for line in output.split("\n"): - if line.startswith("Document has "): - num_pages = line.split(" ")[2] - break - if not num_pages or not num_pages.isdigit() or int(num_pages) <= 0: - self.task_failed.emit("Invalid number of pages returned") + success, error_message = self.global_common.validate_convert_to_pixel_output( + self.common, output + ) + if not success: + self.task_failed.emit(error_message) return - num_pages = int(num_pages) - - # Make sure we have the files we expect - expected_filenames = [] - for i in range(1, num_pages + 1): - expected_filenames += [ - f"page-{i}.rgb", - f"page-{i}.width", - f"page-{i}.height", - ] - expected_filenames.sort() - actual_filenames = os.listdir(self.common.pixel_dir.name) - actual_filenames.sort() - - if expected_filenames != actual_filenames: - self.task_failed.emit( - f"We expected these files:\n{expected_filenames}\n\nBut we got these files:\n{actual_filenames}" - ) - return - - # Make sure the files are the correct sizes - for i in range(1, num_pages + 1): - with open(f"{self.common.pixel_dir.name}/page-{i}.width") as f: - w_str = f.read().strip() - with open(f"{self.common.pixel_dir.name}/page-{i}.height") as f: - h_str = f.read().strip() - w = int(w_str) - h = int(h_str) - if ( - not w_str.isdigit() - or not h_str.isdigit() - or w <= 0 - or w > self.max_image_width - or h <= 0 - or h > self.max_image_height - ): - self.task_failed.emit(f"Page {i} has invalid geometry") - return - - # Make sure the RGB file is the correct size - if ( - os.path.getsize(f"{self.common.pixel_dir.name}/page-{i}.rgb") - != w * h * 3 - ): - self.task_failed.emit(f"Page {i} has an invalid RGB file size") - return self.task_finished.emit() diff --git a/dangerzone/tasks_widget.py b/dangerzone/gui/tasks_widget.py similarity index 94% rename from dangerzone/tasks_widget.py rename to dangerzone/gui/tasks_widget.py index 4688d36..94a00a4 100644 --- a/dangerzone/tasks_widget.py +++ b/dangerzone/gui/tasks_widget.py @@ -11,9 +11,10 @@ from .tasks import PullImageTask, ConvertToPixels, ConvertToPDF class TasksWidget(QtWidgets.QWidget): close_window = QtCore.Signal() - def __init__(self, global_common, common): + def __init__(self, global_common, gui_common, common): super(TasksWidget, self).__init__() self.global_common = global_common + self.gui_common = gui_common self.common = common # Dangerous document label @@ -31,7 +32,7 @@ class TasksWidget(QtWidgets.QWidget): self.task_details.setStyleSheet( "QLabel { background-color: #ffffff; font-size: 12px; padding: 10px; }" ) - self.task_details.setFont(self.global_common.fixed_font) + self.task_details.setFont(self.gui_common.fixed_font) self.task_details.setAlignment(QtCore.Qt.AlignTop) self.details_scrollarea = QtWidgets.QScrollArea() @@ -111,7 +112,7 @@ class TasksWidget(QtWidgets.QWidget): # Open if self.global_common.settings.get("open"): - self.global_common.open_pdf_viewer(dest_filename) + self.gui_common.open_pdf_viewer(dest_filename) # Clean up self.common.pixel_dir.cleanup() @@ -122,7 +123,7 @@ class TasksWidget(QtWidgets.QWidget): # In macOS, just close the window self.close_window.emit() else: - self.global_common.app.quit() + self.gui_common.app.quit() def scroll_to_bottom(self, minimum, maximum): self.details_scrollarea.verticalScrollBar().setValue(maximum) diff --git a/dangerzone/settings.py b/dangerzone/settings.py index 556f63a..4242b1f 100644 --- a/dangerzone/settings.py +++ b/dangerzone/settings.py @@ -7,18 +7,12 @@ class Settings: def __init__(self, common): self.common = common self.settings_filename = os.path.join(self.common.appdata_path, "settings.json") - - if len(self.common.pdf_viewers) == 0: - default_pdf_viewer = None - else: - default_pdf_viewer = list(self.common.pdf_viewers)[0] - self.default_settings = { "save": True, "ocr": True, "ocr_language": "English", "open": True, - "open_app": default_pdf_viewer, + "open_app": None, "update_container": True, "linux_prefers_typing_password": None, } diff --git a/dev_scripts/dangerzone-cli b/dev_scripts/dangerzone-cli new file mode 120000 index 0000000..2fe47df --- /dev/null +++ b/dev_scripts/dangerzone-cli @@ -0,0 +1 @@ +dangerzone \ No newline at end of file diff --git a/install/linux/build_rpm.py b/install/linux/build_rpm.py index 42e582a..0d6c2b8 100755 --- a/install/linux/build_rpm.py +++ b/install/linux/build_rpm.py @@ -31,7 +31,7 @@ def main(): print("* Building RPM package") subprocess.run( - "python3 setup.py bdist_rpm --requires='python3-pyside2,python3-appdirs,python3-click,python3-pyxdg,python3-requests,python3-termcolor,(docker or docker-ce)'", + "python3 setup.py bdist_rpm --requires='python3-pyside2,python3-appdirs,python3-click,python3-pyxdg,python3-requests,python3-colorama,(docker or docker-ce)'", shell=True, cwd=root, check=True, diff --git a/poetry.lock b/poetry.lock index 27e525f..94a767c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,7 +374,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = ">=3.7,<3.10" -content-hash = "26e6acc883dad4194c45e399fadaabc12730fdd3a7b2264737e4e81838183eee" +content-hash = "72592722794667cf7cb6bea727b31eb60d710c90d488be9d10a13bbbcaa56688" [metadata.files] altgraph = [ diff --git a/pyproject.toml b/pyproject.toml index 9aca820..860a048 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ wmi = {version = "*", platform = "win32"} pyxdg = {version = "*", platform = "linux"} pyobjc-core = {version = "*", platform = "darwin"} pyobjc-framework-launchservices = {version = "*", platform = "darwin"} +colorama = "^0.4.4" [tool.poetry.dev-dependencies] pyinstaller = {version = "*", platform = "darwin"} @@ -27,6 +28,7 @@ black = "^21.5b2" [tool.poetry.scripts] dangerzone = 'dangerzone:main' dangerzone-container = 'dangerzone:main' +dangerzone-cli = 'dangerzone:main' [build-system] requires = ["poetry>=1.1.4"] diff --git a/setup.py b/setup.py index 21a4741..2edcca2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,9 @@ import setuptools import os import sys -from dangerzone import dangerzone_version + +with open("share/version.txt") as f: + version = f.read().strip() def file_list(path): @@ -15,7 +17,7 @@ def file_list(path): setuptools.setup( name="dangerzone", - version=dangerzone_version, + version=version, author="Micah Lee", author_email="micah.lee@theintercept.com", license="MIT", @@ -23,7 +25,10 @@ setuptools.setup( url="https://github.com/firstlookmedia/dangerzone", packages=["dangerzone"], data_files=[ - ("share/applications", ["install/linux/media.firstlook.dangerzone.desktop"],), + ( + "share/applications", + ["install/linux/media.firstlook.dangerzone.desktop"], + ), ( "share/icons/hicolor/64x64/apps", ["install/linux/media.firstlook.dangerzone.png"], diff --git a/share/version.txt b/share/version.txt new file mode 100644 index 0000000..2f45361 --- /dev/null +++ b/share/version.txt @@ -0,0 +1 @@ +0.2 \ No newline at end of file diff --git a/stdeb.cfg b/stdeb.cfg index cfae021..df13e23 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,6 +1,6 @@ [DEFAULT] Package3: dangerzone -Depends3: python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-appdirs, python3-click, python3-xdg, python3-requests, python3-termcolor +Depends3: python3, python3-pyside2.qtcore, python3-pyside2.qtgui, python3-pyside2.qtwidgets, python3-appdirs, python3-click, python3-xdg, python3-requests, python3-colorama Build-Depends: dh-python, python3, python3-all Suite: bionic X-Python3-Version: >= 3.6 \ No newline at end of file