Last active
December 11, 2023 01:39
-
-
Save chascruzrm/54c8bd396cb4963c0b3e88ac5874e324 to your computer and use it in GitHub Desktop.
Programa que mueve los archivos del directorio actual a una subcarpeta que contendrá otras, cuyos tamaños no superarán a los megabytes indicados por el usuario. Es útil para separar en carpetas un montón de archivos que superen la capacidad de, por ejemplo, un DVD o Bluray.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
Autor: Roberto Marcelo Chas Cruz ([email protected]) | |
A este programa lo ofrezco de forma libre y gratuita. No me hago responsable de los | |
daños que pueda hacer su uso. | |
Programa que mueve los archivos del directorio actual a una subcarpeta que contendrá otras, | |
cuyos tamaños no superarán a los megabytes indicados por el usuario. | |
Es útil para separar en carpetas un montón de archivos que superen la capacidad de, por | |
ejemplo, un DVD o Bluray. | |
Los archivos se moverán a subcarpetas de la carpeta "PARTES_SEPARADAS" dentro del | |
directorio del programa. | |
Cada parte se almacenará en la carpeta .\PARTES_SEPARADAS\PARTE<n>, con n empezando en 1. | |
Los archivos individuales que superen el tamaño máximo indicado por el usuario, no se | |
moverán a una subcarpeta, sino que permanecerán en el directorio original. | |
Al terminar el proceso (O al presionar la tecla "F" durante la ejecución del programa), se | |
generará automáticamente un archivo llamado "restaurar.bat" que, si es ejecutado, moverá de | |
vuelta los archivos previamente separados a sus ubicaciones originales. La tecla "F" (de | |
flush) solo volcará los archivos movidos efectivamente. Esto es útil por si el proceso de | |
copia se congela por un problema de IO. La tecla "F" no finaliza la ejecución del programa | |
y el archivo de restauración creado con la tecla "F" va a quedar obsoleto a medida que se | |
muevan nuevos archivos. | |
''' | |
''' | |
Fecha: 2023-12-10 | |
Plataforma: Windows | |
Creado con Python 3.11.4 | |
Requere de módulo pywin32 v. 306: python -m pip install --upgrade pywin32 | |
''' | |
import os | |
from os import walk, path | |
import ntpath | |
import logging | |
from logging.handlers import RotatingFileHandler | |
from datetime import datetime | |
import time | |
import win32api | |
import threading | |
th_wait_for_key = None | |
abspath = path.abspath(__file__) | |
dir_script = path.dirname(abspath) | |
# Es la cantidad de bytes máxima que tendrán las carpetas separadas | |
tamano_maximo_parte = 0 | |
# Directorio donde se van a crear las carpetas particionadas | |
dir_salida = path.join(dir_script, 'PARTES_SEPARADAS') | |
# Archivo que permite revertir la ubicación de los archivos separados. | |
# fecha_hora = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') | |
# archivo_revertir = path.join(dir_script, f'restaurar_{fecha_hora}.bat') | |
archivo_revertir = path.join(dir_script, f'restaurar.bat') | |
salir = False | |
# Inicialización del logging | |
archivo_log = path.splitext(abspath)[0] + '.log' | |
handler = RotatingFileHandler(archivo_log, maxBytes=1048576*50, backupCount=1, encoding='utf8') | |
handler.setLevel(logging.DEBUG) | |
formatter = logging.Formatter( | |
"[%(asctime)s] %(levelname)s - %(message)s") | |
# "[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s") | |
handler.setFormatter(formatter) | |
logger = logging.getLogger() | |
logger.setLevel(logging.DEBUG) | |
logger.addHandler(handler) | |
# Empieza por la carpeta "parte1" | |
parte_actual = 1 | |
# Bytes acumulados en la parte actual | |
acum_actual = 0 | |
# Lista de los directorios que fueron creados | |
# en la ejecución de este programa | |
directorios_creados = [] | |
# Lista de archivos movidos a las carpetas de partes | |
# por este programa | |
archivos_movidos = [] | |
def get_folder_size(start_path): | |
print(f'Obteniendo tamaño del directorio "{start_path}"...') | |
total_size = 0 | |
with os.scandir(start_path) as d: | |
for f in d: | |
if f.is_file(): | |
fp = os.path.join(start_path, f) | |
total_size += os.path.getsize(fp) | |
return total_size | |
def get_dir_size_of_parte_actual_dir(): | |
''' | |
Este método ayuda a evitar que se acumulen archivos de más en los directorios de salida | |
cuando el proceso haya sido interrumpido. | |
''' | |
ruta_base_parte_actual = path.join(dir_salida, f'PARTE{parte_actual}') | |
if not os.path.exists(ruta_base_parte_actual): return 0 | |
return get_folder_size(ruta_base_parte_actual) | |
# Mueve un archivo a la carpeta de parte que le corresponde | |
# dentro del directorio de salida. | |
def mover(ruta_f): | |
dir_relativo = path.relpath(path.dirname(ruta_f), start = dir_script) | |
ruta_f_destino = path.join(dir_salida, f'PARTE{parte_actual}' , dir_relativo, ntpath.basename(ruta_f)) | |
dir_origen = path.join(dir_script, dir_relativo) | |
dir_destino = path.join(dir_salida, f'PARTE{parte_actual}', dir_relativo) | |
try: | |
os.makedirs(dir_destino, exist_ok=True) | |
except Exception as ex: | |
logger.error(f'** No se ha podido crear el directorio "{dir_destino}": {str(ex)}') | |
return False | |
try: | |
os.rename(ruta_f, ruta_f_destino) | |
comando = f'MKDIR "{dir_origen}"\n' | |
if comando not in directorios_creados: | |
directorios_creados.append(comando) | |
archivos_movidos.append(f'MOVE "{ruta_f_destino}" "{path.dirname(ruta_f)}"\n') | |
logger.info(f'Movido "{ruta_f}" -> "{ruta_f_destino}"') | |
print('Movido', os.path.basename(ruta_f)) | |
except Exception as ex: | |
logger.error(f'** No se ha podido mover "{ruta_f}" a "{ruta_f_destino}": {str(ex)}') | |
return False | |
return True | |
def borrar_directorios_vacios(dname): | |
if not path.exists(dname): | |
return | |
directorios_vacios = [] | |
for (dirpath, dirnames, filenames) in walk(dname, topdown=False): | |
logger.info(f'Entro en directorio "{dirpath}".') | |
if len(dirnames) == 0 and len(filenames) == 0: | |
directorios_vacios.append(dirpath) | |
logger.info(f'Está vacío.') | |
else: | |
for d in dirnames: | |
ruta_d = path.join(dirpath, d) | |
if path.exists(ruta_d): | |
logger.info(f'El directorio "{dirpath}" no está vacío.') | |
borrar_directorios_vacios(ruta_d) | |
for d in directorios_vacios: | |
try: | |
if path.exists(d): | |
os.rmdir(d) | |
logger.info(f'Se eliminó directorio vacío "{d}".') | |
except Exception as ex: | |
logger.warning(f'* No se ha podido eliminar directorio vacío "{d}": {str(ex)}') | |
def get_tamanio_parte_actual_y_get_proxima_parte(sumar_extra = 0): | |
''' | |
Esta funcion debe ser llamada por cada cambio de la variable parte_actual | |
Si el subdirectorio de la parte actual supera el tamaño máximo, se aumentará | |
parte_actual en 1 y se volverá a chequear si en el nuevo directorio de parte | |
actual hay espacio disponible. | |
''' | |
global parte_actual | |
tam_inicial = get_dir_size_of_parte_actual_dir() + sumar_extra | |
while tam_inicial > tamano_maximo_parte: | |
print(f'Parte {parte_actual} ya esta completa ({(tam_inicial-sumar_extra)//1048576} MB), pasando a la siguiente parte...') | |
time.sleep(.5) | |
parte_actual += 1 | |
tam_inicial = get_dir_size_of_parte_actual_dir() + sumar_extra | |
return tam_inicial - sumar_extra | |
def separar(dname): | |
global parte_actual | |
global acum_actual | |
# Ignorar directorio de salida | |
if dname[:len(dir_salida)].lower() == dir_salida.lower(): | |
return | |
logger.info(f'Intentando separar directorio "{dname}"...') | |
acum_actual += get_tamanio_parte_actual_y_get_proxima_parte() | |
print(f'Tamaño del directorio actual (PARTE {parte_actual}): {acum_actual // 1048576} MB') | |
time.sleep(.5) | |
for (dirpath, dirnames, filenames) in walk(dname): | |
# Ignorar directorio de salida | |
if dirpath[:len(dir_salida)].lower() == dir_salida.lower(): | |
continue | |
for f in filenames: | |
ruta_f = path.join(dirpath, f) | |
# Ignoramos archivos de la aplicación y los generados por ésta | |
if ruta_f.lower() == abspath.lower() \ | |
or ruta_f.lower() == archivo_revertir.lower() \ | |
or ruta_f.lower() == archivo_log.lower(): | |
continue | |
try: | |
tamano_f = path.getsize(ruta_f) | |
if tamano_f > tamano_maximo_parte: | |
logger.warning(f'* "{ruta_f}" supera el tamaño máximo permitido.') | |
time.sleep(.5) | |
continue | |
acum_actual += tamano_f | |
except Exception as ex: | |
logger.error(f'** No se ha podido obtener tamaño del archivo "{ruta_f}": {str(ex)}') | |
continue | |
if acum_actual > tamano_maximo_parte: | |
acum_actual = get_tamanio_parte_actual_y_get_proxima_parte(tamano_f) + tamano_f | |
mover(ruta_f) | |
def wait_for_key(): | |
global crear_archivo_reversion | |
bloquear = False | |
while not salir: | |
if not bloquear and win32api.GetAsyncKeyState(ord('F')): | |
bloquear = True | |
print('Generando archivo de reversión...') | |
crear_archivo_reversion() | |
print('Archivo creado.') | |
time.sleep(2) | |
bloquear = False | |
def crear_archivo_reversion(): | |
try: | |
with open(archivo_revertir, 'w') as fi: | |
fi.writelines(directorios_creados) | |
fi.writelines(archivos_movidos) | |
except Exception as ex: | |
logger.error(f'** No se pudo generar el archivo para revertir los cambios "{archivo_revertir}"') | |
if __name__ == '__main__': | |
th_wait_for_key = threading.Thread(target=wait_for_key) | |
th_wait_for_key.start() | |
if path.exists(archivo_revertir): | |
print(f'*** Ya existe un archivo de restauración "{os.path.basename(archivo_revertir)}".' | |
'\n*** Revise que no vaya a hacer cagada y bórrelo manualmente de ser necesario.' | |
'\n*** Luego vuelva a ejecutar este programa.') | |
os.system('pause') | |
salir = True | |
exit(1) | |
print('* Para BD Disc recomiendo 23800 MB') | |
print('* Para DVD recomiendo 4410 MB') | |
tamano_maximo_parte = input('Hola. Indique la cantidad máxima de megas que desea tenga cada parte: ') | |
try: | |
tamano_maximo_parte = abs(int(tamano_maximo_parte)) * 1048576 | |
except Exception as ex: | |
input('Cantidad no válida. Presione ENTER para salir.') | |
os.system('pause') | |
salir = True | |
exit(1) | |
confirmar = input('\n¿ESTÁ SEGURO REALMENTE? ESCRIBA SIN COMILLAS "S" Y PRESIONE ENTER PARA CONTINUAR: ').lower() | |
if confirmar != 's': | |
print('Proceso CANCELADO.') | |
os.system('pause') | |
salir = True | |
exit(0) | |
msj = 'Iniciando...' | |
print(msj) | |
logger.info(msj) | |
try: | |
msj = 'Separando...' | |
print(msj) | |
logger.info(msj) | |
separar(dir_script) | |
msj = 'Borrando directorios vacíos...' | |
print(msj) | |
logger.info(msj) | |
borrar_directorios_vacios(dir_script) | |
finally: | |
crear_archivo_reversion() | |
msj = 'Fin de ejecución.' | |
print(msj) | |
logger.info(msj) | |
salir = True | |
exit(0) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment