diff --git a/__main__.py b/__main__.py index cc4f4e9..a99cbc6 100644 --- a/__main__.py +++ b/__main__.py @@ -6,7 +6,7 @@ import god.log as log import god.quotes as quotes import god.config as config -import god.updater as updater +import god.version as version def load_settings(): @@ -51,7 +51,7 @@ def main(): cli.clear() cli.header() - updater.check_updates() + version.check_updates() load_settings() load_phrases() diff --git a/apply_update.py b/apply_update.py index 3e5cc4a..ab68bdb 100644 --- a/apply_update.py +++ b/apply_update.py @@ -1,16 +1,29 @@ +"""Aplica a última atualização baixada + +Esse arquivo é basicamente um `.cmd` glorificado +""" + import os +import os.path if os.path.isdir(".update"): - os.system("move .update\\*.* .\\ >nul 2>nul") + # Atualiza tudo da pasta raíz + os.system("move /Y .update\\*.* .\\ >nul 2>nul") + # Cria e atualiza cada uma das subpastas necessárias for subdir in os.walk(".update"): dirname = subdir[0].replace(".update\\", '') + os.system( f"if not exist .\\{dirname}" + f" mkdir .\\{dirname} >nul 2>nul") + os.system( - f"move .update\\{dirname}\\*.* .\\{dirname}\\ " + + f"move /Y .update\\{dirname}\\*.* .\\{dirname}\\ " + ">nul 2>nul") +# Apaga a .update (agora vazia) os.system("rmdir .update /s /q >nul 2>nul") + +# Roda o god os.system("start python .") diff --git a/god/cli.py b/god/cli.py index 3cfc681..d44972e 100644 --- a/god/cli.py +++ b/god/cli.py @@ -1,6 +1,7 @@ import os import god +import god.version as version import god.config as config import god.quotes as quotes @@ -27,7 +28,7 @@ def header(color=Fore.BLUE): newline() print(color + f.renderText('G o d')) newline() - print(color + "v2.0.0".center(width())) + print(color + version.current().center(width())) print(flush=True) @@ -95,6 +96,16 @@ def read_command(): return input().strip() +def yesno(prompt="Confirmar?"): + while True: + answer = input(f"\t{prompt} [S/n] ").lower().strip() + if answer in ['s', '', 'n']: + break + + newline() + return answer.lower().strip() in ['s', ''] + + def interactive_header(): header(Fore.RED if god.state == 'alert' else Fore.BLUE) print_random_phrase() diff --git a/god/interactive.py b/god/interactive.py index 70d9356..4496bac 100644 --- a/god/interactive.py +++ b/god/interactive.py @@ -2,6 +2,7 @@ import sys import god +import god.version as version import god.cli as cli import god.log as log import god.config as config @@ -45,8 +46,8 @@ def cmd_quit(): @no_arg def cmd_help(): - print(Fore.YELLOW + """ - GOD v2.0.0, by PD16 + print(Fore.YELLOW + f""" + GOD {version.current()}, by PD16 sm|threshold X : Define o limite de memória para X Kb sp|process X : Define o processo monitorado para X diff --git a/god/updater.py b/god/updater.py deleted file mode 100644 index 98793ac..0000000 --- a/god/updater.py +++ /dev/null @@ -1,106 +0,0 @@ -import requests -import zipfile -import tempfile -import urllib.request -import io -import os -import sys -import os.path -import glob - -from shutil import copyfile - -import god.cli as cli -import god.log as log - -_GITHUB_API_URL = "https://api.github.com" -_REPO = "/repos/GuiBrandt/god" - - -def fetch_latest_release(): - r = requests.get(_GITHUB_API_URL + _REPO + "/releases/latest") - return r.json() - - -def fetch_zip(release): - zip_url = release['zipball_url'] - r = requests.get(zip_url, stream=True) - zip_file = zipfile.ZipFile(io.BytesIO(r.content)) - return zip_file - - -def check_updates(): - cli.i_am("Procurando atualizações...") - - try: - latest_release = fetch_latest_release() - except Exception as e: - log.error("fetch-releases", e) - cli.error("Falha. Verifique sua conexão com a internet.") - return - - cli.info("Encontrado: " + latest_release['tag_name']) - - if os.path.isfile(".version"): - with open(".version", "r") as version_file: - if latest_release['tag_name'] == version_file.readline().strip(): - cli.success("O god está atualizado.") - return - - while True: - answer = input("\tAtualizar? [Y/n] ").lower().strip() - if answer in ['y', '', 'n']: - break - - cli.newline() - - if not answer.lower().strip() in ['y', '']: - return - - cli.i_am("Obtendo arquivo zip...") - release_zip_file = fetch_zip(latest_release) - cli.success("OK") - - tmp_dir = tempfile.mkdtemp() - - cli.i_am("Extraindo...") - root_dir = release_zip_file.namelist()[0] - release_zip_file.extractall(path=tmp_dir) - cli.success("OK") - - tmp_dir = f"{tmp_dir}/{root_dir}".replace("\\", "/") - - cli.i_am("Instalando dependências...") - requirements_file = f"{tmp_dir}/requirements.txt" - result = os.system( - f"pip install --upgrade -r \"{requirements_file}\" >nul 2>nul") - - if result == 0: - cli.success("OK") - else: - cli.error("Falha na instalação. Abortando atualização...") - log.error("update", "Falha na instação das dependências") - return - - cli.i_am("Updating source files...") - main_file = f"{tmp_dir}/__main__.py" - source_files = glob.glob(f"{tmp_dir}/**/*.py") - - for f in source_files: - path = ".update/" + \ - os.path.dirname(f).replace("\\", "/").replace(tmp_dir, '') - if not os.path.isdir(path): - os.makedirs(path) - - copyfile(main_file, f".update/__main__.py") - - for f in source_files: - path = os.path.dirname(f).replace("\\", "/").replace(tmp_dir, '') - copyfile(f, f".update/{path}/{os.path.basename(f)}") - cli.success("OK") - - with open(".version", "w") as version_file: - version_file.write(latest_release['tag_name']) - - os.system("start python apply_update.py") - sys.exit(0) diff --git a/god/version.py b/god/version.py new file mode 100644 index 0000000..6c7caa7 --- /dev/null +++ b/god/version.py @@ -0,0 +1,178 @@ +"""Gerenciador de versão e atualização + +Esse módulo gerencia a versão do God e faz o pareamento com a última versão +disponível no GitHub, e faz a atualização de forma automática. +""" + +import requests +import zipfile +import tempfile +import io +import os +import sys +import os.path +import glob + +from shutil import copyfile + +import god.cli as cli +import god.log as log + +# Informações do GitHub +_GITHUB_API_URL = "https://api.github.com" +_REPO = "GuiBrandt/god" + +# Informação da versão atual +if os.path.isfile(".version"): + with open(".version", "r") as version_file: + _VERSION = version_file.readline().strip() +else: + _VERSION = None + + +def current(): + """Obtém a versão atual do God""" + + return _VERSION + + +def check_updates(): + """Executa o procedimento de busca e instalação de atualizações""" + + cli.i_am("Procurando atualizações...") + + try: + latest_release = _fetch_latest_release() + except Exception as e: + log.error("fetch-releases", e) + cli.error("Falha. Verifique sua conexão com a internet.") + return + + cli.info("Encontrado: " + latest_release['tag_name']) + + if latest_release['tag_name'] == current(): + cli.success("O god está atualizado.") + return + + cli.error("O god está desatualizado!") + + if cli.yesno("Atualizar?"): + update_dir = _download_update(latest_release) + _install_update(update_dir) + + _VERSION = latest_release['tag_name'] + + with open(".version", "w") as version_file: + version_file.write(_VERSION) + + # os.system("start python apply_update.py") + sys.exit(0) + + +def _fetch_latest_release(): + """Obtém informações da última release no GitHub em JSON""" + + r = requests.get(f"{_GITHUB_API_URL}/repos/{_REPO}/releases/latest") + return r.json() + + +def _download_update(release): + """Faz download e extrai o zip de uma release + + Parâmetros + ---------- + release : json + JSON da release, retornado pelo `_fetch_latest_release` + + Retorno + ------- + O caminho da pasta com os arquivos do `.zip` extraídos + """ + + cli.i_am("Obtendo arquivo zip...") + zip_url = release['zipball_url'] + r = requests.get(zip_url, stream=True) + release_zip_file = zipfile.ZipFile(io.BytesIO(r.content)) + cli.success("OK") + + cli.i_am("Extraindo...") + tmp_dir = tempfile.mkdtemp() + root_dir = release_zip_file.namelist()[0] + release_zip_file.extractall(path=tmp_dir) + cli.success("OK") + + tmp_dir = f"{tmp_dir}/{root_dir}".replace("\\", "/") + + return tmp_dir + + +def _install_update(update_dir): + """Faz a instalação de uma atualização + + Isso envolve resolver dependências com pip e mover os arquivos + baixados para suas respectivas pastas. + + TODO: Implementar remoção de arquivos não utilizados + """ + + cli.i_am("Instalando dependências...") + requirements_file = f"{update_dir}/requirements.txt" + result = os.system( + f"pip install --upgrade -r \"{requirements_file}\" >nul 2>nul") + + if result == 0: + cli.success("OK") + else: + cli.error("Falha na instalação. Abortando atualização...") + log.error("update", "Falha na instação das dependências") + return + + cli.i_am("Atualizando código fonte...") + sources = _map_update_sources(update_dir) + _touch_directories(sources) + + for fname, dirname in sources: + path = fname if dirname == '.' else f"{dirname}/{fname}" + copyfile(f"{update_dir}/{path}", f".update/{path}") + + # O arquivo apply_update.py é especial: ele não pode atualizar ele + # mesmo + os.system("move /Y .update\\apply_update.py .") + + cli.success("OK") + + +def _map_update_sources(update_dir): + """Mapeia o índice de uma atualização em um formato mais usável + + Parâmetros + ---------- + update_dir : str + Caminho da pasta onde os arquivos da atualização estão + + Retorno + ------- + Lista de tuplas com o nome dos arquivos (sem pasta) e o nome da + pasta onde devem ser colocados (relativo à pasta raíz do god) + """ + + files = glob.glob(f"{update_dir}/**/*.py", recursive=True) + directories = map(os.path.dirname, files) + + def strip_update_dir(dirname): + if dirname in update_dir: # Na prática, se é o diretório raíz + return '.' + return dirname.replace("\\", "/").replace(update_dir, '') + + directories = map(strip_update_dir, directories) + + return list(zip(map(os.path.basename, files), directories)) + + +def _touch_directories(sources): + """Cria os diretórios para salvar os arquivos da atualização""" + + for _, dirname in sources: + path = f".update/{dirname}" + if not os.path.isdir(path): + os.makedirs(path)