Add the ability to choose the database path at startup. Add more tests. Add bandit
This commit is contained in:
parent
8c7226964a
commit
6bc5b66d3f
16 changed files with 297 additions and 97 deletions
|
|
@ -433,7 +433,7 @@ class DBManager:
|
|||
"""
|
||||
if not name:
|
||||
return "#CCCCCC"
|
||||
h = int(hashlib.sha1(name.encode("utf-8")).hexdigest()[:8], 16)
|
||||
h = int(hashlib.sha1(name.encode("utf-8")).hexdigest()[:8], 16) # nosec
|
||||
return _TAG_COLORS[h % len(_TAG_COLORS)]
|
||||
|
||||
# -------- Tags: per-page -------------------------------------------
|
||||
|
|
@ -514,7 +514,7 @@ class DBManager:
|
|||
SELECT id, name
|
||||
FROM tags
|
||||
WHERE name IN ({placeholders});
|
||||
""",
|
||||
""", # nosec
|
||||
tuple(final_tag_names),
|
||||
).fetchall()
|
||||
ids_by_name = {r["name"]: r["id"] for r in rows}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
)
|
||||
|
||||
from . import strings
|
||||
|
|
@ -18,32 +22,85 @@ class KeyPrompt(QDialog):
|
|||
parent=None,
|
||||
title: str = strings._("key_prompt_enter_key"),
|
||||
message: str = strings._("key_prompt_enter_key"),
|
||||
initial_db_path: str | Path | None = None,
|
||||
show_db_change: bool = False,
|
||||
):
|
||||
"""
|
||||
Prompt the user for the key required to decrypt the database.
|
||||
|
||||
Used when opening the app, unlocking the idle locked screen,
|
||||
or when rekeying.
|
||||
|
||||
If show_db_change is true, also show a QFileDialog allowing to
|
||||
select a database file, else the default from settings is used.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
|
||||
self._db_path: Path | None = Path(initial_db_path) if initial_db_path else None
|
||||
|
||||
v = QVBoxLayout(self)
|
||||
|
||||
v.addWidget(QLabel(message))
|
||||
self.edit = QLineEdit()
|
||||
self.edit.setEchoMode(QLineEdit.Password)
|
||||
v.addWidget(self.edit)
|
||||
|
||||
# DB chooser
|
||||
self.path_edit: QLineEdit | None = None
|
||||
if show_db_change:
|
||||
path_row = QHBoxLayout()
|
||||
self.path_edit = QLineEdit()
|
||||
if self._db_path is not None:
|
||||
self.path_edit.setText(str(self._db_path))
|
||||
|
||||
browse_btn = QPushButton(strings._("select_notebook"))
|
||||
|
||||
def _browse():
|
||||
start_dir = str(self._db_path or "")
|
||||
fname, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
strings._("select_notebook"),
|
||||
start_dir,
|
||||
"SQLCipher DB (*.db);;All files (*)",
|
||||
)
|
||||
if fname:
|
||||
self._db_path = Path(fname)
|
||||
if self.path_edit is not None:
|
||||
self.path_edit.setText(fname)
|
||||
|
||||
browse_btn.clicked.connect(_browse)
|
||||
|
||||
path_row.addWidget(self.path_edit, 1)
|
||||
path_row.addWidget(browse_btn)
|
||||
v.addLayout(path_row)
|
||||
|
||||
# Key entry
|
||||
self.key_entry = QLineEdit()
|
||||
self.key_entry.setEchoMode(QLineEdit.Password)
|
||||
v.addWidget(self.key_entry)
|
||||
|
||||
toggle = QPushButton(strings._("show"))
|
||||
toggle.setCheckable(True)
|
||||
toggle.toggled.connect(
|
||||
lambda c: self.edit.setEchoMode(
|
||||
lambda c: self.key_entry.setEchoMode(
|
||||
QLineEdit.Normal if c else QLineEdit.Password
|
||||
)
|
||||
)
|
||||
v.addWidget(toggle)
|
||||
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
v.addWidget(bb)
|
||||
|
||||
self.key_entry.setFocus()
|
||||
self.resize(500, self.sizeHint().height())
|
||||
|
||||
def key(self) -> str:
|
||||
return self.edit.text()
|
||||
return self.key_entry.text()
|
||||
|
||||
def db_path(self) -> Path | None:
|
||||
"""Return the chosen DB path (or None if unchanged/not shown)."""
|
||||
if self.path_edit is not None:
|
||||
text = self.path_edit.text().strip()
|
||||
if text:
|
||||
return Path(text)
|
||||
return self._db_path
|
||||
|
|
|
|||
|
|
@ -147,5 +147,7 @@
|
|||
"stats_heatmap_metric": "Colour by",
|
||||
"stats_metric_words": "Words",
|
||||
"stats_metric_revisions": "Revisions",
|
||||
"stats_no_data": "No statistics available yet."
|
||||
"stats_no_data": "No statistics available yet.",
|
||||
"select_notebook": "Select notebook"
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -329,14 +329,14 @@ class MainWindow(QMainWindow):
|
|||
try:
|
||||
self.db = DBManager(self.cfg)
|
||||
ok = self.db.connect()
|
||||
return ok
|
||||
except Exception as e:
|
||||
if str(e) == "file is not a database":
|
||||
error = strings._("db_key_incorrect")
|
||||
else:
|
||||
error = str(e)
|
||||
QMessageBox.critical(self, strings._("db_database_error"), error)
|
||||
return False
|
||||
return ok
|
||||
sys.exit(1)
|
||||
|
||||
def _prompt_for_key_until_valid(self, first_time: bool) -> bool:
|
||||
"""
|
||||
|
|
@ -349,10 +349,20 @@ class MainWindow(QMainWindow):
|
|||
title = strings._("unlock_encrypted_notebook")
|
||||
message = strings._("unlock_encrypted_notebook_explanation")
|
||||
while True:
|
||||
dlg = KeyPrompt(self, title, message)
|
||||
dlg = KeyPrompt(
|
||||
self, title, message, initial_db_path=self.cfg.path, show_db_change=True
|
||||
)
|
||||
if dlg.exec() != QDialog.Accepted:
|
||||
return False
|
||||
self.cfg.key = dlg.key()
|
||||
|
||||
# Update DB path if the user changed it
|
||||
new_path = dlg.db_path()
|
||||
if new_path is not None and new_path != self.cfg.path:
|
||||
self.cfg.path = new_path
|
||||
# Persist immediately so next run is pre-filled with this file
|
||||
save_db_config(self.cfg)
|
||||
|
||||
if self._try_connect():
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -13,15 +13,31 @@ def get_settings() -> QSettings:
|
|||
return QSettings(APP_ORG, APP_NAME)
|
||||
|
||||
|
||||
def _default_db_location() -> Path:
|
||||
"""Where we put the notebook if nothing has been configured yet."""
|
||||
base = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
|
||||
base.mkdir(parents=True, exist_ok=True)
|
||||
return base / "notebook.db"
|
||||
|
||||
|
||||
def load_db_config() -> DBConfig:
|
||||
s = get_settings()
|
||||
default_db_path = str(
|
||||
Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
|
||||
/ "notebook.db"
|
||||
)
|
||||
|
||||
path = Path(s.value("db/path", default_db_path))
|
||||
# --- DB Path -------------------------------------------------------
|
||||
# Prefer the new key; fall back to the legacy one.
|
||||
path_str = s.value("db/default_db", "", type=str)
|
||||
if not path_str:
|
||||
legacy = s.value("db/path", "", type=str)
|
||||
if legacy:
|
||||
path_str = legacy
|
||||
# Optional: migrate and clean up the old key
|
||||
s.setValue("db/default_db", legacy)
|
||||
s.remove("db/path")
|
||||
path = Path(path_str) if path_str else _default_db_location()
|
||||
|
||||
# --- Other settings ------------------------------------------------
|
||||
key = s.value("db/key", "")
|
||||
|
||||
idle = s.value("ui/idle_minutes", 15, type=int)
|
||||
theme = s.value("ui/theme", "system", type=str)
|
||||
move_todos = s.value("ui/move_todos", False, type=bool)
|
||||
|
|
@ -38,7 +54,7 @@ def load_db_config() -> DBConfig:
|
|||
|
||||
def save_db_config(cfg: DBConfig) -> None:
|
||||
s = get_settings()
|
||||
s.setValue("db/path", str(cfg.path))
|
||||
s.setValue("db/default_db", str(cfg.path))
|
||||
s.setValue("db/key", str(cfg.key))
|
||||
s.setValue("ui/idle_minutes", str(cfg.idle_minutes))
|
||||
s.setValue("ui/theme", str(cfg.theme))
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@ from PySide6.QtWidgets import (
|
|||
QLabel,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QFileDialog,
|
||||
QDialogButtonBox,
|
||||
QRadioButton,
|
||||
QSizePolicy,
|
||||
|
|
@ -47,7 +44,7 @@ class SettingsDialog(QDialog):
|
|||
self.setMinimumWidth(560)
|
||||
self.setSizeGripEnabled(True)
|
||||
|
||||
current_settings = load_db_config()
|
||||
self.current_settings = load_db_config()
|
||||
|
||||
# Add theme selection
|
||||
theme_group = QGroupBox(strings._("theme"))
|
||||
|
|
@ -58,7 +55,7 @@ class SettingsDialog(QDialog):
|
|||
self.theme_dark = QRadioButton(strings._("dark"))
|
||||
|
||||
# Load current theme from settings
|
||||
current_theme = current_settings.theme
|
||||
current_theme = self.current_settings.theme
|
||||
if current_theme == Theme.DARK.value:
|
||||
self.theme_dark.setChecked(True)
|
||||
elif current_theme == Theme.LIGHT.value:
|
||||
|
|
@ -80,7 +77,7 @@ class SettingsDialog(QDialog):
|
|||
|
||||
self.locale_combobox = QComboBox()
|
||||
self.locale_combobox.addItems(strings._AVAILABLE)
|
||||
self.locale_combobox.setCurrentText(current_settings.locale)
|
||||
self.locale_combobox.setCurrentText(self.current_settings.locale)
|
||||
locale_layout.addWidget(self.locale_combobox, 0, Qt.AlignLeft)
|
||||
|
||||
# Explanation for locale
|
||||
|
|
@ -104,26 +101,12 @@ class SettingsDialog(QDialog):
|
|||
self.move_todos = QCheckBox(
|
||||
strings._("move_yesterdays_unchecked_todos_to_today_on_startup")
|
||||
)
|
||||
self.move_todos.setChecked(current_settings.move_todos)
|
||||
self.move_todos.setChecked(self.current_settings.move_todos)
|
||||
self.move_todos.setCursor(Qt.PointingHandCursor)
|
||||
|
||||
behaviour_layout.addWidget(self.move_todos)
|
||||
form.addRow(behaviour_group)
|
||||
|
||||
self.path_edit = QLineEdit(str(self._cfg.path))
|
||||
self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
browse_btn = QPushButton(strings._("browse"))
|
||||
browse_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
browse_btn.clicked.connect(self._browse)
|
||||
path_row = QWidget()
|
||||
h = QHBoxLayout(path_row)
|
||||
h.setContentsMargins(0, 0, 0, 0)
|
||||
h.addWidget(self.path_edit, 1)
|
||||
h.addWidget(browse_btn, 0)
|
||||
h.setStretch(0, 1)
|
||||
h.setStretch(1, 0)
|
||||
form.addRow(strings._("database_path"), path_row)
|
||||
|
||||
# Encryption settings
|
||||
enc_group = QGroupBox(strings._("encryption"))
|
||||
enc = QVBoxLayout(enc_group)
|
||||
|
|
@ -132,7 +115,7 @@ class SettingsDialog(QDialog):
|
|||
|
||||
# Checkbox to remember key
|
||||
self.save_key_btn = QCheckBox(strings._("remember_key"))
|
||||
self.key = current_settings.key or ""
|
||||
self.key = self.current_settings.key or ""
|
||||
self.save_key_btn.setChecked(bool(self.key))
|
||||
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
||||
self.save_key_btn.toggled.connect(self._save_key_btn_clicked)
|
||||
|
|
@ -236,16 +219,6 @@ class SettingsDialog(QDialog):
|
|||
v.addLayout(form)
|
||||
v.addWidget(bb, 0, Qt.AlignRight)
|
||||
|
||||
def _browse(self):
|
||||
p, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
strings._("database_path"),
|
||||
self.path_edit.text(),
|
||||
"(*.db);;(*)",
|
||||
)
|
||||
if p:
|
||||
self.path_edit.setText(p)
|
||||
|
||||
def _save(self):
|
||||
# Save the selected theme into QSettings
|
||||
if self.theme_dark.isChecked():
|
||||
|
|
@ -258,7 +231,7 @@ class SettingsDialog(QDialog):
|
|||
key_to_save = self.key if self.save_key_btn.isChecked() else ""
|
||||
|
||||
self._cfg = DBConfig(
|
||||
path=Path(self.path_edit.text()),
|
||||
path=Path(self.current_settings.path),
|
||||
key=key_to_save,
|
||||
idle_minutes=self.idle_spin.value(),
|
||||
theme=selected_theme.value,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue