zipasta/zipasta.py

236 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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.
Foobar 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 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/>.
"""
import base64
import itertools
import random
import shutil
import string
import sys
import zlib
import pathlib
import tkinter as tk
from tkinter import filedialog as fdlg
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 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)
self._build_frame_arquivos(main_frame).grid(row=1, column=0)
main_frame.grid(row=0, column=0)
return main_frame
def _build_frame_caminho(self, parent):
_frame = self._make_frame(parent)
self._make_label(_frame, 'caminho').grid(row=0, column=0)
if self.path is not None:
self['caminho'] = self.path
self._make_button(_frame, 'caminho', '').grid(row=0, column=1)
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)
self._arquivos = tk.Listbox(_frame)
self._arquivos.grid(row=1, column=0, columnspan=3)
self._make_button(_frame, 'adicionar', '').grid(row=2, column=0)
self._make_button(_frame, 'extrair', '').grid(row=2, column=1)
self._make_button(_frame, 'deletar', '🗑').grid(row=2, column=2)
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)
_mainpath = self.path / '_z'
if _mainpath.is_dir():
for filename in _mainpath.iterdir():
if filename.is_dir() and filename.name != '_':
self.arquivos_prefixos[filename.name] = (filename, next(filename.iterdir()).name)
self._arquivos.insert(tk.END, filename.name)
def btn_adicionar(self):
_paths = fdlg.askopenfilenames(parent=self.parent, title='Selecione arquivos a codificar')
pastas_criar = []
for _path in _paths:
_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 = self.path / '_z'
while True:
prefix = _random_chars(3)
_datapath = _mainpath / '_' / prefix
if not _datapath.exists():
break
# _datapath.mkdir(parents=True, exist_ok=False)
print(f'Salvando {_path.name}({original_size // 1024}k) como {prefix}')
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
print(f'{digitos_usar} digitos para {num_pastas} pastas')
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)
self._read_folder()
def btn_deletar(self):
_datapath = self.path / '_z' / '_'
for i in self._arquivos.curselection():
pastas_deletar = []
_filename = self._arquivos.get(i)
_path, _prefix = self.arquivos_prefixos[_filename]
pastas_deletar.append(_path / _prefix)
pastas_deletar.append(_path)
for pasta in (_datapath / _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(_datapath / _prefix)
shutil.rmtree(_path)
self._read_folder()
def btn_extrair(self):
# Traverse the tuple returned by
# curselection method and print
# corresponding value(s) in the listbox
_sel = self._arquivos.curselection()
if len(_sel) == 1:
_filename = self._arquivos.get(_sel[0])
_new_filename = fdlg.asksaveasfilename(parent=self.parent, title='Salvar como...', initialfile=_filename)
if not _new_filename:
return
_new_filename = pathlib.Path(_new_filename).absolute()
self._extract_file(_filename, _new_filename)
else:
pasta_salvar = fdlg.askdirectory(parent=self.parent, mustexist=True, title='Pasta a salvar arquivos')
if not pasta_salvar:
return
pasta_salvar = pathlib.Path(pasta_salvar)
for i in self._arquivos.curselection():
_filename = self._arquivos.get(i)
_new_filename = pasta_salvar / _filename
self._extract_file(_filename, _new_filename)
def _extract_file(self, filename, new_filename):
_fullname, prefix = self.arquivos_prefixos[filename]
_datapath = self.path / '_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 main(argv=None):
root = tk.Tk()
root.title('Zipasta 0.1 © Clóvis Fabrício Costa')
app = App(root)
root.mainloop()
main(sys.argv)