CodeGym /Kursy /JAVA 25 SELF /Uprawnienia i dostęp do systemu plików

Uprawnienia i dostęp do systemu plików

JAVA 25 SELF
Poziom 38, Lekcja 3
Dostępny

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:

  1. Sprawdziłeś, że plik jest dostępny do zapisu (isWritable).
  2. W tym momencie inny proces lub użytkownik zmienił uprawnienia — teraz plik jest chroniony.
  3. 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ń.

1
Zadanie
JAVA 25 SELF, poziom 38, lekcja 3
Niedostępne
Dostęp do tajnych rysunków: sprawdzenie uprawnień do podglądu 👁️
Dostęp do tajnych rysunków: sprawdzenie uprawnień do podglądu 👁️
1
Zadanie
JAVA 25 SELF, poziom 38, lekcja 3
Niedostępne
Przygotowanie magazynu na nowe dostawy: sprawdzenie zezwolenia na przechowywanie 📦
Przygotowanie magazynu na nowe dostawy: sprawdzenie zezwolenia na przechowywanie 📦
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION