JavaRush /Курси /Go SELF /Директорії даних — os.MkdirAll, права доступу та безпечні...

Директорії даних — os.MkdirAll, права доступу та безпечні значення за замовчуванням

Go SELF
Рівень 39 , Лекція 3
Відкрита

1. Навіщо потрібна директорія даних

Коли ви пишете програму, дуже легко почати думати, що файли «просто існують». Але файлова система влаштована, як реальний світ: якщо ви намагаєтеся поставити коробку на полицю, а шафи немає — коробка просто падає на підлогу (у Go це виглядає як помилка). Тому майже будь-яка програма, яка зберігає дані на диску, спочатку має переконатися, що потрібна директорія існує.

У навчальному застосунку ми будемо вважати, що маємо невеликий CLI-менеджер завдань (умовно назвемо його todo), який зберігає дані у файлі всередині директорії data/todo. Саме в цей момент багато новачків уперше стикаються з помилкою "no such file or directory" і ставлять філософське запитання: «Але ж я вказав шлях! Чому цього недостатньо?»

Шлях — це лише адреса. Адреса не будує дім.

Мініприклад: «каталогу немає» — класика жанру

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Create("data/todo/items.txt")
	if err != nil {
		fmt.Println("create:", err) // create: open data/todo/items.txt: no such file or directory
		return
	}
	defer f.Close()
}

Сама помилка нормальна: ОС чесно каже «я не можу створити файл у директорії, якої не існує».

2. os.MkdirAll: що робить і навіщо потрібен

У програмуванні є корисна лінь: не «я нічого не буду робити», а «я не буду робити одне й те саме вручну сто разів». Створення директорій якраз із цієї серії. Можна створювати директорії по одній, перевіряти, чи існує батьківська, потім — чи існує батьківська тієї батьківської, і десь на третьому рівні ви почнете задумливо дивитися у вікно та згадувати, навіщо взагалі обрали розробку.

os.MkdirAll(path, perm) розв’язує проблему радикально: він створює всю ієрархію директорій, якщо чогось не вистачає. А якщо директорії вже існують — це не катастрофа і не привід панікувати: це штатна ситуація.

Важливо розуміти, що MkdirAll працює саме для директорій. Він не створює файл і не перевіряє, що всередині лежить «правильний формат даних». Він лише гарантує: «директорія є».

Мініприклад: готуємо директорію для даних

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	dir := filepath.Join("data", "todo")
	if err := os.MkdirAll(dir, 0755); err != nil {
		fmt.Println("mkdirall:", err)
		return
	}
	fmt.Println("dir ready:", dir) // dir ready: data/todo
}

Зверніть увагу: ми використовуємо filepath.Join, щоб не склеювати рядки вручну. А потім — MkdirAll, щоб не винаходити велосипед із квадратними колесами.

3. Права доступу та os.FileMode

Права доступу — тема, де новачок зазвичай каже: «Я зрозумів… здається… ні, не зрозумів». Це нормально. Давайте розкладемо все максимально приземлено.

У Unix-подібних системах (Linux, macOS) права на файли та директорії зазвичай задають трьома групами: для власника, для групи, для інших. Кожна група отримує набір прав: читання, запис, виконання. Для директорій «виконання» означає право зайти всередину (так, звучить дивно, але так історично склалося).

У Go права задаються типом os.FileMode. Часто ви пишете числа на кшталт 0644 і 0755. І головний сюрприз: це вісімкові числа. Провідний нуль — не «для краси»: він каже компілятору «рахуй це в base-8».

Невелика таблиця: що зазвичай означають режими

Режим Для чого Що приблизно означає на практиці
0755
директорія власник може все, інші можуть читати й заходити
0700
директорія лише власник може читати й заходити
0644
файл власник читає й пише, інші читають
0600
файл лише власник читає й пише

У межах курсу нам важливі не всі тонкощі, а надійний мінімум: обирати права усвідомлено й не ставити про всяк випадок 0777.

Мініприклад: друкуємо режим як вісімкове число

package main

import (
	"fmt"
	"os"
)

func main() {
	var dm os.FileMode = 0755
	var fm os.FileMode = 0644

	fmt.Printf("dir=%#o file=%#o\n", dm, fm) // dir=0755 file=0644
}

Формат %#o зручний тим, що він показує число у вісімковому вигляді й зберігає «образ» 0755, а не просто 493 (що теж математично правильно, але дратує людей).

4. Безпечні значення за замовчуванням і режими

Є особливий різновид програмістської магії: «я поставлю максимальні права, щоб точно працювало». Працювати справді буде… але ви відкриваєте дані всім навколо. Це як зберігати ключі від квартири під килимком і потім дивуватися, що в холодильнику хтось їсть вашу піцу.

У навчальних завданнях це ще може зійти. У реальному житті — це джерело проблем: випадковий витік даних, неочікуваний доступ іншого користувача, дивні збої в CI та раптове «чому в мене на сервері зʼявився файл, який я не створював».

Як безпечних значень за замовчуванням ми дотримуватимемося простого правила.

  • Якщо дані звичайні й не секретні, то директорія 0755, файл 0644.
  • Якщо дані приватні (токени, ключі, персональні записи, щось чутливе), то директорія 0700, файл 0600.

І навіть якщо ви поки що пишете навчальний todo, корисно формувати звичку: права — це частина контракту вашого застосунку.

Мініприклад: «публічна» і «приватна» директорії

package main

import (
	"fmt"
	"os"
)

func main() {
	if err := os.MkdirAll("data/public", 0755); err != nil {
		fmt.Println("mkdir public:", err)
		return
	}
	if err := os.MkdirAll("data/private", 0700); err != nil {
		fmt.Println("mkdir private:", err)
		return
	}
}

