#!/usr/bin/python3

import shutil
import subprocess
import os
import os.path
import re
from pathlib import Path

import DiskLib
import FileSystemLib
import SystemLib
import ogGlobals
import FileLib
import CacheLib
import NetLib

## ProtocolLib.ogUcastSyntax() calls ogCreateImageSyntax()
## in ogCreateImageSyntax(), param2 may contain a pipe or may be empty
## if param2 is empty, then ogUcastSyntax():
## - does a .split()
## - accesses the third element of the resulting array (ie. does "parts[2]")
## - promptly gets an IndexError exception
##
## param2 in ogCreateImageSyntax() only contains a pipe if 'mbuffer' is installed
## therefore, a hard dependency on mbuffer is created
##
## raise an Exception at import time if mbuffer is not present
if not shutil.which ('mbuffer'):
    raise FileNotFoundError ('"mbuffer" utility must be present')

#/**
#@file    ImageLib.py
#@brief   Librería o clase Image
#@class   Image
#@brief   Funciones para creación, restauración y clonación de imágenes de sistemas.
#@warning License: GNU GPLv3+
#*/


#/**
#         ogCreateImageSyntax path_device path_filename [str_tool] [str_compressionlevel]
#@brief   Genera una cadena de texto con la instrucción para crear un fichero imagen
#@param   path_device           dispositivo Linux del sistema de archivos
#@param   path_fileneme         path absoluto del fichero imagen
#@param   [opcional] str_tool   herrmaienta de clonacion [partimage, partclone, ntfsclone]
#@param   [opcional] str_compressionlevel nivel de compresion. [0 -none-, 1-lzop-, 2-gzip]
#@return  str_command - cadena con el comando que se debe ejecutar.
#@warning Salida nula si se producen errores.
#@TODO    introducir las herramientas fsarchiver, dd
#*/ ##
#ogCreateImageSyntax /dev/sda1 /opt/opengnsys/images/prueba.img partclone lzop
#ogCreateImageSyntax /dev/sda1 /opt/opengnsys/images/prueba.img
def ogCreateImageSyntax (dev, imgfile, tool='partclone', level='gzip'):

    if 'ntfsclone' == tool:
        param1 = f'ntfsclone --force --save-image -O - {dev}'
    elif 'partimage' == tool or 'default' == tool:
        param1 = f'partimage -M -f3 -o -d -B gui=no -c -z0 --volume=0 save {dev} stdout'
    elif 'partclone' == tool:
        disk, part = DiskLib.ogDevToDisk (dev).split()
        fs = FileSystemLib.ogGetFsType (disk, part)
        param1 = {
            'EXT2':     'partclone.extfs',
            'EXT3':     'partclone.extfs',
            'EXT4':     'partclone.extfs',
            'BTRFS':    'partclone.btrfs',
            'REISERFS': 'partclone.reiserfs',
            'REISER4':  'partclone.reiser4',
            'JFS':      'partclone.jfs',
            'XFS':      'partclone.xfs',
            'F2FS':     'partclone.f2fs',
            'NILFS2':   'partclone.nilfs2',
            'NTFS':     'partclone.ntfs',
            'EXFAT':    'partclone.exfat',
            'FAT16':    'partclone.fat',
            'FAT32':    'partclone.fat',
            'HFS':      'partclone.hfsp',
            'HFSPLUS':  'partclone.hfsp',
            'UFS':      'partclone.ufs',
            'VMFS':     'partclone.vmfs',
        }.get (fs, 'partclone.imager')
        dash_c = '-c'
        if not shutil.which (param1):
            param1 = 'partclone.dd'
            # Algunas versiones de partclone.dd no tienen opción "-c".
            out = subprocess.run (['partclone.dd', '--help'], capture_output=True, text=True).stdout
            if ' -c' not in out: dash_c = ''
        param1=f'{param1} -d0 -F {dash_c} -s {dev}'
    else:
        raise Exception (f'unknown tool "{tool}"')

    param2 = '| mbuffer -q -m 40M ' if shutil.which ('mbuffer') else ' '

    param3 = {
        0:      ' > ',
        'none': ' > ',
        1:      ' | lzop > ',
        'lzop': ' | lzop > ',
        2:      ' | gzip -c > ',
        'gzip': ' | gzip -c > ',
        3:      ' | bzip -c > ',
        'bzip': ' | bzip -c > ',
        4:      ' | zstd -c > ',
        'zstd': ' | zstd -c > ',
    }.get (level, ' > ')

    #print (f'param1 ({param1}) param2 ({param2}) param3 ({param3}) imgfile ({imgfile})')
    if param1: return f'{param1} {param2} {param3} {imgfile}'


