files-server-fastapi 0.1.4__tar.gz → 0.1.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/PKG-INFO +1 -1
  2. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/pyproject.toml +1 -1
  3. files_server_fastapi-0.1.6/src/files_server_fastapi/files/__init__.py +12 -0
  4. files_server_fastapi-0.1.6/src/files_server_fastapi/files/constants.py +24 -0
  5. files_server_fastapi-0.1.6/src/files_server_fastapi/files/dependencies.py +90 -0
  6. files_server_fastapi-0.1.6/src/files_server_fastapi/files/download_router.py +42 -0
  7. files_server_fastapi-0.1.6/src/files_server_fastapi/files/folder_router.py +41 -0
  8. files_server_fastapi-0.1.6/src/files_server_fastapi/files/list_router.py +44 -0
  9. files_server_fastapi-0.1.6/src/files_server_fastapi/files/open_url_router.py +51 -0
  10. files_server_fastapi-0.1.6/src/files_server_fastapi/files/tree_router.py +31 -0
  11. files_server_fastapi-0.1.6/src/files_server_fastapi/files/upload_router.py +64 -0
  12. files_server_fastapi-0.1.6/src/files_server_fastapi/routers/area_router.py +131 -0
  13. files_server_fastapi-0.1.6/src/files_server_fastapi/routers/files_router.py +18 -0
  14. files_server_fastapi-0.1.6/src/files_server_fastapi/routers/users_extend_router.py +151 -0
  15. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/uv.lock +1 -1
  16. files_server_fastapi-0.1.4/src/files_server_fastapi/routers/area_router.py +0 -26
  17. files_server_fastapi-0.1.4/src/files_server_fastapi/routers/files_router.py +0 -479
  18. files_server_fastapi-0.1.4/src/files_server_fastapi/routers/users_extend_router.py +0 -67
  19. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/.gitignore +0 -0
  20. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/.python-version +0 -0
  21. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/LICENSE +0 -0
  22. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/MANIFEST.in +0 -0
  23. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/README.md +0 -0
  24. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/__init__.py +0 -0
  25. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/__init__.py +0 -0
  26. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/area_model.py +0 -0
  27. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/permisos_model.py +0 -0
  28. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/rol_model.py +0 -0
  29. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/rutas_model.py +0 -0
  30. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/models/users_extend_model.py +0 -0
  31. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/routers/__init__.py +0 -0
  32. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/routers/permisos_router.py +0 -0
  33. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/routers/rol_router.py +0 -0
  34. {files_server_fastapi-0.1.4 → files_server_fastapi-0.1.6}/src/files_server_fastapi/routers/rutas_router.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: files-server-fastapi
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Este repositorio es un paquete python desarrollado con FastAPI, gestiona la comunicación con un servidor Samba y utiliza PostgreSQL para administrar rutas de archivos y el control de acceso basado en roles (RBAC) de los usuarios.
5
5
  Project-URL: Homepage, https://github.com/AldoBP/files-server-fastapi
6
6
  Project-URL: Repository, https://github.com/AldoBP/files-server-fastapi
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "files-server-fastapi"
3
- version = "0.1.4"
3
+ version = "0.1.6"
4
4
  description = "Este repositorio es un paquete python desarrollado con FastAPI, gestiona la comunicación con un servidor Samba y utiliza PostgreSQL para administrar rutas de archivos y el control de acceso basado en roles (RBAC) de los usuarios."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -0,0 +1,12 @@