Так, це лише два рядки. Але за змістом — це «ми вирішили, хто і що може бачити».

Директорія ≠ файл: про execute-біт

У новачків часто з’являється думка: «Ну, файл 0644 — нормально. Значить, і директорія нехай буде 0644». І тут пастка: директорія — не файл.

Файл читають і пишуть. Директорія — це контейнер. Щоб користуватися директорією, потрібно мати право зайти всередину й переглядати або створювати елементи. Тому для директорій майже завжди використовують режими з «виконанням», наприклад 0755 або 0700. Якщо ви поставите директорії 0644, отримаєте дивні ефекти: директорія ніби «видима», але «зайти в неї» не можна.

Це схоже на ситуацію: двері в під’їзд прозорі, ви бачите, що всередині, але ручку знято — увійти не можна. З архітектурної точки зору це дуже творчо, але жити так важко.

Мініприклад: правильний «набір режимів» як константи

package main

import "os"

const (
	dataDirMode  os.FileMode = 0755
	privateMode  os.FileMode = 0700
	dataFileMode os.FileMode = 0644
)

Ми просто фіксуємо стандарт проєкту. Це робить код читабельнішим: ви не гадаєте, чому тут 0755, а там 0700.

5. Протокол: спочатку директорія, потім файл

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

Це не «бюрократія». Це спосіб зробити поведінку стабільною: ви зможете запускати програму на чистій машині, у CI, у новому каталозі проєкту — і вона сама підготує структуру.

Зручно мислити так:

зібрали шлях → переконалися, що директорія існує → відкрили/створили файл

Можна уявити це як мінісхему:

flowchart TD
    A[Є шлях до файлу даних] --> B[Обчислити директорію файлу]
    B --> C{Директорія існує?}
    C -->|так| D[Відкрити або створити файл]
    C -->|ні| E["os.MkdirAll(dir, mode)"]
    E --> D

Головна думка: ми не сподіваємося на оточення. Ми самі створюємо мінімально потрібні умови.

6. Реалізація: ensureDataDir і помилки

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

І тут ми знову користуємося звичкою Go: помилки — це значення, їх зручно повертати вгору й додавати контекст по дорозі. Саме тому обгортання (fmt.Errorf("...: %w", err)) таке цінне: воно робить помилку зрозумілішою, не втрачаючи її причину.

Функція, яка знає, де живуть дані

package main

import "path/filepath"

func dataDir() string {
	return filepath.Join("data", "todo")
}

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

ensureDataDir: створюємо директорію, якщо потрібно

package main

import (
	"fmt"
	"os"
)

func ensureDataDir(dir string) error {
	if err := os.MkdirAll(dir, 0755); err != nil {
		return fmt.Errorf("не вдалося підготувати директорію даних %q: %w", dir, err)
	}
	return nil
}

Зверніть увагу на три речі.

  • По-перше, режим 0755 тут — усвідомлене значення за замовчуванням для «звичайних даних».
  • По-друге, ми повертаємо помилку, а не друкуємо її всередині. Так код стає придатним до повторного використання.
  • По-третє, ми додаємо контекст ("не вдалося підготувати директорію даних ..."), але зберігаємо причину через %w.

Збираємо шлях до файла із завданнями

package main

import "path/filepath"

func itemsFilePath() string {
	return filepath.Join(dataDir(), "items.txt")
}

Тут логіка та сама: шлях будується через filepath.Join, без ручного склеювання.

Використовуємо в main: спочатку директорія, потім файл

package main

import (
	"fmt"
	"os"
)

func main() {
	if err := ensureDataDir(dataDir()); err != nil {
		fmt.Println("error:", err)
		return
	}

	f, err := os.OpenFile(itemsFilePath(), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		fmt.Println("openfile:", err)
		return
	}
	defer f.Close()

	_, _ = f.WriteString("buy milk\n")
}

Тут ми вже бачимо цілісний протокол: директорія готова — значить, файл можна створювати або відкривати. Файл ми відкрили в режимі дописування (O_APPEND), а режим 0644 обрали як безпечне значення за замовчуванням для звичайного текстового файла.

7. Типові помилки під час роботи з директоріями та правами

Помилка № 1: створювати файл, не створюючи директорію.
Це найчастіший випадок. Код виглядає логічно, але ОС не зобов’язана здогадуватися, яку директорію ви мали на увазі. Рішення просте: перед будь-яким Create/OpenFile/WriteFile переконайтеся, що директорію підготовлено через os.MkdirAll.

Помилка № 2: писати 755 замість 0755 і отримувати «магічні» числа.
Без провідного нуля число стає десятковим. У результаті ви задаєте не ті права, які думаєте. Це одна з тих помилок, які особливо неприємні тим, що код компілюється й «майже працює», а потім починає поводитися дивно на чужій машині.

Помилка № 3: ставити директоріям режим 0644 і дивуватися, що «всередину не заходить».
Директорія потребує права «входу» (execute). Тому для директорій зазвичай використовуються 0755 або 0700. Якщо переплутати, ви отримаєте загадкові помилки доступу навіть у власника.

Помилка № 4: лікувати все через 0777, бо «так точно не буде permission denied».
Це справді може прибрати одну помилку, але створює іншу: ви робите дані доступними всім. Навіть для навчального застосунку корисно тренувати дисципліну й обирати мінімально достатні права.

Помилка № 5: друкувати помилку без контексту й потім не розуміти, де вона сталася.
Якщо ви повертаєте назовні просто err, то у великій програмі лог виглядатиме як «permission denied» без указання, що саме ви робили і з яким шляхом. Обгортання через fmt.Errorf("...: %w", err) допомагає зробити помилки придатними для інженерного аналізу, зберігаючи першопричину помилки.

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