#/**
#         ogRestoreImageSyntax path_filename path_device [str_tools] [str_compressionlevel]
#@brief   Genera una cadena de texto con la instrucción para crear un fichero imagen
#@param   path_device           dispositivo Linux del sistema de archivos
#@param   path_fileneme         path absoluto del fichero imagen
#@param   [opcional] str_tools  herrmaienta de clonacion [partimage, partclone, ntfsclone]
#@param   [opcional] str_compressionlevel nivel de compresion. [0 -none-, 1-lzop-, 2-gzip]
#@return  cadena con el comando que se debe ejecutar.
#@exception OG_ERR_FORMAT    formato incorrecto.
#@warning En pruebas iniciales
#@TODO    introducir las herramientas fsarchiver, dd
#@TODO    introducir el nivel de compresion gzip
#*/ ##
#ogRestoreImageSyntax /opt/opengnsys/images/prueba.img /dev/sda1 [partclone] [lzop]
def ogRestoreImageSyntax (imgfile, part, tool=None, level=None):
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        ## original bash code is broken: 'return' is never called
        #return

    if tool is None or level is None:
        infoimg = ogGetImageInfo (imgfile)
        if not infoimg:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'no image {imgfile}')
            ## original bash code is broken: 'return' is never called
            #return
        try:
            tool, level = infoimg.split (':')[0:2]
        except:
            tool, level = '', ''
        return ogRestoreImageSyntax (imgfile, part, tool, level)

    tool = tool.lower()
    level = level.lower()
    compressor = {
        0:      ' ',
        'none': ' ',
        1:      ' lzop -dc ',
        'lzop': ' lzop -dc ',
        2:      ' gzip -dc ',
        'gzip': ' gzip -dc ',
        3:      ' bzip -dc ',
        'bzip': ' bzip -dc ',
        4:      ' zstd -dc ',
        'zstd': ' zstd -dc ',
    }.get (level, '')
    #print (f'tool ({tool}) level ({level}) compressor ({compressor})')
    if compressor == '':
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'Compressor no valid {level}')
        ## original bash code is broken: 'return' is never called
        #return

    mbuffer = '| mbuffer -q -m 40M ' if shutil.which ('mbuffer') else ' '
    #print (f'mbuffer ({mbuffer})')
    if 'ntfsclone' == tool:
        tool = f'| ntfsclone --restore-image --overwrite {part} -'
    elif 'partimage' == tool:
        tool = f'| partimage -f3 -B gui=no restore {part} stdin'
    elif 'partclone' in tool:
        # -C para que no compruebe tamaños
        tool = f'| partclone.restore -d0 -C -I -o {part}'
    elif 'dd' == tool:
        tool = f'| pv | dd conv=sync,noerror bs=1M of={part}'
    else:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'Tools imaging no valid {tool}')
        ## original bash code is broken: 'return' is never called
        #return
    #print (f'tool ({tool})')

    return f'{compressor} {imgfile} {mbuffer} {tool}'.strip()




#/**
#         ogCreateDiskImage int_ndisk str_repo path_image [str_tools] [str_compressionlevel]
#@brief   Crea una imagen (copia de seguridad) de un disco completo.
#@param   int_ndisk      nº de orden del disco
#@param   str_repo       repositorio de imágenes (remoto o caché local)
#@param   path_image     camino de la imagen (sin extensión)
#@return  (nada, por determinar)
#@note    repo = { REPO, CACHE }
#@note    Esta primera versión crea imágenes con dd comprimidas con gzip.
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_LOCKED    particion bloqueada por otra operación.
#@exception OG_ERR_IMAGE     error al crear la imagen del sistema.
#@warning En pruebas iniciales
#@todo    Gestión de bloqueos de disco
#@todo    Comprobar si debe desmontarse la caché local
#@todo    Comprobar que no se crea la imagen en el propio disco
#*/ ##


