Я уже писал о том, как работать с шифрованными базами данных на Python. И сегодня для закрепления материала мы напишем небольшую программу для хранения текстовых заметок в зашифрованном виде.
Для начала установим необходимые библиотеки. Шифрование возьмёт на себя SQLCipher — библиотека с открытым исходным кодом, которая шифрует базы SQLite алгоритмом AES-256 цепочками шифроблоков. Для этого установим соответствующий пакет:
$ pip install pysqlcipher
Также установим peewee для безопасной работы с записями базы данных:
$ pip install peewee
Работа с БД
Мы создадим подключение к базе данных и модель, представляющую таблицу с записями:
#!/usr/bin/env python
from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase
db = SqlCipherDatabase(None)
class Entry(Model):
content = TextField()
timestamp = DateTimeField(default=datetime.datetime.now)
class Meta:
database = db
def initialize(passphrase):
db.init('diary.db', passphrase=passphrase, kdf_iter=64000)
Entry.create_table(fail_silently=True)
В стандартной библиотеке python есть модуль для чтения паролей из stdin без вывода пароля на экран. Мы воспользуемся этим модулем для безопасного ввода пароля при разблокировке дневника.
В начале работы приложения мы принимаем пароль (как ключ к шифру БД), инициализируем базу данных, а затем выводим главное меню (о нём будет ниже):
#!/usr/bin/env python
from getpass import getpass
import sys
from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase
# ... Здесь код модели из предыдущего блока ...
if __name__ == '__main__':
# безопасно принимаем пароль.
passphrase = getpass('Enter password: ')
if not passphrase:
sys.stderr.write('Passphrase required to access diary.\n')
sys.stderr.flush()
sys.exit(1)
# инициализируем базу данных.
initialize(passphrase)
menu_loop()
Интерактивное меню
Обычно в мире *nix не принято делать интерактивные меню, это излишне усложняет программу. Однако для дневника, думаю, это отличный подход — так можно будет ввести пароль лишь один раз, а потом выполнять много различных операций в ней уже без пароля.
Для простоты сделаем наше меню выполняющим всего три операции:
- Добавление новой записи
- Вывод списка записей, отсортированных по дате создания
- Поиск записей, основываясь на поиске подстроки в тексте
Вот структура меню и его операции:
#!/usr/bin/env python
from collections import OrderedDict
import datetime
from getpass import getpass
import sys
from peewee import *
from playhouse.sqlcipher_ext import SqlCipherDatabase
# ... Здесь код модели из предыдущего блока ...
def menu_loop():
choice = None
while choice != 'q':
for key, value in menu.items():
print('{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s) {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}s' {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a} (key, value.__doc__))
choice = raw_input('Action: ').lower().strip()
if choice in menu:
menu[choice]()
def add_entry():
"""Добавление записи"""
def view_entries(search_query=None):
"""Вывод прошлых записей"""
def search_entries():
"""Поиск записей"""
menu = OrderedDict([
('a', add_entry),
('v', view_entries),
('s', search_entries),
])
if __name__ == '__main__':
# ... начало программы ...
Меню выводится после инициализации базы данных, предлагая выбор одного варианта из трёх. Программа завершает работу при нажатии q.
Создадим функцию add_entry. Она будет принимать строки, введённые пользователем, считывая их до получения EOF (Ctrl+d на моём компьютере). После ввода пользователем заметки, программа спросит о необходимости сохранить запись и вернётся в главное меню:
def add_entry():
"""Добавление записи"""
print('Enter your entry. Press ctrl+d when finished.')
data = sys.stdin.read().strip()
if data and raw_input('Save entry? [Yn] ') != 'n':
Entry.create(content=data)
print('Saved successfully.')
Вызов sys.stdin.read() автоматически читает до появления EOF. А так как menu_loop вызвал функцию, то при её завершении мы вернёмся обратно в главное меню.
Теперь создадим функцию view_entries, выводящую на экран сохранённые ранее записи. Мы будем выводить по одной записи на экран, а пользователь может либо запросить следующую, либо прекратить цикл и вернуться в меню.
Для повторного использования этой логики в дальнейшем, я написал функцию view_entries принимающей поисковый запрос:
def view_entries(search_query=None):
"""Вывод прошлых записей"""
query = Entry.select().order_by(Entry.timestamp.desc())
if search_query:
query = query.where(Entry.content.contains(search_query))
for entry in query:
timestamp = entry.timestamp.strftime('{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}A {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}B {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}d, {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}Y {33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}I:{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}M{33d8302486bd10b0fde64d2037652320e6f176a736d71849c0427b0d7398501a}p')
print(timestamp)
print('=' * len(timestamp))
print(entry.content)
print('n) next entry')
print('q) return to main menu')
if raw_input('Choice? (Nq) ') == 'q':
break
А метод search_entries будет вызывать view_entries с передачей поискового запроса от пользователя. Вот его код:
def search_entries():
"""Поиск записей"""
view_entries(raw_input('Search query: '))
На этом наша программа завершена!
Что можно улучшить
Попробуйте самостоятельно реализовать следующий функционал:
- Удаление записи
- Умный постраничный вывод списка записей
- Цвета в консоли для улучшения внешнего вида. Посмотрите в сторону Blessings для этого.
- Редактирование записей
- Календарный вид или поиск записей по дате
- Веб-фронтенд?
Полный исходный код программы можно скачать здесь.
По материалам «Dear Diary, an Encrypted Command-Line Diary with Python»
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).