1
+ """
2
+ Paquete de sub-módulos para el router de archivos.
3
+
4
+ constants.py — BASE_DIR, OFFICE_PROTOCOLS, INLINE_MIME_TYPES
5
+ dependencies.py — check_folder_access (validación ACL)
6
+ list_router.py — GET /files/list
7
+ folder_router.py — POST /files/folder
8
+ upload_router.py — POST /files/upload
9
+ open_url_router.py — GET /files/open-url
10
+ download_router.py — GET /files/download
11
+ tree_router.py — GET /files/tree
12
+ """
@@ -0,0 +1,24 @@
1
+ # Directorio maestro (ruta de red Samba vista desde Windows)
2
+ BASE_DIR = r"\\192.168.1.122\Compartido"
3
+
4
+ # Mapeo extensión → protocolo de Office
5
+ OFFICE_PROTOCOLS = {
6
+ "doc": "ms-word",
7
+ "docx": "ms-word",
8
+ "dot": "ms-word",
9
+ "dotx": "ms-word",
10
+ "xls": "ms-excel",
11
+ "xlsx": "ms-excel",
12
+ "xlsm": "ms-excel",
13
+ "ppt": "ms-powerpoint",
14
+ "pptx": "ms-powerpoint",
15
+ "pps": "ms-powerpoint",
16
+ "ppsx": "ms-powerpoint",
17
+ }
18
+
19
+ # MIME types que el navegador puede mostrar inline (sin descarga forzada)
20
+ INLINE_MIME_TYPES = {
21
+ "application/pdf",
22
+ "image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml",
23
+ "text/plain", "text/csv", "text/html",
24
+ }
@@ -0,0 +1,90 @@
1
+ from fastapi import HTTPException, Depends
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+ from sqlmodel import select
4
+ from pgsqlasync2fast_fastapi.dependencies import get_db_session
5
+ from oauth2fast_fastapi import get_current_user, User
6
+ from files_server_fastapi.models.permisos_model import User_Ruta_Access
7
+ from files_server_fastapi.models.rutas_model import Rutas
8
+ from files_server_fastapi.models.users_extend_model import Users_extend
9
+ from files_server_fastapi.models.area_model import Area
10
+ from files_server_fastapi.models.rol_model import Rol
11
+
12
+
13
+ async def check_folder_access(
14
+ area: str,
15
+ subpath: str = "/",
16
+ required_access: str = "allow_read",
17
+ current_user: User = Depends(get_current_user),
18
+ db: AsyncSession = Depends(get_db_session),
19
+ ):
20
+ """
21
+ Verifica si el usuario actual tiene acceso al área y subpath indicados.
22
+ required_access: 'allow_read' (por defecto) o 'allow_write'
23
+ """
24
+ # 1. Obtener extensión de usuario y verificar pertenencia al área
25
+ result_ext = await db.execute(
26
+ select(Users_extend).where(Users_extend.user_id == current_user.id)
27
+ )
28
+ user_exts = result_ext.scalars().all()
29
+
30
+ area_obj = None
31
+ user_ext_match = None
32
+ for ext in user_exts:
33
+ res_area = await db.execute(select(Area).where(Area.id == ext.area_id))
34
+ a = res_area.scalars().first()
35
+ if a and a.area_name.upper() == area.upper():
36
+ area_obj = a
37
+ user_ext_match = ext
38
+ break
39
+
40
+ if not area_obj or not user_ext_match:
41
+ raise HTTPException(status_code=403, detail="No perteneces a esta área")
42
+
43
+ # Obtener el Rol Base del usuario en esta área
44
+ res_rol = await db.execute(select(Rol).where(Rol.id == user_ext_match.rol_id))
45
+ rol_obj = res_rol.scalars().first()
46
+ rol_name = rol_obj.role_name.lower() if rol_obj else ""
47
+
48
+ # 2. Verificar ACL específico por ruta y herencia de padres
49
+ logical_path = f"/{area.upper()}/{subpath.strip('/')}".replace("//", "/")
50
+ parts = logical_path.strip("/").split("/")
51
+ paths_to_check = []
52
+ current_path = ""
53
+ for part in parts:
54
+ if part:
55
+ current_path += f"/{part}"
56
+ paths_to_check.append(current_path)
57
+
58
+ paths_to_check.reverse() # Más específico primero
59
+
60
+ res_acl = await db.execute(
61
+ select(Rutas.ruta, User_Ruta_Access)
62
+ .join(User_Ruta_Access, User_Ruta_Access.ruta_id == Rutas.id)
63
+ .where(Rutas.ruta.in_(paths_to_check))
64
+ .where(User_Ruta_Access.user_id == current_user.id)
65
+ )
66
+ acls_encontrados = {row[0]: row[1] for row in res_acl.all()}
67
+
68
+ for path_in_tree in paths_to_check:
69
+ if path_in_tree in acls_encontrados:
70
+ acl_obj = acls_encontrados[path_in_tree]
71
+
72
+ if acl_obj.access_type == "deny_all":
73
+ raise HTTPException(status_code=403, detail="Acceso denegado a esta carpeta o heredado de una superior")
74
+
75
+ if required_access == "allow_write":
76
+ if acl_obj.access_type == "allow_write":
77
+ return True
78
+ elif acl_obj.access_type == "allow_read":
79
+ raise HTTPException(status_code=403, detail="Solo tienes permiso de lectura (regla heredada)")
80
+
81
+ if required_access == "allow_read":
82
+ if acl_obj.access_type in ["allow_read", "allow_write"]:
83
+ return True
84
+
85
+ # 3. Sin ACL explícito → aplicar reglas del Rol Base
86
+ if required_access == "allow_write":
87
+ if "editor" not in rol_name and "admin" not in rol_name:
88
+ raise HTTPException(status_code=403, detail="Tu rol no permite modificar esta carpeta")
89
+
90
+ return True
@@ -0,0 +1,42 @@
1
+ import os
2
+ import mimetypes
3
+ from fastapi import APIRouter, HTTPException, Depends
4
+ from fastapi.responses import FileResponse
5
+ from files_server_fastapi.files.constants import BASE_DIR, INLINE_MIME_TYPES
6
+ from files_server_fastapi.files.dependencies import check_folder_access
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get("/download", summary="Descargar o visualizar un archivo inline en el navegador")
12
+ async def download_file(
13
+ area: str,
14
+ filename: str,
15
+ subpath: str = "/",
16
+ has_access: bool = Depends(check_folder_access)
17
+ ):
18
+ """
19
+ Sirve un archivo desde el share Samba.
20
+ - PDFs e imágenes → inline en el navegador.
21
+ - Otros tipos → descarga forzada.
22
+ """
23
+ if ".." in subpath or ".." in filename:
24
+ raise HTTPException(status_code=400, detail="Ruta o nombre de archivo inválido")
25
+
26
+ safe_filename = os.path.basename(filename)
27
+ safe_subpath = subpath.strip("/")
28
+ ruta_real = os.path.join(BASE_DIR, area.upper(), safe_subpath, safe_filename) if safe_subpath else os.path.join(BASE_DIR, area.upper(), safe_filename)
29
+
30
+ if not os.path.isfile(ruta_real):
31
+ raise HTTPException(status_code=404, detail=f"Archivo no encontrado: {safe_filename}")
32
+
33
+ mime_type, _ = mimetypes.guess_type(safe_filename)
34
+ mime_type = mime_type or "application/octet-stream"
35
+ disposition = "inline" if mime_type in INLINE_MIME_TYPES else "attachment"
36
+
37
+ return FileResponse(
38
+ path=ruta_real,
39
+ media_type=mime_type,
40
+ filename=safe_filename,
41
+ headers={"Content-Disposition": f'{disposition}; filename="{safe_filename}"', "Cache-Control": "no-cache"},
42
+ )
@@ -0,0 +1,41 @@
1
+ import os
2
+ from fastapi import APIRouter, HTTPException, Depends
3
+ from pydantic import BaseModel
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from pgsqlasync2fast_fastapi.dependencies import get_db_session
6
+ from oauth2fast_fastapi import get_current_user, User
7
+ from files_server_fastapi.files.constants import BASE_DIR
8
+ from files_server_fastapi.files.dependencies import check_folder_access
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ class FolderCreate(BaseModel):
14
+ area: str
15
+ subpath: str
16
+ folder_name: str
17
+
18
+
19
+ @router.post("/folder", summary="Crear una nueva carpeta en el servidor")
20
+ async def create_folder(
21
+ req: FolderCreate,
22
+ current_user: User = Depends(get_current_user),
23
+ db: AsyncSession = Depends(get_db_session)
24
+ ):
25
+ await check_folder_access(area=req.area, subpath=req.subpath, required_access="allow_write", current_user=current_user, db=db)
26
+
27
+ if ".." in req.subpath or ".." in req.folder_name or "/" in req.folder_name:
28
+ raise HTTPException(status_code=400, detail="Nombre de carpeta o ruta inválida")
29
+
30
+ safe_subpath = req.subpath.strip("/")
31
+ ruta_final = os.path.join(BASE_DIR, req.area.upper(), safe_subpath, req.folder_name) if safe_subpath else os.path.join(BASE_DIR, req.area.upper(), req.folder_name)
32
+
33
+ try:
34
+ os.makedirs(ruta_final, exist_ok=False)
35
+ return {"message": "Carpeta creada exitosamente", "path": ruta_final}
36
+ except FileExistsError:
37
+ raise HTTPException(status_code=400, detail="Ya existe una carpeta con ese nombre aquí")
38
+ except PermissionError:
39
+ raise HTTPException(status_code=403, detail="El servidor rechazó el permiso de escritura")
40
+ except Exception as e:
41
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,44 @@
1
+ import os
2
+ from datetime import datetime
3
+ from fastapi import APIRouter, HTTPException, Depends
4
+ from files_server_fastapi.files.constants import BASE_DIR
5
+ from files_server_fastapi.files.dependencies import check_folder_access
6
+
7
+ router = APIRouter()
8
+
9
+
10
+ @router.get("/list", summary="Listar archivos de una carpeta")
11
+ async def list_directory(
12
+ area: str,
13
+ subpath: str = "/",
14
+ has_access: bool = Depends(check_folder_access)
15
+ ):
16
+ """Devuelve el contenido de una carpeta dentro del área indicada."""
17
+ if ".." in subpath:
18
+ raise HTTPException(status_code=400, detail="Ruta inválida")
19
+
20
+ safe_subpath = subpath.strip("/")
21
+ ruta_real = os.path.join(BASE_DIR, area.upper(), safe_subpath) if safe_subpath else os.path.join(BASE_DIR, area.upper())
22
+
23
+ if not os.path.exists(ruta_real):
24
+ return []
25
+ if not os.path.isdir(ruta_real):
26
+ raise HTTPException(status_code=400, detail="La ruta no es un directorio")
27
+
28
+ items = []
29
+ try:
30
+ with os.scandir(ruta_real) as ficheros:
31
+ for fichero in ficheros:
32
+ info = fichero.stat()
33
+ fecha_mod = datetime.fromtimestamp(info.st_mtime).strftime("%Y-%m-%d %H:%M")
34
+ if fichero.is_dir():
35
+ items.append({"name": fichero.name, "type": "folder", "updated": fecha_mod, "size": "", "locked": False})
36
+ else:
37
+ size_kb = info.st_size / 1024
38
+ size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.1f} MB"
39
+ items.append({"name": fichero.name, "type": "file", "updated": fecha_mod, "size": size_str, "locked": False})
40
+
41
+ items.sort(key=lambda x: (x["type"] == "file", x["name"].lower()))
42
+ return items
43
+ except PermissionError:
44
+ raise HTTPException(status_code=403, detail="Acceso denegado por el sistema operativo")
@@ -0,0 +1,51 @@
1
+ import os
2
+ from fastapi import APIRouter, HTTPException, Depends
3
+ from files_server_fastapi.files.constants import BASE_DIR, OFFICE_PROTOCOLS
4
+ from files_server_fastapi.files.dependencies import check_folder_access
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get("/open-url", summary="Obtener URL para abrir un archivo en la app local (Office, etc.)")
10
+ async def get_open_url(
11
+ area: str,
12
+ filename: str,
13
+ subpath: str = "/",
14
+ has_access: bool = Depends(check_folder_access)
15
+ ):
16
+ """
17
+ Devuelve la URL de protocolo adecuada para abrir el archivo directamente
18
+ en la aplicación instalada en la PC del usuario.
19
+
20
+ - Archivos **Office** (.docx, .xlsx, .pptx…): URL `ms-word:ofe|u|...` para abrir y guardar directo.
21
+ - **Otros** (PDF, imágenes, txt): URL del endpoint `/files/download` para vista inline.
22
+ """
23
+ if ".." in subpath or ".." in filename:
24
+ raise HTTPException(status_code=400, detail="Ruta o nombre de archivo inválido")
25
+
26
+ safe_filename = os.path.basename(filename)
27
+ safe_subpath = subpath.strip("/")
28
+ ruta_real = os.path.join(BASE_DIR, area.upper(), safe_subpath, safe_filename) if safe_subpath else os.path.join(BASE_DIR, area.upper(), safe_filename)
29
+
30
+ if not os.path.isfile(ruta_real):
31
+ raise HTTPException(status_code=404, detail=f"Archivo no encontrado: {safe_filename}")
32
+
33
+ ext = safe_filename.rsplit(".", 1)[-1].lower() if "." in safe_filename else ""
34
+ office_protocol = OFFICE_PROTOCOLS.get(ext)
35
+
36
+ if office_protocol:
37
+ subpath_win = safe_subpath.replace("/", "\\")
38
+ unc_path = f"{BASE_DIR}\\{area.upper()}\\{subpath_win}\\{safe_filename}" if safe_subpath else f"{BASE_DIR}\\{area.upper()}\\{safe_filename}"
39
+ return {
40
+ "type": "office",
41
+ "protocol": office_protocol,
42
+ "url": f"{office_protocol}:ofe|u|{unc_path}",
43
+ "unc_path": unc_path,
44
+ "filename": safe_filename,
45
+ }
46
+
47
+ return {
48
+ "type": "download",
49
+ "url": f"/files/download?area={area}&subpath={subpath}&filename={safe_filename}",
50
+ "filename": safe_filename,
51
+ }
@@ -0,0 +1,31 @@
1
+ import os
2
+ from fastapi import APIRouter
3
+ from files_server_fastapi.files.constants import BASE_DIR
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ def get_directory_tree(path_to_scan: str, base_name: str = "") -> list:
9
+ """Función recursiva para leer el árbol de subcarpetas."""
10
+ tree = []
11
+ try:
12
+ with os.scandir(path_to_scan) as entries:
13
+ for entry in entries:
14
+ if entry.is_dir():
15
+ relative_path = os.path.join(base_name, entry.name).replace("\\", "/")
16
+ tree.append({
17
+ "name": entry.name,
18
+ "path": f"/{relative_path}",
19
+ "children": get_directory_tree(entry.path, relative_path)
20
+ })
21
+ except PermissionError:
22
+ pass
23
+ return sorted(tree, key=lambda x: x["name"].lower())
24
+
25
+
26
+ @router.get("/tree", summary="Obtener el árbol de carpetas de un área")
27
+ async def get_area_tree(area: str):
28
+ area_path = os.path.join(BASE_DIR, area.upper())
29
+ if not os.path.exists(area_path):
30
+ return []
31
+ return get_directory_tree(area_path)
@@ -0,0 +1,64 @@
1
+ import os
2
+ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from pgsqlasync2fast_fastapi.dependencies import get_db_session
5
+ from oauth2fast_fastapi import get_current_user, User
6
+ from files_server_fastapi.files.constants import BASE_DIR
7
+ from files_server_fastapi.files.dependencies import check_folder_access
8
+
9
+ router = APIRouter()
10
+
11
+
12
+ @router.post("/upload", summary="Subir un archivo a una carpeta del servidor")
13
+ async def upload_file(
14
+ area: str = Form(...),
15
+ subpath: str = Form(default="/"),
16
+ file: UploadFile = File(...),
17
+ current_user: User = Depends(get_current_user),
18
+ db: AsyncSession = Depends(get_db_session)
19
+ ):
20
+ """
21
+ Sube un archivo a la carpeta indicada dentro del share Samba.
22
+
23
+ - **area**: Área destino (ej: 'CONTABILIDAD')
24
+ - **subpath**: Subcarpeta relativa (ej: '/' raíz, '/2024/Enero' subcarpeta)
25
+ - **file**: El archivo a subir (multipart/form-data)
26
+ """
27
+ await check_folder_access(area=area, subpath=subpath, required_access="allow_write", current_user=current_user, db=db)
28
+
29
+ if ".." in subpath or ".." in (file.filename or ""):
30
+ raise HTTPException(status_code=400, detail="Ruta o nombre de archivo inválido")
31
+
32
+ safe_filename = os.path.basename(file.filename or "archivo_sin_nombre")
33
+ if not safe_filename:
34
+ raise HTTPException(status_code=400, detail="Nombre de archivo inválido")
35
+
36
+ safe_subpath = subpath.strip("/")
37
+ ruta_destino = os.path.join(BASE_DIR, area.upper(), safe_subpath) if safe_subpath else os.path.join(BASE_DIR, area.upper())
38
+
39
+ if not os.path.isdir(ruta_destino):
40
+ raise HTTPException(status_code=404, detail=f"La carpeta de destino no existe: /{area.upper()}/{safe_subpath}")
41
+
42
+ ruta_archivo = os.path.join(ruta_destino, safe_filename)
43
+ if os.path.exists(ruta_archivo):
44
+ raise HTTPException(status_code=409, detail=f"Ya existe un archivo con ese nombre: {safe_filename}")
45
+
46
+ try:
47
+ CHUNK_SIZE = 1024 * 1024 # 1 MB por chunk
48
+ with open(ruta_archivo, "wb") as f:
49
+ while chunk := await file.read(CHUNK_SIZE):
50
+ f.write(chunk)
51
+
52
+ size_bytes = os.path.getsize(ruta_archivo)
53
+ size_kb = size_bytes / 1024
54
+ size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb / 1024:.1f} MB"
55
+ ruta_logica = f"/{area.upper()}/{safe_subpath}/{safe_filename}".replace("//", "/")
56
+
57
+ return {"message": "Archivo subido exitosamente", "filename": safe_filename, "size": size_str, "path": ruta_logica}
58
+
59
+ except PermissionError:
60
+ raise HTTPException(status_code=403, detail="El servidor rechazó el permiso de escritura en el share Samba")
61
+ except OSError as e:
62
+ raise HTTPException(status_code=500, detail=f"Error del sistema al guardar el archivo: {e}")
63
+ finally:
64
+ await file.close()
@@ -0,0 +1,131 @@
1
+ from typing import Optional
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from sqlalchemy import select
5
+ from pydantic import BaseModel
6
+
7
+ from pgsqlasync2fast_fastapi.dependencies import get_db_session
8
+ from files_server_fastapi.models.area_model import Area
9
+
10
+ router = APIRouter(prefix="/areas", tags=["Gestión de Áreas"])
11
+
12
+
13
+ class AreaCreate(BaseModel):
14
+ area_name: str
15
+ description: Optional[str] = None
16
+
17
+
18
+ class AreaUpdate(BaseModel):
19
+ area_name: Optional[str] = None
20
+ description: Optional[str] = None
21
+
22
+
23
+ # ==========================================
24
+ # POST /areas/ — Crear nueva área
25
+ # ==========================================
26
+ @router.post("/", response_model=Area, status_code=status.HTTP_201_CREATED, summary="Crear una nueva Área")
27
+ async def create_area(area_data: AreaCreate, db: AsyncSession = Depends(get_db_session)):
28
+ """
29
+ Crea y guarda una nueva área en la base de datos.
30
+ Solo accesible por superusuarios desde el panel de administración.
31
+ """
32
+ # Verificar si ya existe un área con el mismo nombre
33
+ result = await db.execute(select(Area).where(Area.area_name == area_data.area_name))
34
+ existing = result.scalars().first()
35
+ if existing:
36
+ raise HTTPException(
37
+ status_code=status.HTTP_409_CONFLICT,
38
+ detail=f"Ya existe un área con el nombre '{area_data.area_name}'."
39
+ )
40
+
41
+ new_area = Area(**area_data.model_dump())
42
+ db.add(new_area)
43
+ await db.commit()
44
+ await db.refresh(new_area)
45
+ return new_area
46
+
47
+
48
+ # ==========================================
49
+ # GET /areas/ — Listar todas las áreas
50
+ # ==========================================
51
+ @router.get("/", response_model=list[Area], summary="Obtener todas las Áreas")
52
+ async def get_areas(db: AsyncSession = Depends(get_db_session)):
53
+ """
54
+ Devuelve la lista de todas las áreas registradas.
55
+ """
56
+ result = await db.execute(select(Area))
57
+ return result.scalars().all()
58
+
59
+
60
+ # ==========================================
61
+ # GET /areas/{area_id} — Obtener área por ID
62
+ # ==========================================
63
+ @router.get("/{area_id}", response_model=Area, summary="Obtener un Área por ID")
64
+ async def get_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
65
+ """
66
+ Devuelve los datos de un área específica por su ID.
67
+ """
68
+ result = await db.execute(select(Area).where(Area.id == area_id))
69
+ area = result.scalars().first()
70
+ if not area:
71
+ raise HTTPException(
72
+ status_code=status.HTTP_404_NOT_FOUND,
73
+ detail=f"No se encontró el área con ID {area_id}."
74
+ )
75
+ return area
76
+
77
+
78
+ # ==========================================
79
+ # PATCH /areas/{area_id} — Actualizar área
80
+ # ==========================================
81
+ @router.patch("/{area_id}", response_model=Area, summary="Actualizar un Área")
82
+ async def update_area(area_id: int, area_update: AreaUpdate, db: AsyncSession = Depends(get_db_session)):
83
+ """
84
+ Actualiza parcialmente los datos de un área existente.
85
+ """
86
+ result = await db.execute(select(Area).where(Area.id == area_id))
87
+ area = result.scalars().first()
88
+ if not area:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_404_NOT_FOUND,
91
+ detail=f"No se encontró el área con ID {area_id}."
92
+ )
93
+
94
+ # Si se quiere cambiar el nombre, verificar que no exista otro con ese nombre
95
+ if area_update.area_name and area_update.area_name != area.area_name:
96
+ dup_result = await db.execute(select(Area).where(Area.area_name == area_update.area_name))
97
+ if dup_result.scalars().first():
98
+ raise HTTPException(
99
+ status_code=status.HTTP_409_CONFLICT,
100
+ detail=f"Ya existe un área con el nombre '{area_update.area_name}'."
101
+ )
102
+
103
+ update_data = area_update.model_dump(exclude_unset=True)
104
+ for key, value in update_data.items():
105
+ setattr(area, key, value)
106
+
107
+ db.add(area)
108
+ await db.commit()
109
+ await db.refresh(area)
110
+ return area
111
+
112
+
113
+ # ==========================================
114
+ # DELETE /areas/{area_id} — Eliminar área
115
+ # ==========================================
116
+ @router.delete("/{area_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Eliminar un Área")
117
+ async def delete_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
118
+ """
119
+ Elimina un área por su ID.
120
+ ADVERTENCIA: Solo eliminar si no hay usuarios asociados a esta área.
121
+ """
122
+ result = await db.execute(select(Area).where(Area.id == area_id))
123
+ area = result.scalars().first()
124
+ if not area:
125
+ raise HTTPException(
126
+ status_code=status.HTTP_404_NOT_FOUND,
127
+ detail=f"No se encontró el área con ID {area_id}."
128
+ )
129
+
130
+ await db.delete(area)
131
+ await db.commit()
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter
2
+ from files_server_fastapi.files import (
3
+ list_router,
4
+ folder_router,
5
+ upload_router,
6
+ open_url_router,
7
+ download_router,
8
+ tree_router,
9
+ )
10
+
11
+ router = APIRouter(prefix="/files", tags=["Archivos del Sistema"])
12
+
13
+ router.include_router(list_router.router)
14
+ router.include_router(folder_router.router)
15
+ router.include_router(upload_router.router)
16
+ router.include_router(open_url_router.router)
17
+ router.include_router(download_router.router)
18
+ router.include_router(tree_router.router)