diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2323555 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +image: python:latest + +test: + stage: test + script: + - pip3 install pytest + - python3 -m pytest -v diff --git a/README.md b/README.md new file mode 100644 index 0000000..28b9647 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# backupill + +Backupill (Backup Pill), generates barcoded PDF to backup text files on paper. Designed to backup ASCII-armored GnuPG and SSH key files and ciphertext. diff --git a/backupill/__init__.py b/backupill/__init__.py new file mode 100644 index 0000000..3741e61 --- /dev/null +++ b/backupill/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/python3 + +""" +Copyright (C) 2021- beucismis +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import os +import click +from .backuper import Pill + + +__version__ = "0.1.0" +__license__ = "GPL-3.0" +__author__ = "Adil Gurbuz" +__contact__ = "beucismis@tutamail.com" +__source__ = "https://github.com/beucismis/backupill" +__description__ = "Generates barcoded PDF to backup text files on paper." + + +@click.version_option(version=__version__) +@click.option("-f", "--file", help="ASC file path.") +@click.command(context_settings=dict(help_option_names=["-h", "--help"])) +def main(file): + """Generates barcoded PDF to backup text files on paper. + https://github.com/beucismis/backupill + """ + + if file: + if not os.path.isfile(file): + click.echo("File '{}' not found!".format(file)) + exit() + + Pill(asc_file=file).generate_backup() + click.echo("Done!") + else: + click.echo("Usage: packupill -f FILENAME.asc") + + +if __name__ == "__main__": + main() diff --git a/backupill/backuper.py b/backupill/backuper.py new file mode 100644 index 0000000..c219d3e --- /dev/null +++ b/backupill/backuper.py @@ -0,0 +1,110 @@ +import os + +# import re +import qrcode + +# import hashlib +import logging +from pyx import * + +# import subprocess +from tempfile import mkstemp +from datetime import datetime + + +TEXT_X_OFFSET = 0.6 +TEXT_Y_OFFSET = 8.2 +PLAINTEXT_MAXLINECHARS = 73 + +QRCODE_HEIGHT = 8 +QRCODE_PER_PAGE = 6 +QRCODE_MAX_BYTE = 140 +QRCODE_X_POS = [1.5, 11, 1.5, 11, 1.5, 11] +QRCODE_Y_POS = [18.7, 18.7, 10, 10, 1.2, 1.2] + +PF_STR = "A4" +PF_OBJ = document.paperformat.A4 + +# suppressing all the warnings +for name in logging.Logger.manager.loggerDict.keys(): + logging.getLogger(name).setLevel(logging.CRITICAL) + + +class Pill: + def __init__(self, asc_file): + self.ASC_FILE = asc_file + + self.pageno = 0 + self.pageid = 0 + self.code_blocks = [] + + self._init_pdf() + + with open(asc_file) as file: + self.ASCDATA = file.read() + + def _init_pdf(self): + unit.set(defaultunit="cm") + self.pdf = document.document() + + def _generate_barcode(self, chunkdata): + qr = qrcode.QRCode( + version=1, + border=4, + box_size=10, + error_correction=qrcode.constants.ERROR_CORRECT_L, + ) + qr.add_data(chunkdata) + qr.make(fit=True) + + im = qr.make_image(fill_color="black", back_color="white") + + return im + + def _finish_page(self, pdf, canvas, pageno): + canvas.text(10, 0.6, "Page {}".format(pageno + 1)) + pdf.append(document.page(canvas, paperformat=PF_OBJ, fittosize=0, centered=0)) + + def generate_backup(self): + chunkdata = "^1 " + c = canvas.canvas() + + for char in list(self.ASCDATA): + if len(chunkdata) + 1 > QRCODE_MAX_BYTE: + self.code_blocks.append(self._generate_barcode(chunkdata)) + chunkdata = "^" + str(len(self.code_blocks) + 1) + " " + chunkdata += char + + self.code_blocks.append(self._generate_barcode(chunkdata)) + + for bc in range(len(self.code_blocks)): + if self.pageid >= QRCODE_PER_PAGE: + self._finish_page(self.pdf, c, self.pageno) + c = canvas.canvas() + self.pageno += 1 + self.pageid = 0 + + c.text( + QRCODE_X_POS[self.pageid] + TEXT_X_OFFSET, + QRCODE_Y_POS[self.pageid] + TEXT_Y_OFFSET, + "{} ({}/{})".format( + text.escapestring(self.ASC_FILE), bc + 1, len(self.code_blocks) + ), + ) + + c.insert( + bitmap.bitmap( + QRCODE_X_POS[self.pageid], + QRCODE_Y_POS[self.pageid], + self.code_blocks[bc], + height=QRCODE_HEIGHT, + ) + ) + + self.pageid += 1 + + self._finish_page(self.pdf, c, self.pageno) + + fd, temp_barcode_path = mkstemp(".pdf", "qr_", ".") + self.pdf.writetofile(temp_barcode_path) + os.rename(temp_barcode_path.split(os.sep)[-1], self.ASC_FILE + ".pdf") diff --git a/backupill/gui/README.md b/backupill/gui/README.md new file mode 100644 index 0000000..ec16b0d --- /dev/null +++ b/backupill/gui/README.md @@ -0,0 +1,3 @@ +# backupill-gui + + diff --git a/backupill/gui/__init__.py b/backupill/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backupill/gui/workbench.py b/backupill/gui/workbench.py new file mode 100644 index 0000000..e69de29 diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..0af9879 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +twine +black +pytest +virtualenv diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..869346e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pyx +click +qrcode +pillow diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..240f07f --- /dev/null +++ b/setup.py @@ -0,0 +1,34 @@ +import sys +import backupill as bp +from setuptools import setup + + +if sys.version_info < (3, 5): + raise RuntimeError("backupill requires Python 3.5 or later") + +with open("README.md") as f: + long_description = f.read() + + +setup( + name="backupill", + packages=["backupill"], + version=bp.__version__, + description=bp.__description__, + long_description=long_description, + long_description_content_type="text/markdown", + url=bp.__source__, + author=bp.__author__, + author_email=bp.__contact__, + license=bp.__license__, + classifiers=[], + platforms=["Linux"], + python_requires=">=3.5", + install_requires=["pyx", "click", "qrcode"], + keywords=[], + entry_points={ + "console_scripts": [ + "backupill = backupill:main", + ], + }, +) diff --git a/test/test.asc b/test/test.asc new file mode 100644 index 0000000..5ba97fc --- /dev/null +++ b/test/test.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +dRYbPoN2pT4P +=XYxH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/test/test.asc.pdf b/test/test.asc.pdf new file mode 100644 index 0000000..7045150 Binary files /dev/null and b/test/test.asc.pdf differ diff --git a/test/test.py b/test/test.py new file mode 100644 index 0000000..b49f018 --- /dev/null +++ b/test/test.py @@ -0,0 +1,11 @@ +import os +import pytest +from backupill import Pill + + +this_dir, this_filename = os.path.split(__file__) + + +def test_generate_backup(): + pill = Pill(asc_file=os.path.join(this_dir, "test.asc")) + pill.generate_backup()