Skip to content

Commit

Permalink
Merge pull request #374 from freedomofpress/api-queue
Browse files Browse the repository at this point in the history
Implementation of the API queue
  • Loading branch information
redshiftzero authored May 28, 2019
2 parents 30f9967 + d842bc7 commit 844a0c2
Show file tree
Hide file tree
Showing 23 changed files with 1,455 additions and 709 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ mypy: ## Run static type checker
securedrop_client/gui/__init__.py \
securedrop_client/resources/__init__.py \
securedrop_client/storage.py \
securedrop_client/message_sync.py
securedrop_client/message_sync.py \
securedrop_client/queue.py

.PHONY: clean
clean: ## Clean the workspace of generated resources
Expand Down
5 changes: 3 additions & 2 deletions create_dev_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import os
import sys
from securedrop_client.config import Config
from securedrop_client.db import Base, make_engine
from securedrop_client.db import Base, make_session_maker

sdc_home = sys.argv[1]
Base.metadata.create_all(make_engine(sdc_home))
session = make_session_maker(sdc_home)()
Base.metadata.create_all(bind=session.get_bind())

with open(os.path.join(sdc_home, Config.CONFIG_NAME), 'w') as f:
f.write(json.dumps({
Expand Down
12 changes: 5 additions & 7 deletions securedrop_client/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@
import sys
import socket
from argparse import ArgumentParser
from sqlalchemy.orm import sessionmaker
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtCore import Qt, QTimer
from logging.handlers import TimedRotatingFileHandler
from securedrop_client import __version__
from securedrop_client.logic import Controller
from securedrop_client.gui.main import Window
from securedrop_client.resources import load_icon, load_css
from securedrop_client.db import make_engine
from securedrop_client.db import make_session_maker
from securedrop_client.utils import safe_mkdir

DEFAULT_SDC_HOME = '~/.securedrop_client'
Expand Down Expand Up @@ -185,15 +184,14 @@ def start_app(args, qt_args) -> None:

prevent_second_instance(app, args.sdc_home)

session_maker = make_session_maker(args.sdc_home)

gui = Window()

app.setWindowIcon(load_icon(gui.icon))
app.setStyleSheet(load_css('sdclient.css'))

engine = make_engine(args.sdc_home)
Session = sessionmaker(bind=engine)
session = Session()

controller = Controller("http://localhost:8081/", gui, session,
controller = Controller("http://localhost:8081/", gui, session_maker,
args.sdc_home, not args.no_proxy)
controller.setup()

Expand Down
20 changes: 10 additions & 10 deletions securedrop_client/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
import subprocess
import tempfile

from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session
from uuid import UUID

from securedrop_client.config import Config
from securedrop_client.db import make_engine, Source
from securedrop_client.db import Source
from securedrop_client.utils import safe_mkdir

logger = logging.getLogger(__name__)
Expand All @@ -39,17 +39,15 @@ class CryptoError(Exception):

class GpgHelper:

def __init__(self, sdc_home: str, is_qubes: bool) -> None:
def __init__(self, sdc_home: str, session_maker: scoped_session, is_qubes: bool) -> None:
'''
:param sdc_home: Home directory for the SecureDrop client
:param is_qubes: Whether the client is running in Qubes or not
'''
safe_mkdir(os.path.join(sdc_home), "gpg")
self.sdc_home = sdc_home
self.is_qubes = is_qubes
engine = make_engine(sdc_home)
Session = sessionmaker(bind=engine)
self.session = Session()
self.session_maker = session_maker

config = Config.from_home_dir(self.sdc_home)
self.journalist_key_fingerprint = config.journalist_key_fingerprint
Expand Down Expand Up @@ -110,13 +108,14 @@ def _gpg_cmd_base(self) -> list:
return cmd

def import_key(self, source_uuid: UUID, key_data: str, fingerprint: str) -> None:
local_source = self.session.query(Source).filter_by(uuid=source_uuid).one()
session = self.session_maker()
local_source = session.query(Source).filter_by(uuid=source_uuid).one()

self._import(key_data)

local_source.fingerprint = fingerprint
self.session.add(local_source)
self.session.commit()
session.add(local_source)
session.commit()

def _import(self, key_data: str) -> None:
'''Wrapper for `gpg --import-keys`'''
Expand Down Expand Up @@ -145,7 +144,8 @@ def encrypt_to_source(self, source_uuid: str, data: str) -> str:
'''
:param data: A string of data to encrypt to a source.
'''
source = self.session.query(Source).filter_by(uuid=source_uuid).one()
session = self.session_maker()
source = session.query(Source).filter_by(uuid=source_uuid).one()
cmd = self._gpg_cmd_base()

with tempfile.NamedTemporaryFile('w+') as content, \
Expand Down
12 changes: 5 additions & 7 deletions securedrop_client/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

from sqlalchemy import Boolean, Column, create_engine, DateTime, ForeignKey, Integer, String, \
Text, MetaData, CheckConstraint, text, UniqueConstraint
from sqlalchemy.engine import Engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, backref
from sqlalchemy.orm import relationship, backref, scoped_session, sessionmaker


convention = {
Expand All @@ -22,9 +21,11 @@
Base = declarative_base(metadata=metadata) # type: Any


def make_engine(home: str) -> Engine:
def make_session_maker(home: str) -> scoped_session:
db_path = os.path.join(home, 'svs.sqlite')
return create_engine('sqlite:///{}'.format(db_path))
engine = create_engine('sqlite:///{}'.format(db_path))
maker = sessionmaker(bind=engine)
return scoped_session(maker)


class Source(Base):
Expand Down Expand Up @@ -215,8 +216,5 @@ class User(Base):
uuid = Column(String(36), unique=True, nullable=False)
username = Column(String(255), nullable=False)

def __init__(self, username: str) -> None:
self.username = username

def __repr__(self) -> str:
return "<Journalist: {}>".format(self.username)
4 changes: 2 additions & 2 deletions securedrop_client/gui/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging

from gettext import gettext as _
from typing import Dict, List, Optional # noqa: F401

from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \
QApplication

Expand All @@ -43,7 +43,7 @@ class Window(QMainWindow):

icon = 'icon.png'

def __init__(self):
def __init__(self) -> None:
"""
Create the default start state. The window contains a root widget into
which is placed:
Expand Down
84 changes: 46 additions & 38 deletions securedrop_client/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
"""
import logging
import arrow
from gettext import gettext as _
import html
import sys

from gettext import gettext as _
from typing import List
from uuid import uuid4

from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QTimer, QSize
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QTimer, QSize, pyqtBoundSignal, QObject
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
Expand Down Expand Up @@ -587,7 +587,7 @@ class MainView(QWidget):
}
'''

def __init__(self, parent):
def __init__(self, parent: QObject):
super().__init__(parent)

self.setStyleSheet(self.CSS)
Expand Down Expand Up @@ -1274,58 +1274,52 @@ class FileWidget(QWidget):
Represents a file.
"""

def __init__(self, source_db_object, submission_db_object,
controller, file_ready_signal, align="left"):
def __init__(
self,
file_uuid: str,
controller: Controller,
file_ready_signal: pyqtBoundSignal,
) -> None:
"""
Given some text, an indication of alignment ('left' or 'right') and
a reference to the controller, make something to display a file.
Align is set to left by default because currently SecureDrop can only
accept files from sources to journalists.
Given some text and a reference to the controller, make something to display a file.
"""
super().__init__()
self.controller = controller
self.source = source_db_object
self.submission = submission_db_object
self.file_uuid = self.submission.uuid
self.align = align
self.file = self.controller.get_file(file_uuid)

self.layout = QHBoxLayout()
self.update()
self.setLayout(self.layout)

file_ready_signal.connect(self._on_file_download)
file_ready_signal.connect(self._on_file_downloaded, type=Qt.QueuedConnection)

def update(self):
def update(self) -> None:
icon = QLabel()
icon.setPixmap(load_image('file.png'))

if self.submission.is_downloaded:
if self.file.is_downloaded:
description = QLabel("Open")
else:
human_filesize = humanize_filesize(self.submission.size)
human_filesize = humanize_filesize(self.file.size)
description = QLabel("Download ({})".format(human_filesize))

if self.align != "left":
# Float right...
self.layout.addStretch(5)

self.layout.addWidget(icon)
self.layout.addWidget(description, 5)
self.layout.addStretch(5)

if self.align == "left":
# Add space on right hand side...
self.layout.addStretch(5)

def clear(self):
def clear(self) -> None:
while self.layout.count():
child = self.layout.takeAt(0)
if child.widget():
child.widget().deleteLater()

@pyqtSlot(str)
def _on_file_download(self, file_uuid: str) -> None:
if file_uuid == self.file_uuid:
def _on_file_downloaded(self, file_uuid: str) -> None:
if file_uuid == self.file.uuid:
# update state
self.file = self.controller.get_file(self.file.uuid)

# update gui
self.clear() # delete existing icon and label
self.update() # draw modified widget

Expand All @@ -1334,12 +1328,15 @@ def mouseReleaseEvent(self, e):
Handle a completed click via the program logic. The download state
of the file distinguishes which function in the logic layer to call.
"""
if self.submission.is_downloaded:
# update state
self.file = self.controller.get_file(self.file.uuid)

if self.file.is_downloaded:
# Open the already downloaded file.
self.controller.on_file_open(self.submission)
self.controller.on_file_open(self.file.uuid)
else:
# Download the file.
self.controller.on_file_download(self.source, self.submission)
self.controller.on_submission_download(File, self.file.uuid)


class ConversationView(QWidget):
Expand All @@ -1351,7 +1348,11 @@ class ConversationView(QWidget):
https://github.com/freedomofpress/securedrop-client/issues/273
"""

def __init__(self, source_db_object: Source, controller: Controller):
def __init__(
self,
source_db_object: Source,
controller: Controller,
):
super().__init__()
self.source = source_db_object
self.controller = controller
Expand Down Expand Up @@ -1403,8 +1404,12 @@ def add_file(self, source_db_object, submission_db_object):
Add a file from the source.
"""
self.conversation_layout.addWidget(
FileWidget(source_db_object, submission_db_object,
self.controller, self.controller.file_ready))
FileWidget(
submission_db_object.uuid,
self.controller,
self.controller.file_ready,
),
)

def update_conversation_position(self, min_val, max_val):
"""
Expand Down Expand Up @@ -1472,9 +1477,12 @@ class SourceConversationWrapper(QWidget):
per-source resources.
"""

def __init__(self, source: Source, controller: Controller) -> None:
def __init__(
self,
source: Source,
controller: Controller,
) -> None:
super().__init__()

layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
Expand Down
Loading

0 comments on commit 844a0c2

Please sign in to comment.