Skip to content

Commit 844a0c2

Browse files
authored
Merge pull request #374 from freedomofpress/api-queue
Implementation of the API queue
2 parents 30f9967 + d842bc7 commit 844a0c2

23 files changed

+1455
-709
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ mypy: ## Run static type checker
1414
securedrop_client/gui/__init__.py \
1515
securedrop_client/resources/__init__.py \
1616
securedrop_client/storage.py \
17-
securedrop_client/message_sync.py
17+
securedrop_client/message_sync.py \
18+
securedrop_client/queue.py
1819

1920
.PHONY: clean
2021
clean: ## Clean the workspace of generated resources

create_dev_data.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import os
44
import sys
55
from securedrop_client.config import Config
6-
from securedrop_client.db import Base, make_engine
6+
from securedrop_client.db import Base, make_session_maker
77

88
sdc_home = sys.argv[1]
9-
Base.metadata.create_all(make_engine(sdc_home))
9+
session = make_session_maker(sdc_home)()
10+
Base.metadata.create_all(bind=session.get_bind())
1011

1112
with open(os.path.join(sdc_home, Config.CONFIG_NAME), 'w') as f:
1213
f.write(json.dumps({

securedrop_client/app.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@
2525
import sys
2626
import socket
2727
from argparse import ArgumentParser
28-
from sqlalchemy.orm import sessionmaker
2928
from PyQt5.QtWidgets import QApplication, QMessageBox
3029
from PyQt5.QtCore import Qt, QTimer
3130
from logging.handlers import TimedRotatingFileHandler
3231
from securedrop_client import __version__
3332
from securedrop_client.logic import Controller
3433
from securedrop_client.gui.main import Window
3534
from securedrop_client.resources import load_icon, load_css
36-
from securedrop_client.db import make_engine
35+
from securedrop_client.db import make_session_maker
3736
from securedrop_client.utils import safe_mkdir
3837

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

186185
prevent_second_instance(app, args.sdc_home)
187186

187+
session_maker = make_session_maker(args.sdc_home)
188+
188189
gui = Window()
190+
189191
app.setWindowIcon(load_icon(gui.icon))
190192
app.setStyleSheet(load_css('sdclient.css'))
191193

192-
engine = make_engine(args.sdc_home)
193-
Session = sessionmaker(bind=engine)
194-
session = Session()
195-
196-
controller = Controller("http://localhost:8081/", gui, session,
194+
controller = Controller("http://localhost:8081/", gui, session_maker,
197195
args.sdc_home, not args.no_proxy)
198196
controller.setup()
199197

securedrop_client/crypto.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
import subprocess
2323
import tempfile
2424

25-
from sqlalchemy.orm import sessionmaker
25+
from sqlalchemy.orm import scoped_session
2626
from uuid import UUID
2727

2828
from securedrop_client.config import Config
29-
from securedrop_client.db import make_engine, Source
29+
from securedrop_client.db import Source
3030
from securedrop_client.utils import safe_mkdir
3131

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

4040
class GpgHelper:
4141

42-
def __init__(self, sdc_home: str, is_qubes: bool) -> None:
42+
def __init__(self, sdc_home: str, session_maker: scoped_session, is_qubes: bool) -> None:
4343
'''
4444
:param sdc_home: Home directory for the SecureDrop client
4545
:param is_qubes: Whether the client is running in Qubes or not
4646
'''
4747
safe_mkdir(os.path.join(sdc_home), "gpg")
4848
self.sdc_home = sdc_home
4949
self.is_qubes = is_qubes
50-
engine = make_engine(sdc_home)
51-
Session = sessionmaker(bind=engine)
52-
self.session = Session()
50+
self.session_maker = session_maker
5351

5452
config = Config.from_home_dir(self.sdc_home)
5553
self.journalist_key_fingerprint = config.journalist_key_fingerprint
@@ -110,13 +108,14 @@ def _gpg_cmd_base(self) -> list:
110108
return cmd
111109

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

115114
self._import(key_data)
116115

117116
local_source.fingerprint = fingerprint
118-
self.session.add(local_source)
119-
self.session.commit()
117+
session.add(local_source)
118+
session.commit()
120119

121120
def _import(self, key_data: str) -> None:
122121
'''Wrapper for `gpg --import-keys`'''
@@ -145,7 +144,8 @@ def encrypt_to_source(self, source_uuid: str, data: str) -> str:
145144
'''
146145
:param data: A string of data to encrypt to a source.
147146
'''
148-
source = self.session.query(Source).filter_by(uuid=source_uuid).one()
147+
session = self.session_maker()
148+
source = session.query(Source).filter_by(uuid=source_uuid).one()
149149
cmd = self._gpg_cmd_base()
150150

151151
with tempfile.NamedTemporaryFile('w+') as content, \

securedrop_client/db.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
from sqlalchemy import Boolean, Column, create_engine, DateTime, ForeignKey, Integer, String, \
66
Text, MetaData, CheckConstraint, text, UniqueConstraint
7-
from sqlalchemy.engine import Engine
87
from sqlalchemy.ext.declarative import declarative_base
9-
from sqlalchemy.orm import relationship, backref
8+
from sqlalchemy.orm import relationship, backref, scoped_session, sessionmaker
109

1110

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

2423

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

2930

3031
class Source(Base):
@@ -215,8 +216,5 @@ class User(Base):
215216
uuid = Column(String(36), unique=True, nullable=False)
216217
username = Column(String(255), nullable=False)
217218

218-
def __init__(self, username: str) -> None:
219-
self.username = username
220-
221219
def __repr__(self) -> str:
222220
return "<Journalist: {}>".format(self.username)

securedrop_client/gui/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
along with this program. If not, see <http://www.gnu.org/licenses/>.
2121
"""
2222
import logging
23+
2324
from gettext import gettext as _
2425
from typing import Dict, List, Optional # noqa: F401
25-
2626
from PyQt5.QtWidgets import QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QDesktopWidget, \
2727
QApplication
2828

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

4444
icon = 'icon.png'
4545

46-
def __init__(self):
46+
def __init__(self) -> None:
4747
"""
4848
Create the default start state. The window contains a root widget into
4949
which is placed:

securedrop_client/gui/widgets.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
"""
1919
import logging
2020
import arrow
21-
from gettext import gettext as _
2221
import html
2322
import sys
23+
24+
from gettext import gettext as _
2425
from typing import List
2526
from uuid import uuid4
26-
27-
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QTimer, QSize
27+
from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QTimer, QSize, pyqtBoundSignal, QObject
2828
from PyQt5.QtGui import QIcon, QPalette, QBrush, QColor, QFont, QLinearGradient
2929
from PyQt5.QtWidgets import QListWidget, QLabel, QWidget, QListWidgetItem, QHBoxLayout, \
3030
QPushButton, QVBoxLayout, QLineEdit, QScrollArea, QDialog, QAction, QMenu, QMessageBox, \
@@ -587,7 +587,7 @@ class MainView(QWidget):
587587
}
588588
'''
589589

590-
def __init__(self, parent):
590+
def __init__(self, parent: QObject):
591591
super().__init__(parent)
592592

593593
self.setStyleSheet(self.CSS)
@@ -1274,58 +1274,52 @@ class FileWidget(QWidget):
12741274
Represents a file.
12751275
"""
12761276

1277-
def __init__(self, source_db_object, submission_db_object,
1278-
controller, file_ready_signal, align="left"):
1277+
def __init__(
1278+
self,
1279+
file_uuid: str,
1280+
controller: Controller,
1281+
file_ready_signal: pyqtBoundSignal,
1282+
) -> None:
12791283
"""
1280-
Given some text, an indication of alignment ('left' or 'right') and
1281-
a reference to the controller, make something to display a file.
1282-
1283-
Align is set to left by default because currently SecureDrop can only
1284-
accept files from sources to journalists.
1284+
Given some text and a reference to the controller, make something to display a file.
12851285
"""
12861286
super().__init__()
12871287
self.controller = controller
1288-
self.source = source_db_object
1289-
self.submission = submission_db_object
1290-
self.file_uuid = self.submission.uuid
1291-
self.align = align
1288+
self.file = self.controller.get_file(file_uuid)
12921289

12931290
self.layout = QHBoxLayout()
12941291
self.update()
12951292
self.setLayout(self.layout)
12961293

1297-
file_ready_signal.connect(self._on_file_download)
1294+
file_ready_signal.connect(self._on_file_downloaded, type=Qt.QueuedConnection)
12981295

1299-
def update(self):
1296+
def update(self) -> None:
13001297
icon = QLabel()
13011298
icon.setPixmap(load_image('file.png'))
13021299

1303-
if self.submission.is_downloaded:
1300+
if self.file.is_downloaded:
13041301
description = QLabel("Open")
13051302
else:
1306-
human_filesize = humanize_filesize(self.submission.size)
1303+
human_filesize = humanize_filesize(self.file.size)
13071304
description = QLabel("Download ({})".format(human_filesize))
13081305

1309-
if self.align != "left":
1310-
# Float right...
1311-
self.layout.addStretch(5)
1312-
13131306
self.layout.addWidget(icon)
13141307
self.layout.addWidget(description, 5)
1308+
self.layout.addStretch(5)
13151309

1316-
if self.align == "left":
1317-
# Add space on right hand side...
1318-
self.layout.addStretch(5)
1319-
1320-
def clear(self):
1310+
def clear(self) -> None:
13211311
while self.layout.count():
13221312
child = self.layout.takeAt(0)
13231313
if child.widget():
13241314
child.widget().deleteLater()
13251315

13261316
@pyqtSlot(str)
1327-
def _on_file_download(self, file_uuid: str) -> None:
1328-
if file_uuid == self.file_uuid:
1317+
def _on_file_downloaded(self, file_uuid: str) -> None:
1318+
if file_uuid == self.file.uuid:
1319+
# update state
1320+
self.file = self.controller.get_file(self.file.uuid)
1321+
1322+
# update gui
13291323
self.clear() # delete existing icon and label
13301324
self.update() # draw modified widget
13311325

@@ -1334,12 +1328,15 @@ def mouseReleaseEvent(self, e):
13341328
Handle a completed click via the program logic. The download state
13351329
of the file distinguishes which function in the logic layer to call.
13361330
"""
1337-
if self.submission.is_downloaded:
1331+
# update state
1332+
self.file = self.controller.get_file(self.file.uuid)
1333+
1334+
if self.file.is_downloaded:
13381335
# Open the already downloaded file.
1339-
self.controller.on_file_open(self.submission)
1336+
self.controller.on_file_open(self.file.uuid)
13401337
else:
13411338
# Download the file.
1342-
self.controller.on_file_download(self.source, self.submission)
1339+
self.controller.on_submission_download(File, self.file.uuid)
13431340

13441341

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

1354-
def __init__(self, source_db_object: Source, controller: Controller):
1351+
def __init__(
1352+
self,
1353+
source_db_object: Source,
1354+
controller: Controller,
1355+
):
13551356
super().__init__()
13561357
self.source = source_db_object
13571358
self.controller = controller
@@ -1403,8 +1404,12 @@ def add_file(self, source_db_object, submission_db_object):
14031404
Add a file from the source.
14041405
"""
14051406
self.conversation_layout.addWidget(
1406-
FileWidget(source_db_object, submission_db_object,
1407-
self.controller, self.controller.file_ready))
1407+
FileWidget(
1408+
submission_db_object.uuid,
1409+
self.controller,
1410+
self.controller.file_ready,
1411+
),
1412+
)
14081413

14091414
def update_conversation_position(self, min_val, max_val):
14101415
"""
@@ -1472,9 +1477,12 @@ class SourceConversationWrapper(QWidget):
14721477
per-source resources.
14731478
"""
14741479

1475-
def __init__(self, source: Source, controller: Controller) -> None:
1480+
def __init__(
1481+
self,
1482+
source: Source,
1483+
controller: Controller,
1484+
) -> None:
14761485
super().__init__()
1477-
14781486
layout = QVBoxLayout()
14791487
layout.setContentsMargins(0, 0, 0, 0)
14801488
self.setLayout(layout)

0 commit comments

Comments
 (0)