CodeGym /Cursos /JAVA 25 SELF /Permisos y acceso al sistema de archivos

Permisos y acceso al sistema de archivos

JAVA 25 SELF
Nivel 38 , Lección 3
Disponible

1. Permisos de acceso en el SO

Cuando trabajas con archivos y carpetas, es importante recordar: el sistema operativo (SO) los protege mediante un sistema de permisos de acceso. Esto significa que no cualquier programa (ni cualquier usuario) puede leer, modificar o eliminar cualquier archivo.

POSIX (Linux, macOS, etc.)

En sistemas POSIX (Unix, Linux, macOS) cada archivo y carpeta tiene permisos para tres categorías:

  • Propietario (user)
  • Grupo (group)
  • Otros (others)

Para cada categoría se definen tres tipos de permisos:

  • r — read (lectura)
  • w — write (escritura)
  • x — execute (ejecución)

Ejemplo:
-rw-r--r--
Esto significa: el propietario puede leer y escribir, los demás — solo leer.

Windows

En Windows los permisos se definen mediante ACL (Access Control List): listas de permisos para usuarios y grupos. Aquí se puede configurar de forma flexible quién y qué puede hacer con un archivo o carpeta (leer, escribir, modificar, ejecutar, etc.).

Importante:
Una aplicación Java trabaja con archivos dentro de los permisos del usuario con el que se ejecuta. Si el usuario no tiene permisos sobre el archivo, la aplicación tampoco podrá leerlo ni modificarlo.

2. Excepción AccessDeniedException y sus causas

Cuando trabajas con archivos en Java (especialmente con la API NIO), puedes encontrarte con la excepción:

java.nio.file.AccessDeniedException

Esta excepción se lanza si tu aplicación no tiene permisos para realizar la operación con el archivo o el directorio.

Causas principales:

  • No hay permisos de lectura del archivo (por ejemplo, el archivo está protegido contra lectura).
  • No hay permisos de escritura en el archivo o la carpeta (por ejemplo, intentas escribir en una carpeta del sistema).
  • No hay permisos de ejecución del archivo (relevante para iniciar programas).
  • La carpeta o el archivo están protegidos contra cambios (por ejemplo, de solo lectura).
  • El archivo o la carpeta están en uso por otro proceso (muy habitual en Windows).

Ejemplo:

Path path = Paths.get("/etc/shadow"); // archivo del sistema de Linux
Files.readAllLines(path); // AccessDeniedException

¿Qué hacer?

  • Comprueba los permisos de acceso del archivo/carpeta.
  • Ejecuta la aplicación con un usuario que tenga los permisos necesarios.
  • No intentes escribir en directorios del sistema si no es necesario.

3. Comprobación de permisos en Java: métodos Files.isReadable(), isWritable(), isExecutable()

Java proporciona métodos prácticos para comprobar permisos sobre un archivo o carpeta:

Path path = Paths.get("example.txt");

System.out.println(Files.isReadable(path));   // true, si se puede leer
System.out.println(Files.isWritable(path));   // true, si se puede escribir
System.out.println(Files.isExecutable(path)); // true, si se puede ejecutar

Estos métodos muestran cómo ve el sistema tus permisos en este momento.

¡Pero!
No garantizan que la operación vaya a completarse con éxito. Las razones pueden ser varias: el archivo puede estar bloqueado por otra aplicación, los permisos pueden haber cambiado después de la comprobación, en discos de red los resultados dependen del servidor, y a veces el SO informa una cosa pero aplica otras restricciones.

Por eso en Java es mejor primero comprobar los permisos y luego envolver la operación real de lectura o escritura en un try-catch: es más fiable.

Problema TOCTOU (Time Of Check To Time Of Use)

Está directamente relacionado con la situación TOCTOU, cuando entre la comprobación del permiso y la operación en sí algo cambia. Por ejemplo:

  1. Compruebas que el archivo está disponible para escritura (isWritable).
  2. En ese momento otro proceso o usuario cambia los permisos: ahora el archivo está protegido.
  3. Intentas escribir datos — obtienes AccessDeniedException.

Conclusión:
La comprobación de permisos solo da una pista del estado actual, pero no garantiza el éxito de la operación. Maneja siempre las excepciones cuando trabajes con archivos.

4. Principio de «escritura segura» (atomic write)

¿Por qué hace falta un método de escritura segura (atómica)?

A veces, al escribir un archivo, puede producirse un fallo: la aplicación se cae, se corta la electricidad, no hay espacio en disco... Como resultado, el archivo puede quedar dañado o escrito solo parcialmente. Esto es especialmente peligroso para datos importantes (por ejemplo, configuraciones, bases de datos, documentos).

Escritura segura es una forma de garantizar que el archivo quede completamente actualizado o que permanezca en su estado anterior. A este enfoque se le llama escritura atómica (atomic write).

¿Cómo implementar la escritura segura en Java?

