mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Merge pull request #116 from firstlookmedia/53_cli_support
Add command line-only support and pretty terminal output
This commit is contained in:
commit
07935a273b
24 changed files with 891 additions and 420 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -133,4 +133,4 @@ dmypy.json
|
|||
deb_dist
|
||||
.DS_Store
|
||||
install/windows/Dangerzone.wxs
|
||||
test-docs/sample-safe.pdf
|
||||
test_docs/sample-safe.pdf
|
13
BUILD.md
13
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:
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
205
dangerzone/cli.py
Normal file
205
dangerzone/cli.py
Normal file
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 = "<b>Dangerzone requires Docker</b><br><br>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.<br><br><b>Adding your user to the 'docker' group is more convenient but less secure</b>, 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 = "<b>Dangerzone requires Docker</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>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
|
||||
|
|
|
@ -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,27 +56,16 @@ 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")
|
||||
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
|
||||
|
303
dangerzone/gui/common.py
Normal file
303
dangerzone/gui/common.py
Normal file
|
@ -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 = "<b>Dangerzone requires Docker</b><br><br>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.<br><br><b>Adding your user to the 'docker' group is more convenient but less secure</b>, 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 = "<b>Dangerzone requires Docker</b><br><br>Docker should be installed, but it looks like it's not running in the background.<br><br>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_()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
|
@ -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,75 +83,16 @@ 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")
|
||||
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}"
|
||||
success, error_message = self.global_common.validate_convert_to_pixel_output(
|
||||
self.common, output
|
||||
)
|
||||
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")
|
||||
if not success:
|
||||
self.task_failed.emit(error_message)
|
||||
return
|
||||
|
||||
self.task_finished.emit()
|
|
@ -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)
|
|
@ -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,
|
||||
}
|
||||
|
|
1
dev_scripts/dangerzone-cli
Symbolic link
1
dev_scripts/dangerzone-cli
Symbolic link
|
@ -0,0 +1 @@
|
|||
dangerzone
|
|
@ -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,
|
||||
|
|
2
poetry.lock
generated
2
poetry.lock
generated
|
@ -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 = [
|
||||
|
|
|
@ -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"]
|
||||
|
|
11
setup.py
11
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"],
|
||||
|
|
1
share/version.txt
Normal file
1
share/version.txt
Normal file
|
@ -0,0 +1 @@
|
|||
0.2
|
|
@ -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
|
Loading…
Reference in a new issue