feat : statistiques admin, livres, setup.sh, permissions rsync (v1.3.0) #66
@@ -40,6 +40,11 @@ SMTP_FROM_NAME=
|
||||
CONTACT_EMAIL=
|
||||
CONTACT_FROM_EMAIL=
|
||||
|
||||
# Chemin absolu vers le répertoire des articles (data/)
|
||||
# Par défaut : BASE_PATH/data (dans le répertoire de l'application)
|
||||
# Recommandé en production : chemin hors du répertoire web, ex. /srv/data/folio
|
||||
DATA_PATH=/srv/data/folio
|
||||
|
||||
# Logs Apache (onglet Recherches dans /admin)
|
||||
# Nom du fichier de log d'accès du vhost dans /var/log/apache2/
|
||||
APACHE_ACCESS_LOG=lan.acegrp.varlog-access.log
|
||||
|
||||
@@ -9,6 +9,18 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag
|
||||
|
||||
---
|
||||
|
||||
## [1.4.0] - 2026-05-15
|
||||
|
||||
### Ajouté
|
||||
- **`DATA_PATH`** : chemin des articles configurable via `.env`, indépendant du document root — permet de stocker `/data` hors de l'arborescence web (ex. `/srv/data/folio`)
|
||||
- **`DataGit`** : auto-commit git sur toutes les écritures articles et livres (création, modification, suppression, métadonnées, tags, fichiers, liens…) sauf `autosave` — no-op silencieux si `DATA_PATH` n'est pas un dépôt git
|
||||
- **Admin — Moteur Folio** : affiche la branche suivie pour les mises à jour (`FOLIO_UPDATE_BRANCH`, défaut `main`), la date du dernier contrôle, et un bouton **Vérifier** pour forcer la vérification sans attendre le TTL du cache (1 h)
|
||||
|
||||
### Modifié
|
||||
- `UpdateChecker` : branche cible configurable via `FOLIO_UPDATE_BRANCH` (plus de `main` hardcodé dans l'URL Gitea)
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0] - 2026-05-15
|
||||
|
||||
### Ajouté
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## Ce qu'est ce dépôt
|
||||
|
||||
**Folio** est un moteur de blog PHP.
|
||||
Ce répertoire est la **copie locale du dépôt Git** (`https://git.abonnel.fr/cedricAbonnel/folio`), branche DEV.
|
||||
Il contient uniquement le code du moteur — pas de données, pas de credentials.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Répertoire local | Site distant | Rôle |
|
||||
|-----------------|-------------|------|
|
||||
| `~/Projects/folio/` | — | Copie du dépôt Folio (branche DEV). On code ici. |
|
||||
| `~/Projects/varlog/` | varlog.a5l.fr | Sync bidirectionnelle des articles varlog. Sert de site de test pour le moteur. |
|
||||
| `~/Projects/fr.abonnel.www/` | www.abonnel.fr | Sync bidirectionnelle des articles abonnel.fr. A aussi servi au déploiement initial. |
|
||||
|
||||
**abonnel.fr** utilise Folio mais se met à jour seul via son UpdateChecker interne (vérifie `version.txt` sur Gitea). Aucune action manuelle nécessaire côté serveur.
|
||||
|
||||
## Articles (`data/`)
|
||||
|
||||
Les articles ne sont pas versionnés dans ce dépôt. Ils ont leur propre git local dans chaque workspace site (`~/Projects/varlog/data/`, `~/Projects/fr.abonnel.www/data/`), synchronisé de façon bidirectionnelle avec le serveur distant.
|
||||
|
||||
## Modifier le moteur
|
||||
|
||||
Pour toute correction ou fonctionnalité : **créer un ticket et une PR**.
|
||||
|
||||
1. Coder ici dans `~/Projects/folio/` (branche feature)
|
||||
2. **Tester sur varlog.a5l.fr** :
|
||||
```bash
|
||||
~/Projects/varlog/scripts/sync.sh
|
||||
# puis tester sur http://varlog.acegrp.lan
|
||||
```
|
||||
3. Une fois validé, ouvrir une PR sur Gitea. Le commit doit inclure :
|
||||
- `public/version.txt` (bump semver)
|
||||
- `CHANGELOG.md` (entrée `### Ajouté / Corrigé / Modifié`)
|
||||
4. Merger la PR → abonnel.fr se met à jour automatiquement.
|
||||
|
||||
## Données articles (`DATA_PATH`)
|
||||
|
||||
Les articles sont stockés dans un répertoire **hors du dépôt Folio**, configurable via `DATA_PATH` dans `.env`.
|
||||
|
||||
| Environnement | Chemin local | Chemin serveur |
|
||||
|--------------|-------------|----------------|
|
||||
| varlog | `~/Projects/varlog-data/` | `/srv/data/folio` |
|
||||
| abonnel.fr | `~/Projects/fr.abonnel.www-data/` | `/srv/data/folio` |
|
||||
|
||||
Les scripts de sync (`pull-data.sh`, `push-data.sh`, `sync.sh`) utilisent `DATA_DIR` (overridable via env) pointant vers ces chemins locaux.
|
||||
|
||||
## Asymétrie de déploiement moteur
|
||||
|
||||
| Site | Mécanisme | Raison |
|
||||
|------|-----------|--------|
|
||||
| varlog (test) | rsync depuis `~/Projects/folio/` | Itération rapide, pas de contrainte de stabilité |
|
||||
| abonnel.fr (prod) | `git pull origin main` sur le serveur | Contrôle via PR/merge, UpdateChecker autonome |
|
||||
|
||||
Pour initialiser git sur un serveur abonnel.fr déployé via rsync : `scripts/git-init-remote.sh`
|
||||
|
||||
## Ne pas mettre ici
|
||||
|
||||
- `.env` (credentials → dans chaque workspace site)
|
||||
- `data/` (articles → dans chaque workspace site)
|
||||
- `vendor/` (non versionné)
|
||||
@@ -6,6 +6,12 @@ if (!defined('BASE_PATH')) {
|
||||
define('BASE_PATH', __DIR__);
|
||||
}
|
||||
|
||||
if (!defined('DATA_PATH')) {
|
||||
$__dataPath = $_ENV['DATA_PATH'] ?? getenv('DATA_PATH') ?: '';
|
||||
define('DATA_PATH', $__dataPath !== '' ? rtrim($__dataPath, '/') : BASE_PATH . '/data');
|
||||
unset($__dataPath);
|
||||
}
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
$sessionName = $_ENV['SESSION_NAME'] ?? (getenv('SESSION_NAME') ?: null);
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
define('BASE_PATH', __DIR__);
|
||||
define('DATA_PATH', BASE_PATH . '/data');
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ require_once BASE_PATH . '/src/Parsedown.php';
|
||||
|
||||
const FEED_PAGE_SIZE = 20;
|
||||
|
||||
$articles = new ArticleManager(BASE_PATH . '/data');
|
||||
$articles = new ArticleManager(DATA_PATH);
|
||||
$privateCats = $articles->getPrivateCategories();
|
||||
$Parsedown = new Parsedown();
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ if ($name === '' || $name[0] === '.') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = BASE_PATH . '/data/' . $uuid . '/files/' . $name;
|
||||
$path = DATA_PATH . '/' . $uuid . '/files/' . $name;
|
||||
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
|
||||
+25
-13
@@ -24,12 +24,14 @@ require_once BASE_PATH . '/src/auth.php';
|
||||
require_once BASE_PATH . '/src/SiteSettings.php';
|
||||
require_once BASE_PATH . '/src/ArticleManager.php';
|
||||
require_once BASE_PATH . '/src/BookManager.php';
|
||||
require_once BASE_PATH . '/src/DataGit.php';
|
||||
|
||||
$articles = new ArticleManager(BASE_PATH . '/data');
|
||||
$books = new BookManager(BASE_PATH . '/data/books');
|
||||
$_dataGit = new DataGit(DATA_PATH);
|
||||
$articles = new ArticleManager(DATA_PATH, $_dataGit);
|
||||
$books = new BookManager(DATA_PATH . '/books', $_dataGit);
|
||||
|
||||
// ─── Mode maintenance ──────────────────────────────────────────────────────
|
||||
if (file_exists(BASE_PATH . '/data/.maintenance')) {
|
||||
if (file_exists(DATA_PATH . '/.maintenance')) {
|
||||
http_response_code(503);
|
||||
header('Retry-After: 60');
|
||||
include BASE_PATH . '/templates/maintenance.php';
|
||||
@@ -37,7 +39,7 @@ if (file_exists(BASE_PATH . '/data/.maintenance')) {
|
||||
}
|
||||
|
||||
require_once BASE_PATH . '/src/UpdateChecker.php';
|
||||
$_updateChecker = new UpdateChecker(BASE_PATH . '/data', BASE_PATH);
|
||||
$_updateChecker = new UpdateChecker(DATA_PATH, BASE_PATH);
|
||||
|
||||
$action = $_GET['action'] ?? 'list';
|
||||
$uuid = $_GET['uuid'] ?? '';
|
||||
@@ -78,7 +80,7 @@ function searchAndRedirect(string $rawPath, ArticleManager $articles): void
|
||||
// ─── Pages statiques depuis data/site/ ──────────────────────────────────────
|
||||
function loadSitePageData(string $slug): array
|
||||
{
|
||||
$base = BASE_PATH . '/data/site';
|
||||
$base = DATA_PATH . '/site';
|
||||
$meta = [];
|
||||
$raw = @file_get_contents($base . '/' . $slug . '.json');
|
||||
if ($raw !== false) {
|
||||
@@ -1383,7 +1385,7 @@ switch ($action) {
|
||||
|
||||
case 'flux':
|
||||
require_once BASE_PATH . '/src/FeedFetcher.php';
|
||||
$fetcher = new FeedFetcher(BASE_PATH . '/data/_cache/feeds');
|
||||
$fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds');
|
||||
$fluxItems = [];
|
||||
$pdo = dbPdo();
|
||||
if ($pdo) {
|
||||
@@ -1535,8 +1537,8 @@ switch ($action) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Paramètres invalides']);
|
||||
exit;
|
||||
}
|
||||
$cfSrc = BASE_PATH . '/data/' . $cfFrom . '/files/' . $cfName;
|
||||
$cfDstDir = BASE_PATH . '/data/' . $cfTo . '/files';
|
||||
$cfSrc = DATA_PATH . '/' . $cfFrom . '/files/' . $cfName;
|
||||
$cfDstDir = DATA_PATH . '/' . $cfTo . '/files';
|
||||
$cfDst = $cfDstDir . '/' . $cfName;
|
||||
if (!file_exists($cfSrc)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Fichier source introuvable']);
|
||||
@@ -1649,7 +1651,7 @@ switch ($action) {
|
||||
// Capture d'écran pour prévisualisation (pages HTML uniquement, URL externes uniquement)
|
||||
$step2Screenshot = null;
|
||||
if (!$step2IsInternal && str_starts_with($step2Meta['mime'] ?? '', 'text/html')) {
|
||||
$filesDir = BASE_PATH . '/data/' . $uuid . '/files';
|
||||
$filesDir = DATA_PATH . '/' . $uuid . '/files';
|
||||
if (!is_dir($filesDir)) {
|
||||
mkdir($filesDir, 0755, true);
|
||||
}
|
||||
@@ -1725,7 +1727,7 @@ switch ($action) {
|
||||
header('Location: /import/' . rawurlencode($urlUuid) . '?error=1');
|
||||
exit;
|
||||
}
|
||||
$filesDir = BASE_PATH . '/data/' . $urlUuid . '/files';
|
||||
$filesDir = DATA_PATH . '/' . $urlUuid . '/files';
|
||||
$previewPath = $filesDir . '/' . $screenshotFile;
|
||||
if (!file_exists($previewPath)) {
|
||||
header('Location: /import/' . rawurlencode($urlUuid) . '?error=1');
|
||||
@@ -1744,7 +1746,7 @@ switch ($action) {
|
||||
}
|
||||
|
||||
if ($mode === 'link') {
|
||||
$filesDir = BASE_PATH . '/data/' . $urlUuid . '/files';
|
||||
$filesDir = DATA_PATH . '/' . $urlUuid . '/files';
|
||||
if (!is_dir($filesDir)) {
|
||||
mkdir($filesDir, 0755, true);
|
||||
}
|
||||
@@ -1895,7 +1897,7 @@ switch ($action) {
|
||||
$done = $fail = $skip = 0;
|
||||
foreach ($articles->getAll() as $article) {
|
||||
$artUuid = $article['uuid'];
|
||||
$filesDir = BASE_PATH . '/data/' . $artUuid . '/files';
|
||||
$filesDir = DATA_PATH . '/' . $artUuid . '/files';
|
||||
foreach ($article['external_links'] ?? [] as $link) {
|
||||
$lMeta = $link['meta'] ?? [];
|
||||
$lMime = $lMeta['mime'] ?? 'text/html';
|
||||
@@ -2784,7 +2786,7 @@ switch ($action) {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$_cmDataDir = BASE_PATH . '/data';
|
||||
$_cmDataDir = DATA_PATH;
|
||||
$_cmTrack = $_cmDataDir . '/.content_migrations.json';
|
||||
$_cmFlag = $_cmDataDir . '/.maintenance';
|
||||
$_cmApplied = file_exists($_cmTrack) ? (json_decode((string) file_get_contents($_cmTrack), true) ?? []) : [];
|
||||
@@ -2814,6 +2816,16 @@ switch ($action) {
|
||||
header('Location: /admin?tab=dashboard¬ice=' . ($_cmErrors ? 'migration_error' : 'migrated'));
|
||||
exit;
|
||||
|
||||
case 'force_update_check':
|
||||
requireAuth();
|
||||
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(403);
|
||||
exit;
|
||||
}
|
||||
$_updateChecker->clearCache();
|
||||
header('Location: /admin?tab=dashboard');
|
||||
exit;
|
||||
|
||||
case 'admin_save_site':
|
||||
requireAuth();
|
||||
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ require_once BASE_PATH . '/src/helpers.php';
|
||||
require_once BASE_PATH . '/config/config.php';
|
||||
require_once BASE_PATH . '/src/ArticleManager.php';
|
||||
|
||||
$articles = new ArticleManager(BASE_PATH . '/data');
|
||||
$articles = new ArticleManager(DATA_PATH);
|
||||
$privateCats = $articles->getPrivateCategories();
|
||||
|
||||
$published = array_filter($articles->getAll(true), static function (array $a) use ($privateCats): bool {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.3.0
|
||||
1.4.0
|
||||
|
||||
+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 = [
|
||||
|
||||
+12
-1
@@ -104,6 +104,8 @@ function adminStatusBadge(array $a, int $now): string
|
||||
$_deployedVer = trim((string) @file_get_contents(BASE_PATH . '/public/version.txt'));
|
||||
$_deployedLabel = $_deployedVer !== '' ? $_deployedVer : '—';
|
||||
$_notices = isset($_updateChecker) ? $_updateChecker->adminNotices() : [];
|
||||
$_branch = isset($_updateChecker) ? $_updateChecker->getBranch() : 'main';
|
||||
$_lastChecked = isset($_updateChecker) ? $_updateChecker->getLastChecked() : null;
|
||||
$_remoteLabel = '—';
|
||||
foreach ($_notices as $_n) {
|
||||
if ($_n['type'] === 'info' && preg_match('/v([\d]+\.[\d]+\.[\d]+)/', $_n['message'], $_m)) {
|
||||
@@ -122,7 +124,16 @@ function adminStatusBadge(array $a, int $now): string
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Dernière version disponible</th>
|
||||
<td><?= htmlspecialchars($_remoteLabel) ?><?= $_remoteLabel !== '—' && $_remoteLabel !== $_deployedLabel ? ' <span class="badge bg-warning text-dark ms-1">Mise à jour disponible</span>' : '' ?></td>
|
||||
<td class="d-flex align-items-center gap-2 flex-wrap">
|
||||
<span><?= htmlspecialchars($_remoteLabel) ?><?= $_remoteLabel !== '—' && $_remoteLabel !== $_deployedLabel ? ' <span class="badge bg-warning text-dark ms-1">Mise à jour disponible</span>' : '' ?></span>
|
||||
<form method="POST" action="/?action=force_update_check" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm py-0">Vérifier</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-muted fw-normal ps-0 pe-2 text-nowrap">Branche suivie</th>
|
||||
<td><code><?= htmlspecialchars($_branch) ?></code><?= $_lastChecked !== null ? ' <span class="text-muted ms-2">· vérifié le ' . date('d/m/Y à H:i', $_lastChecked) . '</span>' : '' ?></td>
|
||||
</tr>
|
||||
<?php if (!empty($_notices)): ?>
|
||||
<tr>
|
||||
|
||||
@@ -60,7 +60,7 @@ $preSource = $step2Meta['canonical'] ?? $step2Meta['source'] ?? $step2Url;
|
||||
<div class="mb-4">
|
||||
<p class="fw-semibold small mb-2">Aperçu de la page</p>
|
||||
<?php
|
||||
$previewMtime = @filemtime(BASE_PATH . '/data/' . $step2Article['uuid'] . '/files/' . $step2Screenshot) ?: time();
|
||||
$previewMtime = @filemtime(DATA_PATH . '/' . $step2Article['uuid'] . '/files/' . $step2Screenshot) ?: time();
|
||||
?>
|
||||
<img src="/file?uuid=<?= rawurlencode($step2Article['uuid']) ?>&name=<?= rawurlencode($step2Screenshot) ?>&v=<?= $previewMtime ?>"
|
||||
class="img-fluid rounded shadow-sm d-block"
|
||||
|
||||
Reference in New Issue
Block a user