Patrón:

  • Escribe los datos en un archivo temporal (normalmente en la misma carpeta).
  • Si la escritura se realizó con éxito — mueve de forma atómica el archivo temporal al lugar del principal (reemplazándolo).

¿Por qué funciona?
La operación de mover un archivo (rename/move) dentro del mismo sistema de archivos suele ser atómica: o bien el archivo se reemplaza por completo, o no. Si algo sale mal, el archivo principal no se toca.

Ejemplo de código: escritura segura de un archivo

import java.nio.file.*;

public class SafeWriteDemo {
    public static void safeWrite(Path target, byte[] data) throws Exception {
        // 1. Creamos un archivo temporal en la misma carpeta
        Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");

        try {
            // 2. Escribimos los datos en el archivo temporal
            Files.write(tempFile, data);

            // 3. Movemos de forma atómica el archivo temporal al destino
            Files.move(
                tempFile,
                target,
                StandardCopyOption.REPLACE_EXISTING,
                StandardCopyOption.ATOMIC_MOVE
            );
        } finally {
            // Si algo salió mal — eliminamos el archivo temporal
            Files.deleteIfExists(tempFile);
        }
    }

    public static void main(String[] args) throws Exception {
        Path file = Paths.get("important.txt");
        byte[] content = "Datos muy importantes".getBytes();

        safeWrite(file, content);
        System.out.println("¡Archivo escrito de forma segura!");
    }
}

Ten en cuenta:

  • Usa Files.createTempFile() para crear el archivo temporal.
  • Para mover utiliza la opción ATOMIC_MOVE: garantiza la atomicidad (si el SO y el sistema de archivos lo soportan).
  • Si algo sale mal, el archivo temporal se elimina (Files.deleteIfExists).

¿Cuándo es especialmente importante?

  • Al trabajar con archivos de configuración, bases de datos y registros (logs).
  • Si otro proceso puede leer el archivo en cualquier momento.
  • Si un error de escritura puede provocar pérdida o corrupción de datos.

5. Registro (logging) y manejo de errores de acceso

¿Cómo manejar correctamente los errores de acceso?

Al trabajar con archivos, usa siempre manejo de excepciones (try-catch). Esto permite:

  • Informar correctamente al usuario del problema (por ejemplo, «No hay permisos de escritura en la carpeta»).
  • Registrar el error en el log para su posterior análisis.
  • Evitar que toda la aplicación falle por una sola operación errónea.

Ejemplo: manejo de AccessDeniedException

import java.nio.file.*;

public class FileAccessDemo {
    public static void main(String[] args) {
        Path file = Paths.get("/etc/shadow"); // ejemplo para Linux

        try {
            Files.readAllLines(file);
        } catch (AccessDeniedException ade) {
            System.err.println("Error de acceso: no hay permisos de lectura para el archivo " + file);
            // Se puede escribir en el log o proponer al usuario elegir otro archivo
        } catch (Exception e) {
            System.err.println("Otro error: " + e.getMessage());
        }
    }
}

Registro de errores

En aplicaciones reales utiliza sistemas de logging (por ejemplo, java.util.logging, Log4j, SLF4J). Esto permite:

  • Registrar errores con detalles (traza de pila, hora, usuario).
  • Analizar los logs para encontrar y solucionar problemas.
  • No mostrar al usuario mensajes «alarmantes», sino enviarlos al log.

Ejemplo con logging:

import java.nio.file.*;
import java.util.logging.*;

public class FileLoggerDemo {
    private static final Logger logger = Logger.getLogger(FileLoggerDemo.class.getName());

    public static void main(String[] args) {
        Path file = Paths.get("data.txt");

        try {
            Files.readAllLines(file);
        } catch (AccessDeniedException ade) {
            logger.severe("Sin acceso al archivo: " + file);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Error al trabajar con el archivo", e);
        }
    }
}

6. Errores típicos

Error n.º 1: ignorar las excepciones al trabajar con archivos.
Nunca escribas simplemente Files.write(path, data) sin try-catch: si algo sale mal, la aplicación se caerá.

Error n.º 2: comprobar permisos sin tener en cuenta TOCTOU.
No te fíes solo de Files.isWritable() y métodos similares. Incluso si devuelven «se puede», la operación puede fallar. Maneja siempre las excepciones (por ejemplo, AccessDeniedException).

Error n.º 3: escribir «encima» de un archivo existente sin copia de seguridad.
Si el archivo es importante, haz una copia de seguridad antes de escribir o utiliza escritura atómica con StandardCopyOption.ATOMIC_MOVE.

Error n.º 4: no eliminar los archivos temporales tras un fallo.
Si algo sale mal durante la escritura atómica, puede quedar el archivo temporal. Usa finally y Files.deleteIfExists().

Error n.º 5: no registrar (log) los errores de acceso.
Si la aplicación no pudo escribir o leer un archivo, el usuario debe saberlo, y tú debes ver los detalles en el log. Usa java.util.logging/SLF4J y registra las excepciones con su traza de pila.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION