1. Uprawnienia dostępu w systemie operacyjnym
Gdy pracujesz z plikami i folderami, ważne jest, by pamiętać: system operacyjny (SO) chroni je za pomocą systemu uprawnień dostępu. Oznacza to, że nie każdy program (i nie każdy użytkownik) może czytać, zmieniać lub usuwać dowolny plik.
POSIX (Linux, macOS i inne)
W systemach POSIX (Unix, Linux, macOS) każdy plik i folder ma uprawnienia dla trzech kategorii:
- Właściciel (user)
- Grupa (group)
- Pozostali (others)
Dla każdej kategorii ustala się trzy typy uprawnień:
- r — read (odczyt)
- w — write (zapis)
- x — execute (wykonanie)
Przykład:
-rw-r--r--
To oznacza: właściciel może czytać i pisać, pozostali — tylko czytać.
Windows
W Windows uprawnienia dostępu określa system ACL (Access Control List) — listy zezwoleń dla użytkowników i grup. Można elastycznie konfigurować, kto i co może robić z plikiem lub folderem (czytać, pisać, modyfikować, uruchamiać itd.).
Ważne:
Program Java działa na plikach w ramach uprawnień użytkownika, pod którym został uruchomiony. Jeśli użytkownik nie ma praw do pliku — program również nie będzie mógł go odczytać ani zmienić.
2. Wyjątek AccessDeniedException i jego przyczyny
Podczas pracy z plikami w Javie (szczególnie przez NIO API) możesz napotkać wyjątek:
java.nio.file.AccessDeniedException
Ten wyjątek jest rzucany, jeśli twój program nie ma uprawnień do wykonania operacji na pliku lub katalogu.
Główne przyczyny:
- Brak uprawnień do odczytu pliku (np. plik jest chroniony przed odczytem).
- Brak uprawnień do zapisu do pliku lub folderu (np. próbujesz zapisać w folderze systemowym).
- Brak uprawnień do wykonywania pliku (istotne przy uruchamianiu programów).
- Folder lub plik są chronione przed zmianami (np. tylko do odczytu).
- Plik lub folder są zajęte przez inny proces (szczególnie często w Windows).
Przykład:
Path path = Paths.get("/etc/shadow"); // systemowy plik Linux
Files.readAllLines(path); // AccessDeniedException!
Co robić?
- Sprawdź uprawnienia dostępu do pliku/folderu.
- Uruchom program jako użytkownik z odpowiednimi uprawnieniami.
- Nie próbuj pisać do katalogów systemowych bez potrzeby.
3. Sprawdzanie uprawnień w Javie: metody Files.isReadable(), isWritable(), isExecutable()
Java udostępnia wygodne metody sprawdzania uprawnień do pliku lub folderu:
Path path = Paths.get("example.txt");
System.out.println(Files.isReadable(path)); // true, jeśli można czytać
System.out.println(Files.isWritable(path)); // true, jeśli można pisać
System.out.println(Files.isExecutable(path)); // true, jeśli można uruchamiać
Te metody pokazują, jak system widzi twoje uprawnienia w danym momencie.
Ale!
One nie gwarantują, że operacja rzeczywiście się powiedzie. Przyczyny mogą być różne: plik może być zablokowany przez inny program, uprawnienia mogły się zmienić po sprawdzeniu, na dyskach sieciowych wyniki zależą od serwera, a czasem system operacyjny zgłasza jedno, a stosuje inne ograniczenia.
Dlatego w Javie lepiej najpierw sprawdzić uprawnienia, a następnie opakować rzeczywistą operację odczytu lub zapisu w try-catch — to bardziej niezawodne.
Problem TOCTOU (Time Of Check To Time Of Use)
Z tym bezpośrednio wiąże się sytuacja TOCTOU, gdy między sprawdzeniem uprawnienia a samą operacją coś się zmienia. Na przykład:
- Sprawdziłeś, że plik jest dostępny do zapisu (isWritable).
- W tym momencie inny proces lub użytkownik zmienił uprawnienia — teraz plik jest chroniony.
- Próbujesz zapisać dane — otrzymujesz AccessDeniedException.
Wniosek:
Sprawdzenie uprawnień daje jedynie wskazówkę o bieżącym stanie, ale nie gwarantuje powodzenia operacji. Zawsze obsługuj wyjątki podczas pracy z plikami.
4. Zasada „bezpiecznego zapisu” (atomic write)
Po co bezpieczny (atomowy) sposób zapisu?
Czasem podczas zapisu pliku może dojść do awarii: program się wyłączy, zabraknie prądu, skończy się miejsce na dysku... W rezultacie plik może okazać się uszkodzony lub częściowo zapisany. To szczególnie niebezpieczne dla ważnych danych (np. ustawień, baz danych, dokumentów).
Bezpieczny zapis — to sposób zapewniający, że plik albo zostanie w pełni zaktualizowany, albo pozostanie w poprzednim stanie. Takie podejście nazywa się zapisem atomowym (atomic write).
Jak zaimplementować bezpieczny zapis w Javie?
Wzorzec:
- Zapisujemy dane do pliku tymczasowego (zwykle w tym samym folderze).
- Jeśli zapis się powiódł — atomowo przenosimy plik tymczasowy w miejsce pliku docelowego (zastępując go).
Dlaczego to działa?
Operacja przeniesienia pliku (rename/move) w obrębie jednego systemu plików jest zwykle atomowa: albo plik zostanie w pełni zastąpiony, albo nie. Jeśli coś pójdzie nie tak — plik główny pozostaje nienaruszony.
Przykład kodu: bezpieczny zapis pliku
import java.nio.file.*;
public class SafeWriteDemo {
public static void safeWrite(Path target, byte[] data) throws Exception {
// 1. Tworzymy plik tymczasowy w tym samym folderze
Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");
try {
// 2. Zapisujemy dane do pliku tymczasowego
Files.write(tempFile, data);
// 3. Atomowo przenosimy plik tymczasowy w miejsce docelowego
Files.move(
tempFile,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} finally {
// Jeśli coś poszło nie tak — usuwamy plik tymczasowy
Files.deleteIfExists(tempFile);
}
}
public static void main(String[] args) throws Exception {
Path file = Paths.get("important.txt");
byte[] content = "Bardzo ważne dane".getBytes();
safeWrite(file, content);
System.out.println("Plik zapisano bezpiecznie!");
}
}
Zwróć uwagę:
- Używamy Files.createTempFile() do utworzenia pliku tymczasowego.
- Do przenoszenia używamy opcji ATOMIC_MOVE — gwarantuje to atomowość (jeśli jest wspierana przez system operacyjny i system plików).
- Jeśli coś poszło nie tak — plik tymczasowy jest usuwany (Files.deleteIfExists).
Kiedy jest to szczególnie ważne?
- Podczas pracy z plikami konfiguracyjnymi, bazami danych, dziennikami.
- Gdy plik może być w każdej chwili odczytywany przez inny proces.
- Gdy błąd podczas zapisu może prowadzić do utraty lub uszkodzenia danych.
5. Logowanie i obsługa błędów dostępu
Jak prawidłowo obsługiwać błędy dostępu?
Podczas pracy z plikami zawsze używaj obsługi wyjątków (try-catch). To pozwoli:
- Poprawnie poinformować użytkownika o problemie (np. „Brak uprawnień do zapisu w folderze”).
- Zapisać błąd do logu do późniejszej analizy.
- Nie „wykładać” całej aplikacji z powodu jednej nieudanej operacji.
Przykład: obsługa AccessDeniedException
import java.nio.file.*;
public class FileAccessDemo {
public static void main(String[] args) {
Path file = Paths.get("/etc/shadow"); // przykład dla Linuksa
try {
Files.readAllLines(file);
} catch (AccessDeniedException ade) {
System.err.println("Błąd dostępu: brak uprawnień do odczytu pliku " + file);
// Można zapisać do logu lub zaproponować użytkownikowi wybór innego pliku
} catch (Exception e) {
System.err.println("Inny błąd: " + e.getMessage());
}
}
}
Logowanie błędów
W rzeczywistych aplikacjach używaj systemów logowania (np. java.util.logging, Log4j, SLF4J). To pozwoli:
- Zapisywać błędy ze szczegółami (stos wywołań, czas, użytkownik).
- Analizować logi, aby znaleźć i usuwać problemy.
- Nie pokazywać użytkownikowi „strasznych” komunikatów, a wyświetlać je tylko w logu.
Przykład z logowaniem:
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("Brak dostępu do pliku: " + file);
} catch (Exception e) {
logger.log(Level.SEVERE, "Błąd podczas pracy z plikiem", e);
}
}
}
6. Typowe błędy
Błąd nr 1: Ignorowanie wyjątków podczas pracy z plikami.
Nigdy nie wywołuj po prostu Files.write(path, data) bez try-catch — jeśli coś pójdzie nie tak, program się wywróci.
Błąd nr 2: Sprawdzanie uprawnień bez uwzględnienia TOCTOU.
Nie polegaj wyłącznie na Files.isWritable() i podobnych metodach. Nawet jeśli zwracają „można”, operacja może się nie powieść. Zawsze obsługuj wyjątki (np. AccessDeniedException).
Błąd nr 3: Zapis nad istniejący plik bez kopii zapasowej.
Jeśli plik jest ważny — wykonaj kopię zapasową przed zapisem lub użyj zapisu atomowego z StandardCopyOption.ATOMIC_MOVE.
Błąd nr 4: Nieusuwanie plików tymczasowych po awarii.
Jeśli podczas zapisu atomowego coś poszło nie tak — plik tymczasowy może pozostać. Używaj finally i Files.deleteIfExists().
Błąd nr 5: Brak logowania błędów dostępu.
Jeśli program nie mógł zapisać lub odczytać pliku — użytkownik powinien się o tym dowiedzieć, a ty — zobaczyć szczegóły w logu. Używaj java.util.logging/SLF4J i zapisuj wyjątki wraz ze stosem wywołań.
GO TO FULL VERSION