#/**
#         ogCreateImage int_ndisk int_npartition str_repo path_image [str_tools] [str_compressionlevel]
#@brief   Crea una imagen a partir de una partición.
#@param   int_ndisk      nº de orden del disco
#@param   int_npartition nº de orden de la partición
#@param   str_repo       repositorio de imágenes (remoto o caché local)
#@param   path_image     camino de la imagen (sin extensión)
#@param   [opcional] str_tools  herrmaienta de clonacion [partimage, partclone, ntfsclone]
#@param   [opcional] str_compressionlevel nivel de compresion. [0 -none-, 1-lzop-, 2-gzip]
#@return  (nada, por determinar)
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_PARTITION partición no accesible o no soportada.
#@exception OG_ERR_LOCKED    particion bloqueada por otra operación.
#@exception OG_ERR_IMAGE     error al crear la imagen del sistema.
#@todo    Comprobaciones, control de errores, definir parámetros, etc.
#*/ ##
def ogCreateImage (disk, par, container, imgfile, tool='partclone', level='gzip'):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART: return None

    if FileSystemLib.ogIsLocked (disk, par):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_LOCKED, f'{ogGlobals.lang.MSG_ERR_LOCKED} {disk}, {par}')
        return None

    imgtype = 'img'           # Extensión genérica de imágenes.
    imgdir = FileLib.ogGetParentPath (src=container, file=imgfile)
    if not imgdir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f'{container} {imgfile}')
        return None

    bn = os.path.basename (imgfile)
    IMGFILE = f'{imgdir}/{bn}.{imgtype}'
    if ogIsImageLocked (container=None, imgfile=IMGFILE):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_LOCKED, f'{ogGlobals.lang.MSG_IMAGE} {container}, {imgfile}')
        return None

# Generar la instruccion a ejecutar antes de aplicar los bloqueos.
    program = ogCreateImageSyntax (PART, IMGFILE, tool=tool, level=level)
# Desmontar partición, bloquear partición e imagen.
    FileSystemLib.ogUnmount (disk, par)
    if not FileSystemLib.ogLock (disk, par):
        return None
    if not ogLockImage (container, f'{imgfile}.{imgtype}'):
        return None

# Crear Imagen.
    #trap
    p = subprocess.run (program, shell=True, check=True)
    errcode = p.returncode
    if 0 == errcode:        ## success
        i = ogGetImageInfo (IMGFILE)
        h = NetLib.ogGetHostname()
        with open (f'{IMGFILE}.info', 'w') as fd:
            fd.write (f'{i}:{h}\n')
    else:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f'{disk} {par} {IMGFILE}')
        if os.path.exists (IMGFILE):
            os.unlink (IMGFILE)

    FileSystemLib.ogUnlock (disk, par)
    ogUnlockImage (container, f'{imgfile}.{imgtype}')
    return not errcode      ## reverse to indicate success


#/**
#         ogCreateMbrImage int_ndisk str_repo path_image
#@brief   Crea una imagen a partir del sector de arranque de un disco.
#@param   int_ndisk    nº de orden del disco
#@param   str_repo     repositorio de imágenes (remoto o caché local)
#@param   path_image   camino de la imagen (sin extensión)
#@return  (nada, por determinar)
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_IMAGE     error al crear la imagen del sistema.
#*/ ##


#/**
#         ogCreateBootLoaderImage int_ndisk str_repo path_image
#@brief   Crea una imagen del boot loader a partir del sector de arranque de un disco.
#@param   int_ndisk    nº de orden del disco
#@param   str_repo     repositorio de imágenes (remoto o caché local)
#@param   path_image   camino de la imagen (sin extensión)
#@return  (nada, por determinar)
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_IMAGE     error al crear la imagen del sistema.
#*/ ##

