diff --git a/.gitignore b/.gitignore
index 0fa92db..678b454 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ thumbs/*
photos/*
!photos/.gitkeep
data/last_indexed.txt
+data/sort.json
# OS/editor
.DS_Store
diff --git a/README.md b/README.md
index b524550..a2aa9dd 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,17 @@ BRANCH=master bash scripts/deploy.sh
## Админка загрузки (по токену)
+Новый контур на MySQL (этап 1):
+
+- `admin-mysql.php?token=...`
+
+Что уже есть в MySQL-админке:
+- создание разделов,
+- загрузка фото "до" + опционально "после",
+- запись в таблицы `sections`, `photos`, `photo_files`,
+- просмотр разделов и загруженных фото.
+
+
Админка использует тот же `token`, что и `deploy.php`, из файла `deploy-config.php`.
Ссылка входа:
diff --git a/admin-mysql.php b/admin-mysql.php
new file mode 100644
index 0000000..0f10800
--- /dev/null
+++ b/admin-mysql.php
@@ -0,0 +1,189 @@
+getMessage());
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $action = (string)($_POST['action'] ?? '');
+ try {
+ if ($action === 'create_section') {
+ $name = trim((string)($_POST['name'] ?? ''));
+ $sort = (int)($_POST['sort_order'] ?? 1000);
+ if ($name === '') {
+ throw new RuntimeException('Название раздела пустое');
+ }
+ sectionCreate($name, $sort);
+ $message = 'Раздел создан';
+ }
+
+ if ($action === 'upload_photo') {
+ $sectionId = (int)($_POST['section_id'] ?? 0);
+ $codeName = trim((string)($_POST['code_name'] ?? ''));
+ $sortOrder = (int)($_POST['sort_order'] ?? 1000);
+ $description = trim((string)($_POST['description'] ?? ''));
+ $description = $description !== '' ? $description : null;
+
+ if ($sectionId < 1) throw new RuntimeException('Выбери раздел');
+ if ($codeName === '') throw new RuntimeException('Укажи код фото (например АВФ1)');
+ if (!isset($_FILES['before'])) throw new RuntimeException('Файл "до" обязателен');
+
+ $section = sectionById($sectionId);
+ if (!$section) throw new RuntimeException('Раздел не найден');
+
+ $photoId = photoCreate($sectionId, $codeName, $description, $sortOrder);
+
+ $before = saveImageUpload($_FILES['before'], $codeName, 'before', $sectionId);
+ photoFileUpsert($photoId, 'before', $before['path'], $before['mime'], $before['size']);
+
+ if (isset($_FILES['after']) && (int)($_FILES['after']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
+ $after = saveImageUpload($_FILES['after'], $codeName . 'р', 'after', $sectionId);
+ photoFileUpsert($photoId, 'after', $after['path'], $after['mime'], $after['size']);
+ }
+
+ $message = 'Фото добавлено';
+ }
+ } catch (Throwable $e) {
+ $errors[] = $e->getMessage();
+ }
+}
+
+$sections = sectionsAll();
+$activeSectionId = (int)($_GET['section_id'] ?? ($sections[0]['id'] ?? 0));
+$photos = $activeSectionId > 0 ? photosBySection($activeSectionId) : [];
+
+function h(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
+function assetUrl(string $path): string { $f=__DIR__ . '/' . ltrim($path,'/'); $v=is_file($f)?(string)filemtime($f):(string)time(); return $path . '?v=' . rawurlencode($v); }
+
+function saveImageUpload(array $file, string $baseName, string $kind, int $sectionId): array
+{
+ $allowedMime = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif'];
+ $err = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE);
+ if ($err !== UPLOAD_ERR_OK) throw new RuntimeException("Ошибка загрузки ({$kind})");
+ $size = (int)($file['size'] ?? 0);
+ if ($size < 1 || $size > MAX_UPLOAD_BYTES) throw new RuntimeException("Файл {$kind}: превышен лимит 3 МБ");
+
+ $tmp = (string)($file['tmp_name'] ?? '');
+ if (!is_uploaded_file($tmp)) throw new RuntimeException("Файл {$kind}: некорректный источник");
+
+ $mime = mime_content_type($tmp) ?: '';
+ if (!isset($allowedMime[$mime])) throw new RuntimeException("Файл {$kind}: недопустимый mime {$mime}");
+
+ $safeBase = preg_replace('/[^\p{L}\p{N}._-]+/u', '_', $baseName) ?? 'photo';
+ $safeBase = trim($safeBase, '._-');
+ if ($safeBase === '') $safeBase = 'photo';
+
+ $ext = $allowedMime[$mime];
+ $dir = __DIR__ . '/photos/section_' . $sectionId;
+ if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) throw new RuntimeException('Не удалось создать папку раздела');
+
+ $final = uniqueName($dir, $safeBase, $ext);
+ $dest = $dir . '/' . $final;
+ if (!move_uploaded_file($tmp, $dest)) throw new RuntimeException('Не удалось сохранить файл');
+
+ return [
+ 'path' => 'photos/section_' . $sectionId . '/' . $final,
+ 'mime' => $mime,
+ 'size' => $size,
+ ];
+}
+
+function uniqueName(string $dir, string $base, string $ext): string
+{
+ $i = 0;
+ do {
+ $name = $i === 0 ? "{$base}.{$ext}" : "{$base}_{$i}.{$ext}";
+ $i++;
+ } while (is_file($dir . '/' . $name));
+ return $name;
+}
+?>
+
+
+
+
+ Админка (MySQL)
+
+
+
+
+
Админка (MySQL, этап 1)
+
← в галерею
+
= h($message) ?>
+
= h($e) ?>
+
+
+
+
+
+
+
+
+
+ Фото раздела
+ | ID | Код | Превью | Описание | Порядок |
+
+
+ | = (int)$p['id'] ?> |
+ = h((string)$p['code_name']) ?> |
+ $p['before_path']) ?>) |
+ = h((string)($p['description'] ?? '')) ?> |
+ = (int)$p['sort_order'] ?> |
+
+
+
+
+
+
diff --git a/lib/db_gallery.php b/lib/db_gallery.php
new file mode 100644
index 0000000..53dfd57
--- /dev/null
+++ b/lib/db_gallery.php
@@ -0,0 +1,64 @@
+query('SELECT s.*, (SELECT COUNT(*) FROM photos p WHERE p.section_id=s.id) AS photos_count FROM sections s ORDER BY s.sort_order, s.name')->fetchAll();
+}
+
+function sectionById(int $id): ?array
+{
+ $st = db()->prepare('SELECT * FROM sections WHERE id=:id');
+ $st->execute(['id' => $id]);
+ $row = $st->fetch();
+ return $row ?: null;
+}
+
+function sectionCreate(string $name, int $sort): void
+{
+ $st = db()->prepare('INSERT INTO sections(name, sort_order) VALUES (:name,:sort)');
+ $st->execute(['name' => $name, 'sort' => $sort]);
+}
+
+function photosBySection(int $sectionId): array
+{
+ $sql = 'SELECT p.*, bf.file_path AS before_path, af.file_path AS after_path
+ FROM photos p
+ LEFT JOIN photo_files bf ON bf.photo_id=p.id AND bf.kind="before"
+ LEFT JOIN photo_files af ON af.photo_id=p.id AND af.kind="after"
+ WHERE p.section_id=:sid
+ ORDER BY p.sort_order, p.id DESC';
+ $st = db()->prepare($sql);
+ $st->execute(['sid' => $sectionId]);
+ return $st->fetchAll();
+}
+
+function photoCreate(int $sectionId, string $codeName, ?string $description, int $sortOrder): int
+{
+ $st = db()->prepare('INSERT INTO photos(section_id, code_name, description, sort_order) VALUES (:sid,:code,:descr,:sort)');
+ $st->execute([
+ 'sid' => $sectionId,
+ 'code' => $codeName,
+ 'descr' => $description,
+ 'sort' => $sortOrder,
+ ]);
+ return (int)db()->lastInsertId();
+}
+
+function photoFileUpsert(int $photoId, string $kind, string $path, string $mime, int $size): void
+{
+ $sql = 'INSERT INTO photo_files(photo_id, kind, file_path, mime_type, size_bytes)
+ VALUES (:pid,:kind,:path,:mime,:size)
+ ON DUPLICATE KEY UPDATE file_path=VALUES(file_path), mime_type=VALUES(mime_type), size_bytes=VALUES(size_bytes), updated_at=CURRENT_TIMESTAMP';
+ $st = db()->prepare($sql);
+ $st->execute([
+ 'pid' => $photoId,
+ 'kind' => $kind,
+ 'path' => $path,
+ 'mime' => $mime,
+ 'size' => $size,
+ ]);
+}