zipasta/zipasta.py

334 lines
12 KiB
Python
Raw Permalink Normal View History

2022-09-29 09:05:27 -03:00
"""
Copyright © 2022 Clóvis Fabrício Costa - All Rights Reserved
This file is part of zipasta.
zipasta is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public
License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
version.
zipasta is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
2022-09-29 09:05:27 -03:00
of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with zipasta. If not,
see https://www.gnu.org/licenses/
2022-09-29 09:05:27 -03:00
"""
import argparse
2022-09-29 09:05:27 -03:00
import base64
import itertools
import random
import shutil
import string
import subprocess
2022-09-29 09:05:27 -03:00
import sys
import zlib
import pathlib
import tkinter as tk
from tkinter import filedialog as fdlg, ttk
2022-09-29 09:05:27 -03:00
PROGRAM_TITLE = 'Zipasta 0.2 © Clóvis Fabrício Costa'
2022-09-29 09:05:27 -03:00
MAX_SIZE = 230
_order_digits = tuple(itertools.chain(string.digits, string.ascii_uppercase, string.ascii_lowercase))
def _random_chars(number=1):
return ''.join(random.choice(_order_digits) for _ign in range(number))
class CommandDialog:
def __init__(self, parent, command, callback, timeout=200):
self.window = tk.Toplevel(parent)
self.window.geometry('600x100')
self.window.title('Aguarde...')
self.command = [sys.executable]
if not getattr(sys, 'frozen', False):
self.command.append(sys.argv[0])
self.command.extend(command)
self._callback = callback
self._timeout = timeout
self._p: subprocess.Popen | None = None
self._init_window()
self._devnull = None # open(os.devnull, 'rb+')
def _init_window(self):
self.progress = ttk.Progressbar(self.window, orient=tk.HORIZONTAL, length=100, mode='indeterminate')
self.progress.pack(expand=True)
self.progress.start()
self.window.grab_set()
def start(self):
self._p = subprocess.Popen(self.command, stdin=self._devnull, stdout=self._devnull,
stderr=subprocess.STDOUT)
self.window.after(self._timeout, self.wait)
def wait(self):
if self._p.poll() is not None:
self.progress.stop()
self.window.destroy()
self._callback(self)
else:
self.window.after(self._timeout, self.wait)
2022-09-29 09:05:27 -03:00
class App:
def __init__(self, parent, default_path: pathlib.Path = None):
self.parent = parent
self.vars = {}
self.labels = {}
self.buttons = {}
self.arquivos_prefixos = {}
self.path = default_path
self._build_gui()
self.btn_caminho()
if self.path is None:
sys.exit(1)
self._read_folder()
def _get_or_create_var(self, varname):
if varname not in self.vars:
self.vars[varname] = tk.StringVar()
return self.vars[varname]
def __setitem__(self, key, value):
self._get_or_create_var(key).set(str(value))
def __getitem__(self, varname):
return self.vars[varname].get()
def _make_label(self, parent, name, initial_text='', **kwds):
_var = self._get_or_create_var(name)
_lbl = self.labels[name] = tk.Label(parent, textvariable=_var, **kwds)
_var.set(initial_text)
return _lbl
def _make_button(self, parent, name, initial_text='', **kwds):
def _command():
getattr(self, f'btn_{name}')(**kwds)
_btn = self.buttons[name] = tk.Button(parent, text=initial_text, command=_command)
return _btn
@staticmethod
def _make_frame(parent, main_row=0, main_col=0):
_frame = tk.Frame(parent)
_frame.rowconfigure(main_row, weight=1)
_frame.columnconfigure(main_col, weight=1)
return _frame
def _build_gui(self):
main_frame = self._make_frame(self.parent, main_row=1)
self._build_frame_caminho(main_frame).grid(row=0, column=0, sticky='nswe')
self._build_frame_arquivos(main_frame).grid(row=1, column=0, sticky='nswe')
2022-09-29 09:05:27 -03:00
main_frame.grid(row=0, column=0, sticky='nswe')
2022-09-29 09:05:27 -03:00
return main_frame
def _build_frame_caminho(self, parent):
_frame = self._make_frame(parent)
self._make_label(_frame, 'caminho').grid(row=0, column=0, sticky='nswe')
2022-09-29 09:05:27 -03:00
if self.path is not None:
self['caminho'] = self.path
self._make_button(_frame, 'caminho', '').grid(row=0, column=1, sticky='nswe')
2022-09-29 09:05:27 -03:00
return _frame
def _build_frame_arquivos(self, parent):
_frame = self._make_frame(parent, main_row=1)
self._make_label(_frame, 'arquivos', 'Arquivos:').grid(row=0, column=0, columnspan=3, sticky='nswe')
2022-09-29 09:05:27 -03:00
self._arquivos = tk.Listbox(_frame)
self._arquivos.grid(row=1, column=0, columnspan=3, sticky='nswe')
self._make_button(_frame, 'adicionar', '').grid(row=2, column=0, sticky='e')
self._make_button(_frame, 'extrair', '').grid(row=2, column=1, sticky='e')
self._make_button(_frame, 'deletar', '🗑').grid(row=2, column=2, sticky='e')
2022-09-29 09:05:27 -03:00
return _frame
def btn_caminho(self):
_path = fdlg.askdirectory(parent=self.parent, title='Pasta destino', mustexist=True)
if _path:
_path = pathlib.Path(_path).absolute()
if _path.is_dir():
self['caminho'] = _path
self.path = _path
self._read_folder()
return
def _read_folder(self):
self._arquivos.delete(0, tk.END)
self.arquivos_prefixos = {}
for filename, prefix in _read_folder(self.path):
self.arquivos_prefixos[filename.name] = (filename, prefix)
self._arquivos.insert(tk.END, filename.name)
2022-09-29 09:05:27 -03:00
def btn_adicionar(self):
_paths = fdlg.askopenfilenames(parent=self.parent, title='Selecione arquivos a codificar')
if not _paths:
return
_cmd = [str(self.path), 'add']
_cmd.extend(str(path) for path in _paths)
_dl = CommandDialog(self.parent, _cmd, self._cmd_ok)
_dl.start()
def _cmd_ok(self, _dl=None):
2022-09-29 09:05:27 -03:00
self._read_folder()
def btn_deletar(self):
_datapath = self.path / '_z' / '_'
arquivos_deletar = [self._arquivos.get(i) for i in self._arquivos.curselection()]
if not arquivos_deletar:
return
_cmd = [str(self.path), 'delete']
_cmd.extend(arquivos_deletar)
_dl = CommandDialog(self.parent, _cmd, self._cmd_ok)
_dl.start()
2022-09-29 09:05:27 -03:00
def btn_extrair(self):
# Traverse the tuple returned by
# curselection method and print
# corresponding value(s) in the listbox
_sel = self._arquivos.curselection()
for i in self._arquivos.curselection():
2022-09-29 09:05:27 -03:00
_filename = self._arquivos.get(_sel[0])
_new_filename = fdlg.asksaveasfilename(parent=self.parent, title='Salvar como...', initialfile=_filename)
if not _new_filename:
return
_cmd = [str(self.path), 'extract', _filename, _new_filename]
_dl = CommandDialog(self.parent, _cmd, self._cmd_ok)
_dl.start()
def _read_folder(origem):
origem = pathlib.Path(origem) / '_z'
if origem.is_dir():
for filename in origem.iterdir():
if filename.is_dir() and filename.name != '_':
yield filename, next(filename.iterdir()).name
def _cmd_extract(origem, arquivo, new_filename):
origem = pathlib.Path(origem)
new_filename = pathlib.Path(new_filename)
arquivos_prefixos = {
_path.name: (_path, _prefix) for _path, _prefix in _read_folder(origem)
}
_fullname, prefix = arquivos_prefixos[arquivo]
_datapath = origem / '_z' / '_' / prefix
data = bytearray()
for folder in sorted(_datapath.rglob('*.*'), key=str):
data += folder.name.rpartition('.')[-1].encode('ascii')
data = base64.urlsafe_b64decode(data)
data = zlib.decompress(data)
with new_filename.open('wb') as f:
f.write(data)
def _cmd_delete(destino, _arquivos):
destino = pathlib.Path(destino)
arquivos_prefixos = {
_path.name: (_path, _prefix) for _path, _prefix in _read_folder(destino)
}
for _filename in _arquivos:
_path, _prefix = arquivos_prefixos[_filename]
# pastas_deletar = []
# pastas_deletar.append(_path / _prefix)
# pastas_deletar.append(_path)
# for pasta in (destino / _prefix).rglob('*'):
# pastas_deletar.append(pasta)
# print(f'Deletando {len(pastas_deletar)} pastas...')
# pastas_deletar.sort(key=lambda x: len(str(x)), reverse=True)
# for pasta in pastas_deletar:
# pasta.rmdir()
shutil.rmtree(destino / '_z' / _prefix, ignore_errors=True)
shutil.rmtree(_path, ignore_errors=True)
def _cmd_add(destino, _arquivos):
destino = pathlib.Path(destino)
pastas_criar = []
for _path in _arquivos:
_path = pathlib.Path(_path)
with _path.open('rb') as f:
data = f.read()
original_size = len(data)
data = zlib.compress(data, level=8)
data = base64.urlsafe_b64encode(data)
_mainpath = destino / '_z'
while True:
prefix = _random_chars(3)
_datapath = _mainpath / '_' / prefix
if not _datapath.exists():
break
# _datapath.mkdir(parents=True, exist_ok=False)
pastas_criar.append(_datapath)
pastas_criar.append(_mainpath / str(_path.name) / prefix)
tamanho_restante = MAX_SIZE - (len(str(_datapath.absolute())) + 1)
digitos_usar = 1
while True:
bytes_por_pasta = tamanho_restante - (digitos_usar + (digitos_usar // 2) + 1)
num_pastas = len(data) // bytes_por_pasta
if num_pastas < len(_order_digits) ** digitos_usar:
break
digitos_usar += 1
for pos, prefixos in enumerate(itertools.product(_order_digits, repeat=digitos_usar)):
pedaco = data[pos * bytes_por_pasta:(pos + 1) * bytes_por_pasta].decode("ascii")
if not pedaco:
break
prefixos = list(prefixos)
pasta_criar = _datapath
while len(prefixos) > 2:
pasta_criar = pasta_criar / ''.join(prefixos[:2])
prefixos = prefixos[2:]
nome_pasta = f'{"".join(prefixos)}.{pedaco}'
pasta_criar = pasta_criar / nome_pasta
pastas_criar.append(pasta_criar)
for pasta in pastas_criar:
pasta.mkdir(parents=True)
def parse_arguments(argv):
parser = argparse.ArgumentParser(description=PROGRAM_TITLE)
parser.add_argument('drive', metavar='DRIVE', type=pathlib.Path, help='Caminho do drive')
subparsers = parser.add_subparsers(help='Comando', dest='cmd')
cmd_add = subparsers.add_parser('add')
cmd_add.add_argument('filenames', metavar='FILENAME', type=pathlib.Path, nargs='+', help='Files to add')
cmd_extract = subparsers.add_parser('extract')
cmd_extract.add_argument('filename', metavar='FILENAME', type=str, help='File to extract', )
cmd_extract.add_argument('destination', metavar='DESTINATION', type=pathlib.Path, help='Where to extract')
cmd_delete = subparsers.add_parser('delete')
cmd_delete.add_argument('filenames', metavar='FILENAME', type=str, nargs='+', help='Files to add')
opts = parser.parse_args(argv)
return opts
2022-09-29 09:05:27 -03:00
def main(argv=None):
if len(argv) > 1:
opts = parse_arguments(argv[1:])
if opts.cmd == 'add':
_cmd_add(opts.drive, opts.filenames)
elif opts.cmd == 'extract':
_cmd_extract(opts.drive, opts.filename, opts.destination)
elif opts.cmd == 'delete':
_cmd_delete(opts.drive, opts.filenames)
else:
root = tk.Tk()
root.title(PROGRAM_TITLE)
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.geometry('800x600')
app = App(root)
root.mainloop()
2022-09-29 09:05:27 -03:00
main(sys.argv)