feat : DATA_PATH configurable, DataGit auto-commit, UpdateChecker branche (v1.4.0)
- DATA_PATH : chemin /data hors document root, configurable via .env (fallback sur BASE_PATH/data si absent) - DataGit : auto-commit git sur toutes les écritures articles/livres (create, update, delete, meta, tags, fichiers, liens…) sauf autosave - UpdateChecker : getBranch() / getLastChecked() / clearCache(), branche configurable via FOLIO_UPDATE_BRANCH (plus de main hardcodé) - Admin dashboard : affiche la branche suivie, date du dernier contrôle, bouton Vérifier pour forcer le check sans attendre le TTL - CLAUDE.md : architecture DATA_PATH et flux de déploiement documentés Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+52
-11
@@ -9,7 +9,7 @@ class ArticleManager
|
||||
private ?array $allCache = null;
|
||||
private ?array $searchIndexCache = null;
|
||||
|
||||
public function __construct(private string $dataDir)
|
||||
public function __construct(private string $dataDir, private ?DataGit $git = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -132,11 +132,12 @@ class ArticleManager
|
||||
file_put_contents($dir . '/index.md', ltrim($content));
|
||||
$this->rebuildSearchIndex();
|
||||
$this->rebuildBacklinksCache();
|
||||
$this->git?->commit("add: $title");
|
||||
|
||||
return $uuid;
|
||||
}
|
||||
|
||||
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', ?array $tags = null): void
|
||||
public function update(string $uuid, string $title, string $content, bool $published, string $slug, string $publishedAt, string $revisionComment = '', string $seoTitle = '', string $seoDescription = '', string $ogImage = '', string $category = '', ?array $tags = null, bool $skipGit = false): void
|
||||
{
|
||||
$article = $this->getByUuid($uuid);
|
||||
if (!$article) {
|
||||
@@ -199,6 +200,9 @@ class ArticleManager
|
||||
file_put_contents($dir . '/index.md', ltrim($content));
|
||||
$this->rebuildSearchIndex();
|
||||
$this->rebuildBacklinksCache();
|
||||
if (!$skipGit) {
|
||||
$this->git?->commit("update: $title");
|
||||
}
|
||||
}
|
||||
|
||||
public function autosave(string $uuid, string $title, string $content, string $slug): bool
|
||||
@@ -247,6 +251,7 @@ class ArticleManager
|
||||
}
|
||||
$meta['updated_at'] = date('Y-m-d H:i:s');
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->git?->commit("meta: " . ($meta['title'] ?? $uuid));
|
||||
}
|
||||
|
||||
public function saveDraftOverlay(string $uuid, array $metaFields, ?string $content = null): void
|
||||
@@ -269,6 +274,9 @@ class ArticleManager
|
||||
if ($content !== null) {
|
||||
file_put_contents($dir . '/draft_overlay.md', $content);
|
||||
}
|
||||
$raw2 = @file_get_contents($dir . '/meta.json');
|
||||
$title = is_string($raw2) ? (json_decode($raw2, true)['title'] ?? $uuid) : $uuid;
|
||||
$this->git?->commit("draft: $title");
|
||||
}
|
||||
|
||||
public function getDraftOverlay(string $uuid): ?array
|
||||
@@ -315,14 +323,22 @@ class ArticleManager
|
||||
return file_exists($this->dataDir . '/' . $uuid . '/draft_overlay.json');
|
||||
}
|
||||
|
||||
public function discardDraftOverlay(string $uuid): void
|
||||
public function discardDraftOverlay(string $uuid, bool $skipGit = false): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
$title = null;
|
||||
if (!$skipGit && $this->git !== null) {
|
||||
$raw = @file_get_contents($dir . '/meta.json');
|
||||
$title = is_string($raw) ? (json_decode($raw, true)['title'] ?? $uuid) : $uuid;
|
||||
}
|
||||
@unlink($dir . '/draft_overlay.json');
|
||||
@unlink($dir . '/draft_overlay.md');
|
||||
if ($title !== null) {
|
||||
$this->git?->commit("discard-draft: $title");
|
||||
}
|
||||
}
|
||||
|
||||
public function commitDraftOverlay(string $uuid, string $revisionComment = ''): void
|
||||
@@ -331,9 +347,10 @@ class ArticleManager
|
||||
if (!$draft) {
|
||||
return;
|
||||
}
|
||||
$title = $draft['title'];
|
||||
$this->update(
|
||||
$uuid,
|
||||
$draft['title'],
|
||||
$title,
|
||||
$draft['content'],
|
||||
(bool)$draft['published'],
|
||||
$draft['slug'] ?? '',
|
||||
@@ -343,12 +360,14 @@ class ArticleManager
|
||||
$draft['seo_description'] ?? '',
|
||||
$draft['og_image'] ?? '',
|
||||
$draft['category'] ?? '',
|
||||
$draft['tags'] ?? []
|
||||
$draft['tags'] ?? [],
|
||||
true // skipGit — commit unique ci-dessous
|
||||
);
|
||||
$this->discardDraftOverlay($uuid);
|
||||
$this->discardDraftOverlay($uuid, skipGit: true);
|
||||
$this->git?->commit("publish: $title");
|
||||
}
|
||||
|
||||
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = []): void
|
||||
public function addFileMeta(string $uuid, string $filename, string $author, string $sourceUrl, string $title = '', array $extraMeta = [], bool $skipGit = false): void
|
||||
{
|
||||
if (!$this->isValidUuid($uuid)) {
|
||||
return;
|
||||
@@ -377,6 +396,9 @@ class ArticleManager
|
||||
}
|
||||
$meta['files_meta'][$filename] = $entry;
|
||||
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
|
||||
if (!$skipGit) {
|
||||
$this->git?->commit("file-meta: {$uuid}/{$filename}");
|
||||
}
|
||||
}
|
||||
|
||||
public function setCover(string $uuid, string $filename): void
|
||||
@@ -424,6 +446,7 @@ class ArticleManager
|
||||
}
|
||||
$meta['cover'] = $coverName;
|
||||
$this->writeMeta($this->dataDir . '/' . $uuid, $meta);
|
||||
$this->git?->commit("cover: " . ($article['title'] ?? $uuid));
|
||||
}
|
||||
|
||||
public function addFileFromUrl(string $uuid, string $url, bool $isCover = false, string $author = '', string $sourceUrl = '', string $title = '', array $extraMeta = []): ?string
|
||||
@@ -499,7 +522,7 @@ class ArticleManager
|
||||
rename($tmp, $filesDir . '/' . $filename);
|
||||
|
||||
if ($author !== '' || $sourceUrl !== '' || $title !== '' || !empty($extraMeta)) {
|
||||
$this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta);
|
||||
$this->addFileMeta($uuid, $filename, $author, $sourceUrl, $title, $extraMeta, skipGit: true);
|
||||
}
|
||||
|
||||
if ($isCover && $isImage) {
|
||||
@@ -513,6 +536,7 @@ class ArticleManager
|
||||
}
|
||||
}
|
||||
|
||||
$this->git?->commit("add-file: {$uuid}/{$filename}");
|
||||
return $filename;
|
||||
}
|
||||
|
||||
@@ -553,6 +577,7 @@ class ArticleManager
|
||||
$meta['external_links'][] = $entry;
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->rebuildBacklinksCache();
|
||||
$this->git?->commit("link: {$uuid}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -583,6 +608,7 @@ class ArticleManager
|
||||
return false;
|
||||
}
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->git?->commit("link-meta: {$uuid}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -606,6 +632,7 @@ class ArticleManager
|
||||
));
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->rebuildBacklinksCache();
|
||||
$this->git?->commit("unlink: {$uuid}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -623,7 +650,7 @@ class ArticleManager
|
||||
return $cats;
|
||||
}
|
||||
|
||||
public function renameCategory(string $old, string $new): void
|
||||
public function renameCategory(string $old, string $new, bool $skipGit = false): void
|
||||
{
|
||||
if (!is_dir($this->dataDir)) {
|
||||
return;
|
||||
@@ -647,11 +674,15 @@ class ArticleManager
|
||||
$meta['category'] = $new;
|
||||
$this->writeMeta($this->dataDir . '/' . $entry, $meta);
|
||||
}
|
||||
if (!$skipGit) {
|
||||
$this->git?->commit("rename-cat: $old → $new");
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCategory(string $name): void
|
||||
{
|
||||
$this->renameCategory($name, '');
|
||||
$this->renameCategory($name, '', skipGit: true);
|
||||
$this->git?->commit("delete-cat: $name");
|
||||
}
|
||||
|
||||
public function getPrivateCategories(): array
|
||||
@@ -676,6 +707,7 @@ class ArticleManager
|
||||
$this->dataDir . '/private_cats.json',
|
||||
json_encode(array_values($cats), JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
$this->git?->commit("private-cat: $cat");
|
||||
}
|
||||
|
||||
// ─── Tag types ──────────────────────────────────────────────────────────────
|
||||
@@ -701,6 +733,7 @@ class ArticleManager
|
||||
$this->tagTypesPath(),
|
||||
json_encode($types, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"
|
||||
);
|
||||
$this->git?->commit("tag-types");
|
||||
}
|
||||
|
||||
/** Enregistre les tags d'un article directement (utile pour les scripts de migration). */
|
||||
@@ -720,6 +753,7 @@ class ArticleManager
|
||||
$meta['tags'] = $this->normalizeTags($tags);
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->rebuildSearchIndex();
|
||||
$this->git?->commit("tags: " . ($meta['title'] ?? $uuid));
|
||||
}
|
||||
|
||||
/** @return list<string> Toutes les valeurs distinctes d'un type de tag, triées. */
|
||||
@@ -769,6 +803,7 @@ class ArticleManager
|
||||
$this->writeMeta($dir, $meta);
|
||||
$this->allCache = null;
|
||||
@unlink($this->articleCachePath($uuid));
|
||||
$this->git?->commit("featured: " . ($meta['title'] ?? $uuid) . " (" . ($featured ? 'on' : 'off') . ")");
|
||||
}
|
||||
|
||||
public function delete(string $uuid): void
|
||||
@@ -777,6 +812,11 @@ class ArticleManager
|
||||
return;
|
||||
}
|
||||
$dir = $this->dataDir . '/' . $uuid;
|
||||
$title = null;
|
||||
if ($this->git !== null && is_dir($dir)) {
|
||||
$raw = @file_get_contents($dir . '/meta.json');
|
||||
$title = is_string($raw) ? (json_decode($raw, true)['title'] ?? null) : null;
|
||||
}
|
||||
if (is_dir($dir)) {
|
||||
$this->allCache = null;
|
||||
@unlink($this->articleCachePath($uuid));
|
||||
@@ -785,6 +825,7 @@ class ArticleManager
|
||||
}
|
||||
$this->rebuildSearchIndex();
|
||||
$this->rebuildBacklinksCache();
|
||||
$this->git?->commit("delete: " . ($title ?? $uuid));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
+5
-2
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
class BookManager
|
||||
{
|
||||
public function __construct(private string $booksDir)
|
||||
public function __construct(private string $booksDir, private ?DataGit $git = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -95,14 +95,17 @@ class BookManager
|
||||
$this->bookPath($slug),
|
||||
json_encode($book, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"
|
||||
);
|
||||
$this->git?->commit("book: " . ($book['title'] ?? $slug));
|
||||
}
|
||||
|
||||
public function delete(string $slug): void
|
||||
{
|
||||
$path = $this->bookPath($slug);
|
||||
$title = $this->getBySlug($slug)['title'] ?? $slug;
|
||||
$path = $this->bookPath($slug);
|
||||
if (file_exists($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
$this->git?->commit("delete-book: $title");
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class DataGit
|
||||
{
|
||||
public function __construct(private string $dataDir) {}
|
||||
|
||||
public function commit(string $message): void
|
||||
{
|
||||
if (!is_dir($this->dataDir . '/.git')) {
|
||||
return;
|
||||
}
|
||||
$dir = escapeshellarg($this->dataDir);
|
||||
$msg = escapeshellarg($message);
|
||||
shell_exec("git -C $dir add -A 2>/dev/null");
|
||||
exec("git -C $dir diff --cached --quiet 2>/dev/null", $_, $rc);
|
||||
if ($rc !== 0) {
|
||||
shell_exec("git -C $dir commit -m $msg 2>/dev/null");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
function siteSettingsPath(): string
|
||||
{
|
||||
return BASE_PATH . '/data/site_settings.json';
|
||||
return DATA_PATH . '/site_settings.json';
|
||||
}
|
||||
|
||||
function siteSettings(): array
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
function smtpSettingsPath(): string
|
||||
{
|
||||
return BASE_PATH . '/data/smtp_settings.json';
|
||||
return DATA_PATH . '/smtp_settings.json';
|
||||
}
|
||||
|
||||
function smtpSettings(): array
|
||||
|
||||
+27
-3
@@ -89,8 +89,31 @@ class UpdateChecker
|
||||
return version_compare($remoteVer, $deployedVer, '>') ? $remoteVer : null;
|
||||
}
|
||||
|
||||
public function getBranch(): string
|
||||
{
|
||||
return (string) ($_ENV['FOLIO_UPDATE_BRANCH'] ?? getenv('FOLIO_UPDATE_BRANCH') ?: 'main');
|
||||
}
|
||||
|
||||
public function getLastChecked(): ?int
|
||||
{
|
||||
$cacheFile = $this->dataDir . '/.version_check_cache.json';
|
||||
if (!file_exists($cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
$cache = json_decode((string) file_get_contents($cacheFile), true) ?? [];
|
||||
return isset($cache['fetched_at']) ? (int) $cache['fetched_at'] : null;
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
$cacheFile = $this->dataDir . '/.version_check_cache.json';
|
||||
if (file_exists($cacheFile)) {
|
||||
unlink($cacheFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère `public/version.txt` depuis le dépôt Gitea (branche main).
|
||||
* Récupère `public/version.txt` depuis le dépôt Gitea.
|
||||
* Résultat mis en cache 1 h dans `data/.version_check_cache.json`.
|
||||
*/
|
||||
private function fetchRemoteVersion(string $repoUrl): ?string
|
||||
@@ -107,8 +130,9 @@ class UpdateChecker
|
||||
}
|
||||
}
|
||||
|
||||
// URL du fichier brut : {repo}/raw/branch/main/public/version.txt
|
||||
$rawUrl = $repoUrl . '/raw/branch/main/public/version.txt';
|
||||
$branch = $this->getBranch();
|
||||
// URL du fichier brut : {repo}/raw/branch/{branch}/public/version.txt
|
||||
$rawUrl = $repoUrl . '/raw/branch/' . $branch . '/public/version.txt';
|
||||
|
||||
$token = (string) ($_ENV['GITEA_TOKEN'] ?? getenv('GITEA_TOKEN') ?: '');
|
||||
$opts = [
|
||||
|
||||
Reference in New Issue
Block a user