440 lines
14 KiB
Python
440 lines
14 KiB
Python
|
import os
|
||
|
import json
|
||
|
from datetime import datetime
|
||
|
import pytz
|
||
|
import mimetypes
|
||
|
from werkzeug.utils import secure_filename
|
||
|
from flask import current_app
|
||
|
|
||
|
from utils.file_utils import (
|
||
|
load_json_file, save_json_file, ensure_dir_exists,
|
||
|
get_next_id, format_document_directory_name,
|
||
|
format_version_filename
|
||
|
)
|
||
|
from utils.security import calculate_checksum, check_file_type
|
||
|
from services.project_service import find_project_directory
|
||
|
|
||
|
def get_allowed_filetypes():
|
||
|
"""
|
||
|
Obtener los tipos de archivo permitidos.
|
||
|
|
||
|
Returns:
|
||
|
dict: Diccionario de tipos de archivo
|
||
|
"""
|
||
|
storage_path = current_app.config['STORAGE_PATH']
|
||
|
filetypes_file = os.path.join(storage_path, 'filetypes', 'filetypes.json')
|
||
|
|
||
|
return load_json_file(filetypes_file, {})
|
||
|
|
||
|
def add_document(project_id, document_data, file, creator_username):
|
||
|
"""
|
||
|
Añadir un nuevo documento a un proyecto.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_data (dict): Datos del documento
|
||
|
file: Objeto de archivo (de Flask)
|
||
|
creator_username (str): Usuario que crea el documento
|
||
|
|
||
|
Returns:
|
||
|
tuple: (success, message, document_id)
|
||
|
"""
|
||
|
# Buscar directorio del proyecto
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return False, f"No se encontró el proyecto con ID {project_id}.", None
|
||
|
|
||
|
# Validar datos obligatorios
|
||
|
if 'nombre' not in document_data or not document_data['nombre']:
|
||
|
return False, "El nombre del documento es obligatorio.", None
|
||
|
|
||
|
# Validar tipo de archivo
|
||
|
allowed_filetypes = get_allowed_filetypes()
|
||
|
filename = secure_filename(file.filename)
|
||
|
extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
||
|
|
||
|
if extension not in allowed_filetypes:
|
||
|
return False, f"Tipo de archivo no permitido: {extension}", None
|
||
|
|
||
|
# Verificar MIME type
|
||
|
if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]):
|
||
|
return False, "El tipo de archivo no coincide con su extensión.", None
|
||
|
|
||
|
# Obtener siguiente ID de documento
|
||
|
document_id = get_next_id('document')
|
||
|
|
||
|
# Preparar directorio del documento
|
||
|
documents_dir = os.path.join(project_dir, 'documents')
|
||
|
dir_name = format_document_directory_name(document_id, document_data['nombre'])
|
||
|
document_dir = os.path.join(documents_dir, dir_name)
|
||
|
|
||
|
# Verificar si ya existe
|
||
|
if os.path.exists(document_dir):
|
||
|
return False, "Ya existe un documento con ese nombre en este proyecto.", None
|
||
|
|
||
|
# Crear directorio
|
||
|
ensure_dir_exists(document_dir)
|
||
|
|
||
|
# Preparar primera versión
|
||
|
version = 1
|
||
|
version_filename = format_version_filename(version, document_data['nombre'], extension)
|
||
|
version_path = os.path.join(document_dir, version_filename)
|
||
|
|
||
|
# Guardar archivo
|
||
|
file.seek(0)
|
||
|
file.save(version_path)
|
||
|
|
||
|
# Calcular checksum
|
||
|
checksum = calculate_checksum(version_path)
|
||
|
|
||
|
# Obtener tamaño del archivo
|
||
|
file_size = os.path.getsize(version_path)
|
||
|
|
||
|
# Preparar metadatos del documento
|
||
|
document_meta = {
|
||
|
'document_id': f"{document_id:03d}_{document_data['nombre'].lower().replace(' ', '_')}",
|
||
|
'original_filename': filename,
|
||
|
'versions': [
|
||
|
{
|
||
|
'version': version,
|
||
|
'filename': version_filename,
|
||
|
'created_at': datetime.now(pytz.UTC).isoformat(),
|
||
|
'created_by': creator_username,
|
||
|
'description': document_data.get('description', 'Versión inicial'),
|
||
|
'file_size': file_size,
|
||
|
'mime_type': allowed_filetypes[extension]['mime_type'],
|
||
|
'checksum': checksum,
|
||
|
'downloads': []
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
|
||
|
# Guardar metadatos
|
||
|
meta_file = os.path.join(document_dir, 'meta.json')
|
||
|
save_json_file(meta_file, document_meta)
|
||
|
|
||
|
return True, "Documento añadido correctamente.", document_id
|
||
|
|
||
|
def add_version(project_id, document_id, version_data, file, creator_username):
|
||
|
"""
|
||
|
Añadir una nueva versión a un documento existente.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
version_data (dict): Datos de la versión
|
||
|
file: Objeto de archivo (de Flask)
|
||
|
creator_username (str): Usuario que crea la versión
|
||
|
|
||
|
Returns:
|
||
|
tuple: (success, message, version_number)
|
||
|
"""
|
||
|
# Buscar directorio del proyecto
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return False, f"No se encontró el proyecto con ID {project_id}.", None
|
||
|
|
||
|
# Buscar documento
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
|
||
|
if not document_dir:
|
||
|
return False, f"No se encontró el documento con ID {document_id}.", None
|
||
|
|
||
|
# Cargar metadatos del documento
|
||
|
meta_file = os.path.join(document_dir, 'meta.json')
|
||
|
document_meta = load_json_file(meta_file)
|
||
|
|
||
|
if not document_meta:
|
||
|
return False, "Error al cargar metadatos del documento.", None
|
||
|
|
||
|
# Validar tipo de archivo
|
||
|
allowed_filetypes = get_allowed_filetypes()
|
||
|
filename = secure_filename(file.filename)
|
||
|
extension = filename.rsplit('.', 1)[1].lower() if '.' in filename else ''
|
||
|
|
||
|
if extension not in allowed_filetypes:
|
||
|
return False, f"Tipo de archivo no permitido: {extension}", None
|
||
|
|
||
|
# Verificar MIME type
|
||
|
if not check_file_type(file.stream, [allowed_filetypes[extension]['mime_type']]):
|
||
|
return False, "El tipo de archivo no coincide con su extensión.", None
|
||
|
|
||
|
# Determinar número de versión
|
||
|
last_version = max([v['version'] for v in document_meta['versions']])
|
||
|
new_version = last_version + 1
|
||
|
|
||
|
# Preparar nombre de archivo
|
||
|
doc_name = document_meta['document_id'].split('_', 1)[1] if '_' in document_meta['document_id'] else 'document'
|
||
|
version_filename = format_version_filename(new_version, doc_name, extension)
|
||
|
version_path = os.path.join(document_dir, version_filename)
|
||
|
|
||
|
# Guardar archivo
|
||
|
file.seek(0)
|
||
|
file.save(version_path)
|
||
|
|
||
|
# Calcular checksum
|
||
|
checksum = calculate_checksum(version_path)
|
||
|
|
||
|
# Obtener tamaño del archivo
|
||
|
file_size = os.path.getsize(version_path)
|
||
|
|
||
|
# Preparar metadatos de la versión
|
||
|
version_meta = {
|
||
|
'version': new_version,
|
||
|
'filename': version_filename,
|
||
|
'created_at': datetime.now(pytz.UTC).isoformat(),
|
||
|
'created_by': creator_username,
|
||
|
'description': version_data.get('description', f'Versión {new_version}'),
|
||
|
'file_size': file_size,
|
||
|
'mime_type': allowed_filetypes[extension]['mime_type'],
|
||
|
'checksum': checksum,
|
||
|
'downloads': []
|
||
|
}
|
||
|
|
||
|
# Añadir versión a metadatos
|
||
|
document_meta['versions'].append(version_meta)
|
||
|
|
||
|
# Guardar metadatos actualizados
|
||
|
save_json_file(meta_file, document_meta)
|
||
|
|
||
|
return True, "Nueva versión añadida correctamente.", new_version
|
||
|
|
||
|
def get_document(project_id, document_id):
|
||
|
"""
|
||
|
Obtener información de un documento.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
|
||
|
Returns:
|
||
|
dict: Datos del documento o None si no existe
|
||
|
"""
|
||
|
# Buscar directorio del proyecto
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return None
|
||
|
|
||
|
# Buscar documento
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
|
||
|
if not document_dir:
|
||
|
return None
|
||
|
|
||
|
# Cargar metadatos
|
||
|
meta_file = os.path.join(document_dir, 'meta.json')
|
||
|
document_meta = load_json_file(meta_file)
|
||
|
|
||
|
# Agregar ruta del directorio
|
||
|
if document_meta:
|
||
|
document_meta['directory'] = os.path.basename(document_dir)
|
||
|
|
||
|
return document_meta
|
||
|
|
||
|
def get_document_version(project_id, document_id, version):
|
||
|
"""
|
||
|
Obtener información de una versión específica de un documento.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
version (int): Número de versión
|
||
|
|
||
|
Returns:
|
||
|
tuple: (dict, str) - (Metadatos de la versión, ruta al archivo)
|
||
|
"""
|
||
|
document = get_document(project_id, document_id)
|
||
|
|
||
|
if not document:
|
||
|
return None, None
|
||
|
|
||
|
# Buscar versión específica
|
||
|
version_meta = None
|
||
|
for v in document['versions']:
|
||
|
if v['version'] == int(version):
|
||
|
version_meta = v
|
||
|
break
|
||
|
|
||
|
if not version_meta:
|
||
|
return None, None
|
||
|
|
||
|
# Preparar ruta al archivo
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
file_path = os.path.join(document_dir, version_meta['filename'])
|
||
|
|
||
|
return version_meta, file_path
|
||
|
|
||
|
def get_latest_version(project_id, document_id):
|
||
|
"""
|
||
|
Obtener la última versión de un documento.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
|
||
|
Returns:
|
||
|
tuple: (dict, str) - (Metadatos de la versión, ruta al archivo)
|
||
|
"""
|
||
|
document = get_document(project_id, document_id)
|
||
|
|
||
|
if not document or not document['versions']:
|
||
|
return None, None
|
||
|
|
||
|
# Encontrar la versión más reciente
|
||
|
latest_version = max(document['versions'], key=lambda v: v['version'])
|
||
|
|
||
|
# Preparar ruta al archivo
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
file_path = os.path.join(document_dir, latest_version['filename'])
|
||
|
|
||
|
return latest_version, file_path
|
||
|
|
||
|
def register_download(project_id, document_id, version, username):
|
||
|
"""
|
||
|
Registrar una descarga de documento.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
version (int): Número de versión
|
||
|
username (str): Usuario que descarga
|
||
|
|
||
|
Returns:
|
||
|
bool: True si se registró correctamente, False en caso contrario
|
||
|
"""
|
||
|
# Buscar documento
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return False
|
||
|
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
|
||
|
if not document_dir:
|
||
|
return False
|
||
|
|
||
|
# Cargar metadatos
|
||
|
meta_file = os.path.join(document_dir, 'meta.json')
|
||
|
document_meta = load_json_file(meta_file)
|
||
|
|
||
|
if not document_meta:
|
||
|
return False
|
||
|
|
||
|
# Buscar versión
|
||
|
for v in document_meta['versions']:
|
||
|
if v['version'] == int(version):
|
||
|
# Registrar descarga
|
||
|
download_info = {
|
||
|
'user_id': username,
|
||
|
'downloaded_at': datetime.now(pytz.UTC).isoformat()
|
||
|
}
|
||
|
v['downloads'].append(download_info)
|
||
|
|
||
|
# Guardar metadatos actualizados
|
||
|
save_json_file(meta_file, document_meta)
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def find_document_directory(project_dir, document_id):
|
||
|
"""
|
||
|
Encontrar el directorio de un documento por su ID.
|
||
|
|
||
|
Args:
|
||
|
project_dir (str): Ruta al directorio del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
|
||
|
Returns:
|
||
|
str: Ruta al directorio o None si no se encuentra
|
||
|
"""
|
||
|
documents_dir = os.path.join(project_dir, 'documents')
|
||
|
|
||
|
if not os.path.exists(documents_dir):
|
||
|
return None
|
||
|
|
||
|
# Prefijo a buscar en nombres de directorios
|
||
|
prefix = f"@{int(document_id):03d}_@"
|
||
|
|
||
|
for dir_name in os.listdir(documents_dir):
|
||
|
if dir_name.startswith(prefix):
|
||
|
return os.path.join(documents_dir, dir_name)
|
||
|
|
||
|
return None
|
||
|
|
||
|
def get_project_documents(project_id):
|
||
|
"""
|
||
|
Obtener todos los documentos de un proyecto.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
|
||
|
Returns:
|
||
|
list: Lista de documentos
|
||
|
"""
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return []
|
||
|
|
||
|
documents_dir = os.path.join(project_dir, 'documents')
|
||
|
|
||
|
if not os.path.exists(documents_dir):
|
||
|
return []
|
||
|
|
||
|
documents = []
|
||
|
|
||
|
# Iterar sobre directorios de documentos
|
||
|
for dir_name in os.listdir(documents_dir):
|
||
|
if dir_name.startswith('@') and os.path.isdir(os.path.join(documents_dir, dir_name)):
|
||
|
document_dir = os.path.join(documents_dir, dir_name)
|
||
|
meta_file = os.path.join(document_dir, 'meta.json')
|
||
|
|
||
|
if os.path.exists(meta_file):
|
||
|
document_meta = load_json_file(meta_file)
|
||
|
|
||
|
if document_meta:
|
||
|
# Extraer ID del documento del nombre del directorio
|
||
|
try:
|
||
|
doc_id = int(dir_name.split('_', 1)[0].replace('@', ''))
|
||
|
document_meta['id'] = doc_id
|
||
|
document_meta['directory'] = dir_name
|
||
|
documents.append(document_meta)
|
||
|
except (ValueError, IndexError):
|
||
|
pass
|
||
|
|
||
|
return documents
|
||
|
|
||
|
def delete_document(project_id, document_id):
|
||
|
"""
|
||
|
Eliminar un documento.
|
||
|
|
||
|
Args:
|
||
|
project_id (int): ID del proyecto
|
||
|
document_id (int): ID del documento
|
||
|
|
||
|
Returns:
|
||
|
tuple: (success, message)
|
||
|
"""
|
||
|
# Buscar documento
|
||
|
project_dir = find_project_directory(project_id)
|
||
|
|
||
|
if not project_dir:
|
||
|
return False, f"No se encontró el proyecto con ID {project_id}."
|
||
|
|
||
|
document_dir = find_document_directory(project_dir, document_id)
|
||
|
|
||
|
if not document_dir:
|
||
|
return False, f"No se encontró el documento con ID {document_id}."
|
||
|
|
||
|
# Eliminar directorio y contenido
|
||
|
try:
|
||
|
import shutil
|
||
|
shutil.rmtree(document_dir)
|
||
|
return True, "Documento eliminado correctamente."
|
||
|
except Exception as e:
|
||
|
return False, f"Error al eliminar el documento: {str(e)}"
|