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)

+

← в галерею

+
+
+ +
+
+

Создать раздел

+
+ +

+

+ +
+
+ +
+

Добавить фото

+
+ +

+

+

+

+

Фото до:

+

Фото после (опционально):

+ +
+
+ +
+

Разделы

+ + + + +
IDНазваниеПорядокФото
+
+ +
+

Фото раздела

+ + + + + + + + + + +
IDКодПревьюОписаниеПорядок
+
+
+
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, + ]); +}