JavaRush /Курси /JAVA 25 SELF /Права та доступ до файлової системи

Права та доступ до файлової системи

JAVA 25 SELF
Рівень 38 , Лекція 3
Відкрита

1. Права доступу в ОС

Коли ви працюєте з файлами й теками, важливо пам’ятати: операційна система (ОС) захищає їх за допомогою системи прав доступу. Це означає, що не будь-яка програма (і не будь-який користувач) може читати, змінювати або видаляти будь-який файл.

POSIX (Linux, macOS та ін.)

У системах POSIX (Unix, Linux, macOS) кожен файл і тека мають права доступу для трьох категорій:

  • Власник (user)
  • Група (group)
  • Інші (others)

Для кожної категорії задаються три типи прав:

  • r — read (читання)
  • w — write (запис)
  • x — execute (виконання)

Приклад:
-rw-r--r--
Це означає: власник може читати й писати, інші — лише читати.

Windows

У Windows права доступу визначаються через систему ACL (Access Control List) — списки дозволів для користувачів і груп. Тут можна гнучко налаштовувати, хто і що може робити з файлом або текою (читати, писати, змінювати, запускати тощо).

Важливо:
Java-програма працює з файлами в межах прав користувача, під яким вона запущена. Якщо користувач не має прав на файл — програма також не зможе його прочитати або змінити.

2. Виняток AccessDeniedException і його причини

Коли ви працюєте з файлами в Java (особливо через NIO API), ви можете зіткнутися з винятком:

java.nio.file.AccessDeniedException

Цей виняток викидається, якщо у вашої програми немає прав на виконання операції з файлом або директорією.

Основні причини:

  • Немає прав на читання файла (наприклад, файл захищений від читання).
  • Немає прав на запис у файл або теку (наприклад, намагаєтеся записати в системну теку).
  • Немає прав на виконання файла (актуально для запуску програм).
  • Тека або файл захищені від змін (наприклад, лише для читання).
  • Файл або тека зайняті іншим процесом (особливо часто у Windows).

Приклад:

Path path = Paths.get("/etc/shadow"); // системний файл Linux
Files.readAllLines(path); // AccessDeniedException!

Що робити?

  • Перевірте права доступу до файла/теки.
  • Запустіть програму від імені користувача з потрібними правами.
  • Не намагайтеся писати в системні директорії без необхідності.

3. Перевірка прав у Java: методи Files.isReadable(), isWritable(), isExecutable()

Java надає зручні методи для перевірки прав на файл або теку:

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

System.out.println(Files.isReadable(path));   // true, якщо можна читати
System.out.println(Files.isWritable(path));   // true, якщо можна писати
System.out.println(Files.isExecutable(path)); // true, якщо можна запускати

Ці методи показують, як система бачить ваші права на даний момент.

Але!
Вони не гарантують, що операція справді завершиться успішно. Причини можуть бути різними: файл може бути заблокований іншою програмою, права могли змінитися після перевірки, на мережевих дисках результати залежать від сервера, а іноді ОС повідомляє одне, а на практиці застосовує інші обмеження.

Тому в Java краще спочатку перевірити права, а потім обгорнути реальну операцію читання чи запису в try-catch — це надійніше.

Проблема TOCTOU (Time Of Check To Time Of Use)

З цим безпосередньо пов’язана ситуація TOCTOU, коли між перевіркою права й самою операцією щось змінюється. Наприклад:

  1. Ви перевірили, що файл доступний для запису (isWritable).
  2. У цей момент інший процес або користувач змінив права — тепер файл захищено.
  3. Ви намагаєтеся записати дані — отримуєте AccessDeniedException.

Висновок:
Перевірка прав дає лише підказку про поточний стан, але не гарантує успішну операцію. Завжди обробляйте винятки під час роботи з файлами.

4. Принцип «безпечного запису» (atomic write)

Навіщо потрібен безпечний (атомарний) спосіб запису?

Іноді під час запису файла може статися збій: програма впала, відключили електрику, не вистачило місця на диску... У результаті файл може виявитися пошкодженим або частково записаним. Це особливо небезпечно для важливих даних (наприклад, налаштувань, бази даних, документів).

Безпечний запис — це спосіб гарантувати, що файл або повністю оновлено, або залишився у попередньому стані. Такий підхід називають атомарним записом (atomic write).

Як реалізувати безпечний запис у Java?

Шаблон:

  • Записуємо дані у тимчасовий файл (зазвичай у тій самій теці).
  • Якщо запис пройшов успішно — атомарно переміщуємо тимчасовий файл на місце основного (замінюючи його).

