1. Permessi di accesso nel SO
Quando lavori con file e cartelle, è importante ricordare: il sistema operativo (SO) li protegge tramite un sistema di permessi. Questo significa che non ogni programma (e non ogni utente) può leggere, modificare o eliminare qualsiasi file.
POSIX (Linux, macOS, ecc.)
Nei sistemi POSIX (Unix, Linux, macOS) ogni file e cartella ha permessi per tre categorie:
- Proprietario (user)
- Gruppo (group)
- Altri (others)
Per ciascuna categoria si definiscono tre tipi di permesso:
- r — read (lettura)
- w — write (scrittura)
- x — execute (esecuzione)
Esempio:
-rw-r--r--
Significa: il proprietario può leggere e scrivere, gli altri — solo leggere.
Windows
In Windows i permessi si definiscono tramite il sistema ACL (Access Control List) — elenchi di autorizzazioni per utenti e gruppi. Qui si può configurare in modo flessibile chi può fare cosa con un file o una cartella (leggere, scrivere, modificare, eseguire, ecc.).
Importante:
Un programma Java lavora con i file entro i permessi dell’utente con cui è avviato. Se l’utente non ha i permessi sul file — il programma non potrà leggerlo o modificarlo.
2. Eccezione AccessDeniedException e cause
Quando lavori con i file in Java (soprattutto tramite le API NIO), potresti imbatterti nell’eccezione:
java.nio.file.AccessDeniedException
Questa eccezione viene lanciata se il tuo programma non ha i permessi per eseguire un’operazione su un file o una directory.
Cause principali:
- Nessun permesso di lettura del file (ad esempio, il file è protetto dalla lettura).
- Nessun permesso di scrittura nel file o nella cartella (ad esempio, stai tentando di scrivere in una cartella di sistema).
- Nessun permesso di esecuzione del file (rilevante per l’avvio di programmi).
- La cartella o il file sono protetti da modifiche (ad esempio, sola lettura).
- Il file o la cartella sono in uso da un altro processo (soprattutto su Windows).
Esempio:
Path path = Paths.get("/etc/shadow"); // file di sistema Linux
Files.readAllLines(path); // AccessDeniedException!
Cosa fare?
- Verifica i permessi di accesso del file/cartella.
- Esegui il programma come un utente con i permessi necessari.
- Evita di scrivere nelle directory di sistema se non è necessario.
3. Verifica dei permessi in Java: metodi Files.isReadable(), isWritable(), isExecutable()
Java fornisce metodi comodi per verificare i permessi su un file o una cartella:
Path path = Paths.get("example.txt");
System.out.println(Files.isReadable(path)); // true se è possibile leggere
System.out.println(Files.isWritable(path)); // true se è possibile scrivere
System.out.println(Files.isExecutable(path)); // true se è possibile eseguire
Questi metodi mostrano come il sistema vede i tuoi permessi in questo momento.
Ma!
Non garantiscono che l’operazione andrà veramente a buon fine. Le ragioni possono essere varie: il file può essere bloccato da un altro programma, i permessi potrebbero essere cambiati dopo il controllo, sui dischi di rete i risultati dipendono dal server e talvolta il SO riferisce una cosa ma applica altre restrizioni.
Perciò in Java è meglio prima verificare i permessi e poi racchiudere l’operazione reale di lettura o scrittura in un blocco try-catch — è più affidabile.
Problema TOCTOU (Time Of Check To Time Of Use)
È direttamente collegata la situazione TOCTOU, quando tra il controllo del permesso e l’uso effettivo qualcosa cambia. Ad esempio:
- Hai verificato che il file è scrivibile (isWritable).
- Nel frattempo un altro processo o utente modifica i permessi — ora il file è protetto.
- Provi a scrivere i dati — ottieni AccessDeniedException.
Conclusione:
Il controllo dei permessi fornisce solo un’indicazione dello stato attuale, ma non garantisce il successo dell’operazione. Gestisci sempre le eccezioni quando lavori con i file.
4. Principio della «scrittura sicura» (atomic write)
Perché serve un metodo di scrittura sicuro (atomico)?
A volte durante la scrittura di un file può verificarsi un errore: il programma si arresta, salta la corrente, manca spazio sul disco... Di conseguenza il file può risultare corrotto o scritto solo parzialmente. Questo è particolarmente pericoloso per i dati importanti (ad esempio, configurazioni, database, documenti).
La scrittura sicura è un modo per garantire che il file sia completamente aggiornato oppure resti nello stato precedente. Questo approccio si chiama scrittura atomica (atomic write).
Come implementare una scrittura sicura in Java?
Pattern:
- Scriviamo i dati in un file temporaneo (di solito nella stessa cartella).
- Se la scrittura è andata a buon fine — spostiamo in modo atomico il file temporaneo al posto dell’originale (sostituendolo).
Perché funziona?
L’operazione di spostamento del file (rename/move) all’interno dello stesso file system è in genere atomica: o il file viene sostituito completamente, oppure no. Se qualcosa va storto — il file originale rimane intatto.
Esempio di codice: scrittura sicura del file
import java.nio.file.*;
public class SafeWriteDemo {
public static void safeWrite(Path target, byte[] data) throws Exception {
// 1. Creiamo un file temporaneo nella stessa cartella
Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");
try {
// 2. Scriviamo i dati nel file temporaneo
Files.write(tempFile, data);
// 3. Spostiamo in modo atomico il file temporaneo al posto dell'originale
Files.move(
tempFile,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} finally {
// Se qualcosa è andato storto — eliminiamo il file temporaneo
Files.deleteIfExists(tempFile);
}
}
public static void main(String[] args) throws Exception {
Path file = Paths.get("important.txt");
byte[] content = "Dati molto importanti".getBytes();
safeWrite(file, content);
System.out.println("File scritto in modo sicuro!");
}
}
Da notare:
- Usiamo Files.createTempFile() per creare il file temporaneo.
- Per lo spostamento usiamo l’opzione ATOMIC_MOVE — garantisce l’atomicità (se supportata dal SO e dal file system).
- Se qualcosa va storto — il file temporaneo viene eliminato (Files.deleteIfExists).
Quando è particolarmente importante?
- Nella gestione di file di configurazione, database, log.
- Se il file può essere letto da un altro processo in qualsiasi momento.
- Se un errore in scrittura può portare a perdita o corruzione dei dati.
5. Logging e gestione degli errori di accesso
Come gestire correttamente gli errori di accesso?
Quando lavori con i file usa sempre la gestione delle eccezioni (try-catch). Questo permette di:
- Informare correttamente l’utente del problema (ad esempio, «Nessun permesso di scrittura nella cartella»).
- Registrare l’errore nel log per un’analisi successiva.
- Evitare di far terminare l’intera applicazione a causa di un’unica operazione non riuscita.
Esempio: gestione di AccessDeniedException
import java.nio.file.*;
public class FileAccessDemo {
public static void main(String[] args) {
Path file = Paths.get("/etc/shadow"); // esempio per Linux
try {
Files.readAllLines(file);
} catch (AccessDeniedException ade) {
System.err.println("Errore di accesso: nessun permesso di lettura per il file " + file);
// Si può scrivere nel log o proporre all'utente di scegliere un altro file
} catch (Exception e) {
System.err.println("Altro errore: " + e.getMessage());
}
}
}
Logging degli errori
Nelle applicazioni reali usa sistemi di logging (ad esempio, java.util.logging, Log4j, SLF4J). Questo permette di:
- Registrare gli errori con i dettagli (stack trace, ora, utente).
- Analizzare i log per trovare e correggere i problemi.
- Non mostrare all’utente messaggi «spaventosi», ma inviarli solo al log.
Esempio 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("Nessun accesso al file: " + file);
} catch (Exception e) {
logger.log(Level.SEVERE, "Errore durante l'operazione sul file", e);
}
}
}
6. Errori tipici
Errore n. 1: ignorare le eccezioni durante il lavoro con i file.
Non scrivere mai semplicemente Files.write(path, data) senza try-catch — se qualcosa va storto, l’applicazione si arresterà.
Errore n. 2: controllare i permessi senza considerare il TOCTOU.
Non fare affidamento solo su Files.isWritable() e metodi simili. Anche se restituiscono “si può”, l’operazione potrebbe non riuscire. Gestisci sempre le eccezioni (ad esempio, AccessDeniedException).
Errore n. 3: sovrascrivere un file esistente senza backup.
Se il file è importante — crea un backup prima di scrivere oppure usa la scrittura atomica con StandardCopyOption.ATOMIC_MOVE.
Errore n. 4: non eliminare i file temporanei dopo un errore.
Se durante la scrittura atomica qualcosa va storto — il file temporaneo può rimanere. Usa finally e Files.deleteIfExists().
Errore n. 5: non registrare nel log gli errori di accesso.
Se l’applicazione non è riuscita a scrivere o leggere un file — l’utente deve saperlo e tu devi vedere i dettagli nel log. Usa java.util.logging/SLF4J e registra le eccezioni con lo stack trace.
GO TO FULL VERSION