Skip to content

Commit

Permalink
Export and print use single method signature across components. Devic…
Browse files Browse the repository at this point in the history
…e does not depend on filepaths.
  • Loading branch information
rocodes committed Jan 26, 2024
1 parent 74f03ad commit 3cc060e
Show file tree
Hide file tree
Showing 17 changed files with 129 additions and 268 deletions.
6 changes: 3 additions & 3 deletions client/securedrop_client/gui/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def _on_triggered(self) -> None:
# out of scope, any pending file removal will be performed
# by the operating system.
with open(file_path, "r") as f:
export = ExportDevice([file_path])
export = ExportDevice()
dialog = PrintConversationTranscriptDialog(export, TRANSCRIPT_FILENAME, str(file_path))
dialog.exec()

Expand Down Expand Up @@ -236,7 +236,7 @@ def _on_triggered(self) -> None:
# out of scope, any pending file removal will be performed
# by the operating system.
with open(file_path, "r") as f:
export_device = ExportDevice([file_path])
export_device = ExportDevice()
dialog = ExportConversationTranscriptDialog(
export_device, TRANSCRIPT_FILENAME, str(file_path)
)
Expand Down Expand Up @@ -325,7 +325,7 @@ def _prepare_to_export(self) -> None:
# out of scope, any pending file removal will be performed
# by the operating system.
with ExitStack() as stack:
export_device = ExportDevice(file_locations)
export_device = ExportDevice()
files = [
stack.enter_context(open(file_location, "r")) for file_location in file_locations
]
Expand Down
173 changes: 71 additions & 102 deletions client/securedrop_client/gui/conversation/export/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,30 @@ class Device(QObject):
print_preflight_check_succeeded = pyqtSignal(object)
print_succeeded = pyqtSignal(object)

# Used for both print and export
export_completed = pyqtSignal(object)

# Emit ExportError(status=ExportStatus)
export_preflight_check_failed = pyqtSignal(object)
export_failed = pyqtSignal(object)

print_preflight_check_failed = pyqtSignal(object)
print_failed = pyqtSignal(object)

def __init__(self, filepaths: [str]) -> None:
super().__init__()

self._filepaths_list = filepaths

def run_printer_preflight_checks(self) -> None:
"""
Make sure the Export VM is started.
"""
logger.info("Running printer preflight check")
logger.info("Beginning printer preflight check")
try:
status = self._build_archive_and_export(
metadata=self._PRINTER_PREFLIGHT_METADATA, filename=self._PRINTER_PREFLIGHT_FN
)
self.print_preflight_check_succeeded.emit(status)
with TemporaryDirectory() as tmp_dir:
archive_path = self._create_archive(
archive_dir=tmp_dir,
archive_fn=self._PRINTER_PREFLIGHT_FN,
metadata=self._PRINTER_PREFLIGHT_METADATA,
)
status = self._run_qrexec_export(archive_path)
self.print_preflight_check_succeeded.emit(status)
except ExportError as e:
logger.error("Print preflight failed")
logger.debug(f"Print preflight failed: {e}")
Expand All @@ -81,50 +83,77 @@ def run_export_preflight_checks(self) -> None:
"""
try:
logger.debug("Beginning export preflight check")
status = self._build_archive_and_export(
metadata=self._USB_TEST_METADATA, filename=self._USB_TEST_FN
)
self.export_preflight_check_succeeded.emit(status)

with TemporaryDirectory() as tmp_dir:
archive_path = self._create_archive(
archive_dir=tmp_dir,
archive_fn=self._USB_TEST_FN,
metadata=self._USB_TEST_METADATA,
)
status = self._run_qrexec_export(archive_path)
self.export_preflight_check_succeeded.emit(status)

except ExportError as e:
logger.error("Export preflight failed")
self.export_preflight_check_failed.emit(e)

def export_transcript(self, file_location: str, passphrase: str) -> None:
def export(self, filepaths: List[str], passphrase: str = None) -> None:
"""
Send the transcript specified by file_location to the Export VM.
Bundle filepaths into a tarball and send to encrypted USB via qrexec,
optionally supplying a passphrase to unlock encrypted drives.
"""
logger.debug("Export transcript")
self._send_file_to_usb_device([file_location], passphrase)
try:
logger.debug(f"Begin exporting {len(filepaths)} item(s)")

def export_files(self, file_locations: List[str], passphrase: str) -> None:
"""
Send the files specified by file_locations to the Export VM.
"""
logger.debug(f"Export {len(file_locations)} files")
self._send_file_to_usb_device(file_locations, passphrase)
# Edit metadata template to include passphrase
metadata = self._DISK_METADATA.copy()
if passphrase:
metadata[self._DISK_ENCRYPTION_KEY_NAME] = passphrase

def export_file_to_usb_drive(self, file_uuid: str, passphrase: str) -> None:
"""
Send the file specified by file_uuid to the Export VM with the user-provided passphrase for
unlocking the attached transfer device. If the file is missing, update the db so that
is_downloaded is set to False.
"""
with TemporaryDirectory() as tmp_dir:
archive_path = self._create_archive(
archive_dir=tmp_dir,
archive_fn=self._DISK_FN,
metadata=metadata,
filepaths=filepaths,
)
status = self._run_qrexec_export(archive_path)

self._send_file_to_usb_device(self._filepaths_list, passphrase)
self.export_succeeded.emit(status)
logger.debug(f"Status {status}")

def print_transcript(self, file_location: str) -> None:
"""
Send the transcript specified by file_location to the Export VM.
"""
self._print([file_location])
except ExportError as e:
logger.error("Export failed")
logger.debug(f"Export failed: {e}")
self.export_failed.emit(e)

self.export_completed.emit(filepaths)

def print_file(self, file_uuid: str) -> None:
def print(self, filepaths: List[str]) -> None:
"""
Send the file specified by file_uuid to the Export VM. If the file is missing, update the db
so that is_downloaded is set to False.
Bundle files at self._filepaths_list into tarball and send for
printing via qrexec.
"""
try:
logger.debug("Beginning print")

with TemporaryDirectory() as tmp_dir:
archive_path = self._create_archive(
archive_dir=tmp_dir,
archive_fn=self._PRINT_FN,
metadata=self._PRINT_METADATA,
filepaths=filepaths,
)
status = self._run_qrexec_export(archive_path)
self.print_succeeded.emit(status)
logger.debug(f"Status {status}")

self._print(self._filepaths_list)
except ExportError as e:
logger.error("Export failed")
logger.debug(f"Export failed: {e}")
self.print_failed.emit(e)

self.export_completed.emit(filepaths)

def _run_qrexec_export(self, archive_path: str) -> ExportStatus:
"""
Expand Down Expand Up @@ -174,7 +203,7 @@ def _run_qrexec_export(self, archive_path: str) -> ExportStatus:
raise ExportError(ExportStatus.CALLED_PROCESS_ERROR)

def _create_archive(
self, archive_dir: str, archive_fn: str, metadata: dict, filepaths: List[str]
self, archive_dir: str, archive_fn: str, metadata: dict, filepaths: List[str] = []
) -> str:
"""
Create the archive to be sent to the Export VM.
Expand Down Expand Up @@ -210,7 +239,7 @@ def _create_archive(
self._add_file_to_archive(
archive, filepath, prevent_name_collisions=is_one_of_multiple_files
)
if missing_count == len(filepaths):
if missing_count == len(filepaths) and missing_count > 0:
# Context manager will delete archive even if an exception occurs
# since the archive is in a TemporaryDirectory
logger.error("Files were moved or missing")
Expand Down Expand Up @@ -258,63 +287,3 @@ def _add_file_to_archive(
arcname = os.path.join("export_data", parent_name, filename)

archive.add(filepath, arcname=arcname, recursive=False)

def _build_archive_and_export(
self, metadata: dict, filename: str, filepaths: List[str] = []
) -> ExportStatus:
"""
Build archive, run qrexec command and return resulting ExportStatus.
ExportError may be raised during underlying _run_qrexec_export call,
and is handled by the calling method.
"""
with TemporaryDirectory() as tmp_dir:
archive_path = self._create_archive(
archive_dir=tmp_dir, archive_fn=filename, metadata=metadata, filepaths=filepaths
)
return self._run_qrexec_export(archive_path)

def _send_file_to_usb_device(self, filepaths: List[str], passphrase: str) -> None:
"""
Export the file to the luks-encrypted usb disk drive attached to the Export VM.
Args:
filepath: The path of file to export.
passphrase: The passphrase to unlock the luks-encrypted usb disk drive.
"""
try:
logger.debug("beginning export")
# Edit metadata template to include passphrase
metadata = self._DISK_METADATA.copy()
metadata[self._DISK_ENCRYPTION_KEY_NAME] = passphrase
status = self._build_archive_and_export(
metadata=metadata, filename=self._DISK_FN, filepaths=filepaths
)

self.export_succeeded.emit(status)
logger.debug(f"Status {status}")
except ExportError as e:
logger.error("Export failed")
logger.debug(f"Export failed: {e}")
self.export_failed.emit(e)

def _print(self, filepaths: List[str]) -> None:
"""
Print the file to the printer attached to the Export VM.
Args:
filepath: The path of file to export.
"""
try:
logger.debug("beginning print")
status = self._build_archive_and_export(
metadata=self._PRINT_METADATA, filename=self._PRINT_FN, filepaths=filepaths
)
self.print_succeeded.emit(status)
logger.debug(f"Status {status}")
except ExportError as e:
logger.error("Export failed")
logger.debug(f"Export failed: {e}")
self.print_failed.emit(e)

self.export_succeeded.emit(filepaths)
4 changes: 2 additions & 2 deletions client/securedrop_client/gui/conversation/export/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Dialog(FileDialog):
"""

def __init__(self, device: Device, summary: str, file_locations: List[str]) -> None:
super().__init__(device, "", summary)
super().__init__(device, summary, file_locations)

self.file_locations = file_locations

Expand All @@ -25,7 +25,7 @@ def _export_files(self, checked: bool = False) -> None:
self.start_animate_activestate()
self.cancel_button.setEnabled(False)
self.passphrase_field.setDisabled(True)
self._device.export_files(self.file_locations, self.passphrase_field.text())
self._device.export(self.file_locations, self.passphrase_field.text())

@pyqtSlot()
def _show_passphrase_request_message(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ class FileDialog(ModalDialog):
NO_MARGIN = 0
FILENAME_WIDTH_PX = 260

def __init__(self, device: Device, file_uuid: str, file_name: str) -> None:
def __init__(self, device: Device, file_name: str, filepath: str) -> None:
super().__init__()
self.setStyleSheet(self.DIALOG_CSS)

self._device = device
self.file_uuid = file_uuid
self.filepath = filepath
self.file_name = SecureQLabel(
file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text()
Expand Down Expand Up @@ -215,7 +215,7 @@ def _export_file(self, checked: bool = False) -> None:
# TODO: If the drive is already unlocked, the passphrase field will be empty.
# This is ok, but could violate expectations. The password should be passed
# via qrexec in future, to avoid writing it to even a temporary file at all.
self._device.export_file_to_usb_drive(self.file_uuid, self.passphrase_field.text())
self._device.export([self.filepath], self.passphrase_field.text())

@pyqtSlot(object)
def _on_export_preflight_check_succeeded(self, result: ExportStatus) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
class PrintDialog(ModalDialog):
FILENAME_WIDTH_PX = 260

def __init__(self, device: Device, file_uuid: str, file_name: str) -> None:
def __init__(self, device: Device, file_name: str, filepath: str) -> None:
super().__init__()

self._device = device
self.file_uuid = file_uuid
self.filepath = filepath
self.file_name = SecureQLabel(
file_name, wordwrap=False, max_length=self.FILENAME_WIDTH_PX
).text()
Expand Down Expand Up @@ -94,7 +94,7 @@ def _run_preflight(self) -> None:

@pyqtSlot()
def _print_file(self) -> None:
self._device.print_file(self.file_uuid)
self._device.print([self.filepath])
self.close()

@pyqtSlot()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ class PrintTranscriptDialog(PrintDialog):
"""

def __init__(self, device: Device, file_name: str, transcript_location: str) -> None:
super().__init__(device, "", file_name)
super().__init__(device, file_name, transcript_location)

self.transcript_location = transcript_location

def _print_transcript(self) -> None:
self._device.print_transcript(self.transcript_location)
self._device.print(self.transcript_location)
self.close()

@pyqtSlot()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ class TranscriptDialog(FileDialog):
"""

def __init__(self, device: Device, file_name: str, transcript_location: str) -> None:
super().__init__(device, "", file_name)
super().__init__(device, file_name, transcript_location)

self.transcript_location = transcript_location

def _export_transcript(self, checked: bool = False) -> None:
self.start_animate_activestate()
self.cancel_button.setEnabled(False)
self.passphrase_field.setDisabled(True)
self._device.export_transcript(self.transcript_location, self.passphrase_field.text())
self._device.export([self.transcript_location], self.passphrase_field.text())

@pyqtSlot()
def _show_passphrase_request_message(self) -> None:
Expand Down
9 changes: 5 additions & 4 deletions client/securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2459,10 +2459,10 @@ def _on_export_clicked(self) -> None:
logger.debug("Clicked export but file not downloaded")
return

export_device = conversation.ExportDevice([file_location])
export_device = conversation.ExportDevice()

self.export_dialog = conversation.ExportFileDialog(
export_device, self.uuid, self.file.filename
export_device, self.file.filename, file_location
)
self.export_dialog.show()

Expand All @@ -2476,9 +2476,10 @@ def _on_print_clicked(self) -> None:
return

filepath = self.file.location(self.controller.data_dir)
export_device = conversation.ExportDevice([filepath])

dialog = conversation.PrintFileDialog(export_device, self.uuid, self.file.filename)
export_device = conversation.ExportDevice()

dialog = conversation.PrintFileDialog(export_device, self.file.filename, filepath)
dialog.exec()

def _on_left_click(self) -> None:
Expand Down
Loading

0 comments on commit 3cc060e

Please sign in to comment.