Compare commits

...

2 Commits
0.1a ... master

Author SHA1 Message Date
Clovis Fabricio Costa 18cd0ce064 Utilização de subprocesso para execução, evitando o travamento da interface. 2022-10-04 13:33:13 -03:00
Clovis Fabricio Costa 2f89ab3d01 Acertos na documentação e texto de licença 2022-09-29 09:23:28 -03:00
3 changed files with 210 additions and 105 deletions

View File

@ -6,7 +6,7 @@ Codifica e decodifica arquivos em forma de nome de pastas vazias
## Instalação
#### Executável windows
Baixe o último zip na seção de releases e extraia (pode estar desatualizado)
Baixe o último zip na [seção de versões](https://fontes.dasua.casa/nosklo/zipasta/releases) e extraia (pode estar desatualizado).
#### Código fonte
Faça um clone do repositório e rode no python; O código não tem dependências externas.
@ -26,3 +26,10 @@ compõem o conteúdo deste arquivo serão excluídas do dispositivo sem confirma
> **Seja paciente, arquivos grandes podem demorar bastante para processar e o programa pode não responder por um tempo.**
## `setup.py`
Permite criar executável com o `cx_freeze`:
pip install cx_freeze
python setup.py build

View File

@ -7,11 +7,11 @@ zipasta is free software: you can redistribute it and/or modify it under the ter
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
zipasta 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/>.
see https://www.gnu.org/licenses/
"""
import sys

View File

@ -7,25 +7,27 @@ zipasta is free software: you can redistribute it and/or modify it under the ter
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
zipasta 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/>.
see https://www.gnu.org/licenses/
"""
import argparse
import base64
import itertools
import random
import shutil
import string
import subprocess
import sys
import zlib
import pathlib
import tkinter as tk
from tkinter import filedialog as fdlg
from tkinter import filedialog as fdlg, ttk
PROGRAM_TITLE = 'Zipasta 0.2 © Clóvis Fabrício Costa'
MAX_SIZE = 230
@ -36,6 +38,41 @@ 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)
class App:
def __init__(self, parent, default_path: pathlib.Path = None):
self.parent = parent
@ -83,28 +120,28 @@ class App:
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)
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')
main_frame.grid(row=0, column=0)
main_frame.grid(row=0, column=0, sticky='nswe')
return main_frame
def _build_frame_caminho(self, parent):
_frame = self._make_frame(parent)
self._make_label(_frame, 'caminho').grid(row=0, column=0)
self._make_label(_frame, 'caminho').grid(row=0, column=0, sticky='nswe')
if self.path is not None:
self['caminho'] = self.path
self._make_button(_frame, 'caminho', '').grid(row=0, column=1)
self._make_button(_frame, 'caminho', '').grid(row=0, column=1, sticky='nswe')
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._make_label(_frame, 'arquivos', 'Arquivos:').grid(row=0, column=0, columnspan=3, sticky='nswe')
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)
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')
return _frame
def btn_caminho(self):
@ -119,117 +156,178 @@ class App:
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)
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)
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}')
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()
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)
def _cmd_ok(self, _dl=None):
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()
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()
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:
for i in self._arquivos.curselection():
_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)
_cmd = [str(self.path), 'extract', _filename, _new_filename]
_dl = CommandDialog(self.parent, _cmd, self._cmd_ok)
_dl.start()
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 _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
def main(argv=None):
root = tk.Tk()
root.title('Zipasta 0.1 © Clóvis Fabrício Costa')
if len(argv) > 1:
opts = parse_arguments(argv[1:])
app = App(root)
root.mainloop()
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()
main(sys.argv)