Черновики и двадцать версий: история правок без базы данных
Как сделать откат правок и невидимые черновики на голых файлах: копия перед записью, обрезка по числу, восстановление наложением.
Две вещи, которые ждёшь от любой CMS: возможность писать втайне и возможность отменить. Обе делаются на файлах, без таблиц и без библиотек — и обе имеют неочевидные детали, на которых легко ошибиться.
Черновик — это не флаг в интерфейсе
Черновик у меня означает буквально: адрес не отвечает никому, кроме автора. Не «страница есть, но скрыта в списке», а 404 для постороннего и полный вид для того, кто вошёл в панель.
Важно, что фильтрует не хранилище. Хранилище отдаёт всё — панели нужны и черновики. Решение «показывать ли» принимает страница, и это правильное место: одно и то же хранилище обслуживает и сайт, и админку.
История: копия перед записью
Никакого журнала изменений. Перед тем как записать новую версию, старая уходит в отдельный каталог под именем-временем. Двадцать последних, остальные удаляются. Пост — это несколько килобайт, так что двадцать штук не стоят ничего.
1/** ISO, с приведёнными к дефисам символами, которые не любит файловая система.
2 Сортируется по времени как строка — на это и опирается список версий. */
3function idFor(when: Date) {
4 return when.toISOString().replace(/[:.]/g, "-");
5}
6
7export async function keepVersion(kind: Kept, slug: string, document: unknown) {
8 const folder = historyDir(kind, slug);
9 await mkdir(folder, { recursive: true });
10
11 const file = join(folder, `${idFor(new Date())}.json`);
12 const temporary = `${file}.${process.pid}.tmp`;
13 await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, "utf8");
14 await rename(temporary, file);
15
16 await prune(kind, slug);
17}
Почему версия накладывается, а не пишется как есть
Версия, сохранённая до появления поля, ничего про это поле не знает. Записать её напрямую значит оставить документ без поля до следующей правки — то есть тихо сломать страницу спустя недели после отката.
Поэтому восстановление накладывает версию на то, чем документ является сейчас. И отдельно: адрес не принадлежит версии. Slug — это место файла на диске и часть адреса страницы; вернуть старый slug из истории означало бы переименовать сущность, чего кнопка «вернуть» не обещала.
Что должно быть у отката, чтобы им можно было пользоваться
- ✓Из откаченного состояния можно откатиться обратноуже так
- ✓Версию можно посмотреть до того, как применятьуже так
- ✓Восстановление не меняет адрес сущностиуже так
- ✓Число версий ограничено, иначе каталог растёт молчауже так
- ✓Удаление сущности убирает и её историюуже так