Чому це працює?
Операція переміщення файла (rename/move) у межах однієї файлової системи зазвичай атомарна: або файл повністю замінено, або ні. Якщо щось пішло не так — основний файл не зачеплено.

Приклад коду: безпечний запис файла

import java.nio.file.*;

public class SafeWriteDemo {
    public static void safeWrite(Path target, byte[] data) throws Exception {
        // 1. Створюємо тимчасовий файл у тій самій теці
        Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");

        try {
            // 2. Записуємо дані у тимчасовий файл
            Files.write(tempFile, data);

            // 3. Атомарно переміщуємо тимчасовий файл на місце основного
            Files.move(
                tempFile,
                target,
                StandardCopyOption.REPLACE_EXISTING,
                StandardCopyOption.ATOMIC_MOVE
            );
        } finally {
            // Якщо щось пішло не так — видаляємо тимчасовий файл
            Files.deleteIfExists(tempFile);
        }
    }

    public static void main(String[] args) throws Exception {
        Path file = Paths.get("important.txt");
        byte[] content = "Дуже важливі дані".getBytes();

        safeWrite(file, content);
        System.out.println("Файл записано безпечно!");
    }
}

Зверніть увагу:

  • Використовуємо Files.createTempFile() для створення тимчасового файла.
  • Для переміщення використовуємо опцію ATOMIC_MOVE — це гарантує атомарність (якщо підтримується ОС і файловою системою).
  • Якщо щось пішло не так — тимчасовий файл видаляється (Files.deleteIfExists).

Коли це особливо важливо?

  • Під час роботи з конфігураційними файлами, базами даних, журналами.
  • Якщо файл може бути прочитаний іншим процесом будь-якої миті.
  • Якщо збій під час запису може призвести до втрати або пошкодження даних.

5. Логування та обробка помилок доступу

Як правильно обробляти помилки доступу?

Під час роботи з файлами завжди використовуйте обробку винятків (try-catch). Це дозволить:

  • Коректно повідомити користувачеві про проблему (наприклад, «Немає прав на запис у теку»).
  • Записати помилку в лог для подальшого аналізу.
  • Не завершувати роботу всієї програми через одну невдалу операцію.

Приклад: обробка AccessDeniedException

import java.nio.file.*;

public class FileAccessDemo {
    public static void main(String[] args) {
        Path file = Paths.get("/etc/shadow"); // приклад для Linux

        try {
            Files.readAllLines(file);
        } catch (AccessDeniedException ade) {
            System.err.println("Помилка доступу: немає прав на читання файла " + file);
            // Можна записати в лог або запропонувати користувачеві вибрати інший файл
        } catch (Exception e) {
            System.err.println("Інша помилка: " + e.getMessage());
        }
    }
}

Логування помилок

У реальних застосунках використовуйте системи логування (наприклад, java.util.logging, Log4j, SLF4J). Це дозволить:

  • Записувати помилки з подробицями (стек викликів, час, користувач).
  • Аналізувати логи для пошуку та усунення проблем.
  • Не показувати користувачеві «страшні» повідомлення, а виводити їх лише в лог.

Приклад із логуванням:

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("Немає доступу до файла: " + file);
        } catch (Exception e) {
            logger.log(Level.SEVERE, "Помилка під час роботи з файлом", e);
        }
    }
}

6. Типові помилки

Помилка № 1: Ігнорування винятків під час роботи з файлами.
Ніколи не пишіть просто Files.write(path, data) без try-catch — якщо щось піде не так, програма впаде.

Помилка № 2: Перевірка прав без урахування TOCTOU.
Не покладайтеся лише на Files.isWritable() і подібні методи. Навіть якщо вони повертають «можна», операція може не відбутися. Завжди обробляйте винятки (наприклад, AccessDeniedException).

Помилка № 3: Запис «поверх» існуючого файла без резервної копії.
Якщо файл важливий — робіть резервну копію перед записом або використовуйте атомарний запис з StandardCopyOption.ATOMIC_MOVE.

Помилка № 4: Не видаляєте тимчасові файли після збою.
Якщо під час атомарного запису щось пішло не так — тимчасовий файл може лишитися. Використовуйте finally і Files.deleteIfExists().

Помилка № 5: Не логувати помилки доступу.
Якщо програма не змогла записати або прочитати файл — користувач має про це дізнатися, а ви — побачити подробиці в журналі. Використовуйте java.util.logging/SLF4J і фіксуйте винятки зі стеком викликів.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