#/**
#         ogGetSizeParameters int_num_disk  int_num_part str_repo [monolit|sync|diff]
#@brief   Devuelve el tamaño de los datos de un sistema de ficheros, el espacio necesario para la imagen y si cabe en el repositorio elegido.
#@param   int_disk     numero de disco
#@param	  int_part     numero de particion
#@param   str_repo     repositorio de imágenes   { REPO, CACHE }
#@param   str_imageName Nombre de la imagen
#@param   str_imageType Tipo de imagen: monolit (por defecto), sync o diff. (parametro opcional)
#@return  SIZEDATA SIZEREQUIRED SIZEFREE ISENOUGHSPACE
#@note    si str_imageType= diff necesario /tmp/ogimg.info, que es creado por ogCreateInfoImage.
#@note    para el tamaño de la imagen no sigue enlaces simbólicos.
#@exception OG_ERR_FORMAT    formato incorrecto.
#*/ ##
#SIZEDATA, SIZEREQUIRED, SIZEFREE, ISENOUGHSPACE = ogGetSizeParameters (1, 1, 'REPO', 'myimg')
def ogGetSizeParameters (disk, par, repo, imgname, imgtype=None):
    repo = repo.upper()
    ## imgtype se soporta como parametro pero se ignora

    mntdir = FileSystemLib.ogMount (disk, par)
    if not mntdir:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, f'{disk} {par}')
        return None

    #if [ "$IMGTYPE" == "_DIFF_" ]; then cosas
    #else:
    sizedata = None
    df_out = subprocess.run (['df', '-k'], capture_output=True, text=True).stdout
    for l in df_out.splitlines():
        if (re.search (f'{mntdir}$', l)):
            sizedata = int (l.split()[2])
            break
    if sizedata is None:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, 'sizedata is None')
        return None

    #if [ "$IMGTYPE" == "_SYNC_" -o "$IMGTYPE" == "_DIFF_" ]; then cosas
    #else:
    factorgzip=55/100
    factorlzop=65/100
    sizerequired = sizedata * factorlzop

    #Comprobar espacio libre en el contenedor.
    sizefree = None
    if 'CACHE' == repo:
        CACHEPART = CacheLib.ogFindCache()
        if not CACHEPART:
            SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTCACHE, '')
            return None
        cache_disk, cache_par = CACHEPART.split()
        sizefree = FileSystemLib.ogGetFreeSize (cache_disk, cache_par)
    if 'REPO'  == repo:
        df_out = subprocess.run (['df', '-k'], capture_output=True, text=True).stdout
        for l in df_out.splitlines():
            if (re.search (f'{ogGlobals.OGIMG}', l)):
                sizefree = int (l.split()[3])
                break
    if sizefree is None:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_GENERIC, 'sizefree is None')
        return None

# Comprobamos si existe una imagen con el mismo nombre en $REPO
# En sincronizadas restamos tamaño de la imagen y en monoloticas de la .ant
    #case "${IMGTYPE}" in blah blah
    #*)
    imgext = 'img.ant'

    imgdir = FileLib.ogGetParentPath (src=repo, file=f'/{imgname}')
    bn = os.path.basename (f'/{imgname}')
    imgfile = FileLib.ogGetPath (file=f'{imgdir}/{bn}.{imgext}')

    if not imgfile:
        imgsize = 0
    else:
        ls_out = subprocess.run (['ls', '-s', imgfile], capture_output=True, text=True).stdout
        imgsize = int (ls_out.split()[0])

    sizefree = sizefree + imgsize

    if sizerequired < sizefree:
        isenoughspace = True
    else:
        isenoughspace = False

    return sizedata, sizerequired, sizefree, isenoughspace

#/**
#         ogIsImageLocked [str_repo] path_image
#@brief   Comprueba si una imagen está bloqueada para uso exclusivo.
#@param   str_repo     repositorio de imágenes (opcional)
#@param   path_image   camino de la imagen (sin extensión)
#@return  Código de salida: 0 - bloqueado, 1 - sin bloquear o error.
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#*/ ##
#ogIsImageLocked ('/opt/opengnsys/images/aula1/win7.img')
#ogIsImageLocked ('REPO', '/aula1/win7.img')
def ogIsImageLocked (container=None, imgfile=None):
    if container and imgfile:
        p = FileLib.ogGetPath (src=container, file=f'{imgfile}.lock')
    elif imgfile:
        p = FileLib.ogGetPath (file=f'{imgfile}.lock')
    else:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, f'{container} {imgfile}')
        return
    return os.path.exists (p)


#/**
#         ogLockImage [str_repo] path_image
#@brief   Bloquea una imagen para uso exclusivo.
#@param   str_repo     repositorio de imágenes (opcional)
#@param   path_image   camino de la imagen (sin extensión)
#@return  Nada.
#@note    Se genera un fichero con extensión .lock
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#*/ ##
def ogLockImage (container=None, imgfile=None):
    if not imgfile:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_FORMAT, '')
        return None

    if container:
        imgdir = FileLib.ogGetParentPath (src=container, file=imgfile)
    else:
        imgdir = FileLib.ogGetParentPath (file=imgfile)

    try:
        bn = os.path.basename (imgfile)
        open (f'{imgdir}/{bn}.lock', 'w').close()
        return True
    except:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTWRITE, f'{container} {imgfile}')
        return None


#/**
#         ogRestoreDiskImage str_repo path_image int_npartition
#@brief   Restaura (recupera) una imagen de un disco completo.
#@param   str_repo       repositorio de imágenes o caché local
#@param   path_image     camino de la imagen
#@param   int_ndisk      nº de orden del disco
#@return  (por determinar)
#@warning Primera versión en pruebas
#@todo    Gestionar bloqueos de disco
#@todo    Comprobar que no se intenta restaurar de la caché sobre el mismo disco
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero de imagen o partición no detectados.
#@exception OG_ERR_LOCKED    partición bloqueada por otra operación.
#@exception OG_ERR_IMAGE     error al restaurar la imagen del sistema.
#@exception OG_ERR_IMGSIZEPARTITION  Tamaño de la particion es menor al tamaño de la imagen.
#*/ ##


#/**
#         ogRestoreImage str_repo path_image int_ndisk int_npartition
#@brief   Restaura una imagen de sistema de archivos en una partición.
#@param   str_repo       repositorio de imágenes o caché local
#@param   path_image     camino de la imagen
#@param   int_ndisk      nº de orden del disco
#@param   int_npartition nº de orden de la partición
#@return  (por determinar)
#@exception OG_ERR_FORMAT   1 formato incorrecto.
#@exception OG_ERR_NOTFOUND  2 fichero de imagen o partición no detectados.
#@exception OG_ERR_PARTITION 3	# Error en partición de disco.
#@exception OG_ERR_LOCKED    4 partición bloqueada por otra operación.
#@exception OG_ERR_IMAGE    5 error al restaurar la imagen del sistema.
#@exception OG_ERR_IMGSIZEPARTITION  30 Tamaño de la particion es menor al tamaño de la imagen.
#@todo    Comprobar incongruencias partición-imagen, control de errores, definir parámetros, caché/repositorio, etc.
#*/ ##
#ogRestoreImage ('REPO', '/aula1/win7', '1', '1')
def ogRestoreImage (repo, imgpath, disk, par):
    PART = DiskLib.ogDiskToDev (disk, par)
    if not PART:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f' {disk} {par}')
        return None

    imgtype = 'img'
    imgfile = FileLib.ogGetPath (repo, f'{imgpath}.{imgtype}')
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, f' {disk} {par}')
        return None

    if not ogGetImageInfo (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f' {repo} {imgpath}')
        return None

    # Error si la imagen no cabe en la particion.
    imgsize = ogGetImageSize (repo, imgpath)
    if not imgsize:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f' {repo} {imgpath}')
        return None
    if not FileSystemLib.ogMount (disk, par):
        FileSystemLib.ogFormat (disk, par)
    partsize = DiskLib.ogGetPartitionSize (disk, par)
    if float (imgsize) > float (partsize):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMGSIZEPARTITION, f'  {partsize} < {imgsize}')
        return None

    if ogIsImageLocked (container=None, imgfile=imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_LOCKED, f'{ogGlobals.lang.MSG_IMAGE} {repo}, {imgpath}.{imgtype}')
        return None

    if FileSystemLib.ogIsLocked (disk, par):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_LOCKED, f'{ogGlobals.lang.MSG_PARTITION} {disk}, {par}')
        return None

    program = ogRestoreImageSyntax (imgfile, PART)

    if not FileSystemLib.ogUnmount (disk, par):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_PARTITION, f' {disk} {par}')
        return None

    if not FileSystemLib.ogLock (disk, par):
        print (f'not FileSystemLib.ogLock()')
        return None

    rc = None
    try:
        p = subprocess.run (program, shell=True, text=True)
        rc = not p.returncode
        if not rc:
            SystemLib.ogRaiseError ([], ogGlobalsOG_ERR_IMAGE, f'{imgfile}, {disk}, {par}')
    except:
        pass
    finally:
        FileSystemLib.ogUnlock (disk, par)

    return rc



