Skip to content

Instantly share code, notes, and snippets.

@chascruzrm
Last active December 11, 2023 01:39
Show Gist options
  • Save chascruzrm/54c8bd396cb4963c0b3e88ac5874e324 to your computer and use it in GitHub Desktop.
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.
'''
Autor: Roberto Marcelo Chas Cruz (chascruzrm2@gmail.com)
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