Dear Diary - зашифрованный дневник в консоли на Python

Разработка /
Разработка: Dear Diary - зашифрованный дневник в консоли на Python
Я уже писал о том, как работать с шифрованными базами данных на 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('%s) %s' % (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('%A %B %d, %Y %I:%M%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»
0 комментариев
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.