#/**
#         ogRestoreMbrImage str_repo path_image int_ndisk
#@brief   Restaura la imagen del sector de arranque de un disco.
#@param   str_repo     repositorio de imágenes o caché local
#@param   path_image   camino de la imagen
#@param   int_ndisk    nº de orden del disco
#@return  (por determinar)
#@exception OG_ERR_FORMAT   formato incorrecto.
#@exception OG_ERR_NOTFOUND fichero de imagen o partición no detectados.
#@exception OG_ERR_IMAGE    error al restaurar la imagen del sistema.
#*/ ##


#/**
#         ogRestoreBootLoaderImage str_repo path_image int_ndisk
#@brief   Restaura la imagen del boot loader del sector de arranque de un disco.
#@param   str_repo     repositorio de imágenes o caché local
#@param   path_image   camino de la imagen
#@param   int_ndisk    nº de orden del disco
#@return  (por determinar)
#@exception OG_ERR_FORMAT   formato incorrecto.
#@exception OG_ERR_NOTFOUND fichero de imagen o partición no detectados.
#@exception OG_ERR_IMAGE    error al restaurar la imagen del sistema.
#*/ ##

#/**
#         ogUnlockImage [str_repo] path_image
#@brief   Desbloquea una imagen con uso exclusivo.
#@param   str_repo     repositorio de imágenes (opcional)
#@param   path_image   camino de la imagen (sin extensión)
#@return  Nada.
#@note    repo = { REPO, CACHE }
#@note    Se elimina el fichero de bloqueo con extensión .lock
#@exception OG_ERR_FORMAT    formato incorrecto.
#*/ ##
#ogUnlockImage REPO /cucu.img
def ogUnlockImage (container=None, imgfile=None):
    f = f'{imgfile}.lock'
    if container:
        p = FileLib.ogGetPath (src=container, file=f)
    else:
        p = FileLib.ogGetPath (file=f)

    if os.path.exists (p):
        os.unlink (p)


#/**
#         ogGetImageInfo filename
#@brief   muestra información sobre la imagen monolitica.
#@param 1   filename           path absoluto del fichero imagen
#@return  cadena compuesta por clonacion:compresor:sistemaarchivos:tamañoKB
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND   fichero no encontrado.
#@exception OG_ERR_IMAGE     "Image format is not valid $IMGFILE"
#@warning En pruebas iniciales
#@TODO    Definir sintaxis de salida (herramienta y compresor en minuscula)
#@TODO       Arreglar loop para ntfsclone
#@TODO    insertar parametros entrada tipo OG
#*/ ##
#ogGetImageInfo /opt/opengnsys/images/prueba.img  ==>  PARTCLONE:LZOP:NTFS:5642158"
def ogGetImageInfo (imgfile):
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        return

    imgdetect = False
    filehead = f'/tmp/{os.path.basename (imgfile)}.infohead'
    compressor = subprocess.run (['file', imgfile], capture_output=True, text=True).stdout.split()[1]
    if 'Zstandard' == compressor: compressor = 'zstd'
    if compressor not in ['gzip', 'lzop', 'zstd']:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f'Image format is not valid {imgfile}')
        return

    ## original bash idiom is: $($COMPRESSOR -dc $IMGFILE 2>/dev/null | head -n 40 > $FILEHEAD) || ogRaiseError
    ## the purpose of which I can't fully comprehend
    #print (f'shelling out "{compressor} -dc {imgfile} |head -n 40 > {filehead}"')
    if subprocess.run (f'{compressor} -dc {imgfile} |head -n 40 > {filehead}', shell=True).returncode:      ## if True, then the compressor failed
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f'Image format is not valid {imgfile}')
        return

    tools = fs = size = None

    if False == imgdetect:
        lc_all = os.getenv ('LC_ALL')
        os.environ['LC_ALL'] = 'C'
        partclone_info = subprocess.run (['partclone.info', filehead], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
        #partclone_info = subprocess.run (['cat', '/tmp/foo-partclone'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
        ## sacado de un email de alberto garcía uma.es
        #Partclone v0.3.13 http://partclone.org
        #Unknown mode
        #File system:  NTFS
        #Device size:   29.2 GB = 7138019 Blocks
        #Space in use:  26.6 GB = 6485355 Blocks
        #Free Space:     2.7 GB = 652664 Blocks
        #Block size:   4096 Byte
        #
        #image format:    0002
        #created on a:    64 bits platform
        #with partclone:  v0.3.13
        #bitmap mode:     BIT
        #checksum algo:   CRC32
        #checksum size:   4
        #blocks/checksum: 256
        if lc_all is not None:
            os.environ["LC_ALL"] = lc_all
        else:
            if "LC_ALL" in os.environ: del os.environ["LC_ALL"]

        if 'size' in partclone_info:
            tools = 'PARTCLONE'
            m = re.search (r'File system *: *(\S+)', partclone_info)
            fs = m.group(1) if m else ''

            sizefactor = 1000000 if 'GB' in partclone_info else 1024
            m = re.search (r'Device size *: *(\S+)', partclone_info)
            size = float (m.group(1)) if m else 0
            size = int (size * sizefactor)

            ## why is this?
            #if fs in ['HFS', 'HFSPLUS', 'FAT32']:
            #    #FSPLUS=$(echo $PARTCLONEINFO | awk '{gsub(/\: /,"\n"); print toupper($9);}')
            #    fsplus = 'PLUS'
            #    if fsplus: #        fs += fsplus     ## 'HFS' -> 'HFSPLUS'

            imgdetect = True

    if False == imgdetect and not os.path.exists ('/dev/loop2'):
        filehead_contents = Path (filehead).read_bytes()
        if b'ntfsclone-image' in filehead_contents:
            #print (f'shelling out "cat {filenead} | ntfsclone --restore --overwrite /dev/loop2 - 2>&1"')
            ntfscloneinfo = subprocess.run (f'cat {filenead} | ntfsclone --restore --overwrite /dev/loop2 - 2>&1', shell=True, capture_output=True, text=True).stdout
            #ntfscloneinfo = subprocess.run (['cat', '/tmp/foo-ntfsclone'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
            ## sacado de claude 3 haiku
            #ntfsclone v2023.4.0 (libntfs-3g)
            #NTFS volume version: 3.1
            #Cluster size: 4096 bytes
            #Image volume size: 104857600 bytes (105 MB)
            #Space in use: 52428800 bytes (52 MB)
            #Reading and restoring NTFS...
            #100.00 percent completed
            #Syncing ...
            #Successfully cloned image to device '/dev/loop2'.
        else:
            ntfscloneinfo = ''

        if 'ntfsclone' in ntfscloneinfo:
            tools = 'NTFSCLONE'
            m = re.search (r'Image volume size *: *(\S+)', ntfscloneinfo)
            size = float (m.group(1))/1000 if m else 0
            fs = 'NTFS'
            imgdetect = True

    if False == imgdetect:
        partimageinfo = subprocess.run (['partimage', '-B', 'gui=no', 'imginfo', filehead], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
        #partimageinfo = subprocess.run (['cat', '/tmp/foo-partimage'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).stdout
        ## sacado de un email de alberto garcía uma.es
        #Volume number:.........0
        #Volume size:...........1,27 MiB
        #Compression level: ....0 -> ninguno
        #Identificator:.........12442509728668372730=ACACACACCB9ECEFA
        #Filesystem:............ntfs
        #Description:...........Sin descripcion
        #Original device:......./dev/nvme0n1p2
        #Original filepath:.... stdout
        #Flags:.................0: Bandera sin activar
        #Creation date:.........Mon Nov 11 21:00:22 2024
        #Partition size:........476,84 GiB
        #Hostname:..............ING-LTR-083
        #Compatible Version:....0.6.1
        #Encryption algorithm:..0 -> ninguno
        #MBR saved count:.......0
        partimageinfo = re.sub (r':\s*\.+', ' : ', partimageinfo)
        if 'Partition' in partimageinfo:
            tools = 'PARTIMAGE'
            m = re.search (r'Filesystem *: *(\S+)', partimageinfo)
            fs = m.group(1).upper() if m else ''

            m = re.search (r'Partition size *: *(\S+)', partimageinfo)
            size = m.group(1) if m else ''
            size = re.sub (r' [MGT]i?B$', '', size)
            size = float (size.replace (',', '.'))
            size = int (size*1024*1024)

            imgdetect = True
        if 'boot sector' in subprocess.run (['file', filehead], capture_output=True, text=True).stdout:
            tools = 'partclone.dd'
            fs = ''
            size = 0
            imgdetect = True

    if not tools or not compressor or False == imgdetect:
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_IMAGE, f'Image format is not valid {imgfile}')
        return

    compressor = compressor.upper()
    return ':'.join ([tools, compressor, fs, str (size)])

#/**
#         ogGetImageProgram str_REPO str_imagen
#@brief   muestra información sobre la imagen monolitica.
#@see     ogGetImageInfo
#@param 1   REPO o CACHE       contenedor de la imagen
#@param 2   filename           nombre de la imagen sin extension
#@return  nombre del programa usado para generar la imagen
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND   fichero no encontrado.
#@note     ogGetImageProgram REPO imagenA -> partclone
#*/ ##

#ogGetImageProgram ('REPO', 'prueba')  ==>  'PARTCLONE'
def ogGetImageProgram (container, filename):
    imgfile = FileLib.ogGetPath (container, f'{filename}.img')
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        return None
    i = ogGetImageInfo (imgfile)
    return i.split (':')[0]

#/**
#         ogGetImageCompressor str_REPO str_imagen
#@brief   muestra información sobre la imagen monolitica.
#@see     ogGetImageInfo
#@param 1   REPO o CACHE       contenedor de la imagen
#@param 2   filename           nombre de la imagen sin extension
#@return    tipo de compresión usada al generar la imagen
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND   fichero no encontrado.
#@note     ogGetImageCompressor REPO imagenA -> lzop
#*/ ##

#ogGetImageCompressor ('REPO', 'prueba')  ==>  'LZOP'
def ogGetImageCompressor (container, filename):
    imgfile = FileLib.ogGetPath (container, f'{filename}.img')
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        return None
    i = ogGetImageInfo (imgfile)
    return i.split (':')[1]


#/**
#         ogGetImageType str_REPO str_imagen
#@brief   muestra información sobre el sistema de archivos de imagen monolitica.
#@see     ogGetImageInfo
#@param 1   REPO o CACHE       contenedor de la imagen
#@param 2   filename           nombre de la imagen sin extension
#@return    tipo de compresión usada al generar la imagen
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND   fichero no encontrado.
#@note     ogGetImageType REPO imagenA -> NTFS
#*/ ##
#ogGetImageType ('REPO',  'imgprueba')  ==>  'NTFS'
#ogGetImageType ('CACHE', 'testimg')    ==>  'EXTFS'
def ogGetImageType (repo, imgname):
    imgfile = FileLib.ogGetPath (src=repo, file=f'{imgname}.img')
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        return None

    i = ogGetImageInfo (imgfile)
    return i.split (':')[2]



#/**
#         ogGetImageSize str_REPO str_imagen
#@brief   muestra información sobre el tamaño (KB) del sistema de archivos de imagen monolitica.
#@see     ogGetImageInfo
#@param 1   REPO o CACHE       contenedor de la imagen
#@param 2   filename           nombre de la imagen sin extension
#@return    tipo de compresión usada al generar la imagen
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND   fichero no encontrado.
#@note     ogGetImagesize REPO imagenA -> 56432234  > Kb
#*/ ##
#ogGetImageSize ('REPO', 'prueba')    ==>  '5642158'
def ogGetImageSize (repo, imgname):
    imgfile = FileLib.ogGetPath (src=repo, file=f'{imgname}.img')
    if not os.path.exists (imgfile):
        SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTFOUND, imgfile)
        return None

    i = ogGetImageInfo (imgfile)
    return i.split (':')[3]


#/**
#         ogCreateGptImage int_ndisk str_repo path_image
#@brief   Crea una imagen de la tabla de particiones GPT de un disco.
#@param   int_ndisk    nº de orden del disco
#@param   str_repo     repositorio de imágenes (remoto o caché local)
#@param   path_image   camino de la imagen (sin extensión)
#@return  (nada, por determinar)
#@note    repo = { REPO, CACHE }
#@exception OG_ERR_FORMAT    formato incorrecto.
#@exception OG_ERR_NOTFOUND  fichero o dispositivo no encontrado.
#@exception OG_ERR_IMAGE     error al crear la imagen del sistema.
#*/ ##

#/**
#         ogRestoreGptImage str_repo path_image int_ndisk
#@brief   Restaura la imagen de la tabla de particiones GPT de un disco.
#@param   str_repo     repositorio de imágenes o caché local
#@param   path_image   camino de la imagen
#@param   int_ndisk    nº de orden del disco
#@return  (por determinar)
#@exception OG_ERR_FORMAT   formato incorrecto.
#@exception OG_ERR_NOTFOUND fichero de imagen o partición no detectados.
#@exception OG_ERR_IMAGE    error al restaurar la imagen del sistema.
#*/ ##
