feat(cms): sync site assets, revalidate webhook, and document download naming
This commit is contained in:
111
app/Services/NextjsRepoSyncService.php
Normal file
111
app/Services/NextjsRepoSyncService.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
class NextjsRepoSyncService
|
||||
{
|
||||
/**
|
||||
* Stage+commit+push assets under Next.js public/ that were copied by SiteAssetSyncService / ImportHugoContent.
|
||||
*
|
||||
* Best-effort and never throws: failure should not block the CMS workflow.
|
||||
*/
|
||||
public static function pushPublicAssets(): void
|
||||
{
|
||||
$repoPath = static::resolveRepoRoot();
|
||||
if (! $repoPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lock = Cache::lock('nextjs-repo-assets-push', 120);
|
||||
if (! $lock->get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (! is_dir($repoPath.'/.git')) {
|
||||
Log::warning("Next.js repo push skipped (no .git): {$repoPath}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$remote = (string) config('services.nextjs.repo_remote', 'origin');
|
||||
$branch = (string) config('services.nextjs.repo_branch', 'main');
|
||||
|
||||
// Keep the repo up to date to reduce push failures.
|
||||
static::run($repoPath, ['git', 'fetch', $remote]);
|
||||
static::run($repoPath, ['git', 'checkout', $branch]);
|
||||
static::run($repoPath, ['git', 'pull', '--rebase', $remote, $branch]);
|
||||
|
||||
// Only stage asset directories we manage.
|
||||
static::run($repoPath, ['git', 'add', '-A', '--', 'public/uploads', 'public/images']);
|
||||
|
||||
$status = static::run($repoPath, ['git', 'status', '--porcelain']);
|
||||
if (trim($status) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$msg = (string) config('services.nextjs.repo_assets_commit_message', 'chore(assets): sync public assets');
|
||||
$msg .= ' ('.now()->format('Y-m-d H:i:s').')';
|
||||
|
||||
static::run($repoPath, ['git', 'commit', '-m', $msg]);
|
||||
static::run($repoPath, ['git', 'push', $remote, $branch]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Next.js repo push failed: '.$e->getMessage());
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
public static function scheduleAssetsPush(): void
|
||||
{
|
||||
if (! config('services.nextjs.autopush_assets')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If queue is sync, this runs inline; otherwise requires a worker.
|
||||
\App\Jobs\PushNextjsPublicAssetsJob::dispatch()->delay(now()->addSeconds(5));
|
||||
}
|
||||
|
||||
private static function resolveRepoRoot(): ?string
|
||||
{
|
||||
$configured = config('services.nextjs.repo_path');
|
||||
if (is_string($configured) && $configured !== '' && is_dir($configured)) {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
$public = config('services.nextjs.public_path');
|
||||
if (is_string($public) && $public !== '') {
|
||||
$root = dirname($public);
|
||||
if (is_dir($root)) {
|
||||
return $root;
|
||||
}
|
||||
}
|
||||
|
||||
$guess = base_path('../usher-site');
|
||||
if (is_dir($guess)) {
|
||||
return $guess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function run(string $cwd, array $cmd): string
|
||||
{
|
||||
$p = new Process($cmd, $cwd);
|
||||
$p->setTimeout(60);
|
||||
$p->run();
|
||||
|
||||
$out = (string) $p->getOutput();
|
||||
$err = (string) $p->getErrorOutput();
|
||||
if (! $p->isSuccessful()) {
|
||||
throw new \RuntimeException('Command failed: '.implode(' ', $cmd)."\n".$err.$out);
|
||||
}
|
||||
|
||||
return $out.$err;
|
||||
}
|
||||
}
|
||||
|
||||
101
app/Services/SiteAssetSyncService.php
Normal file
101
app/Services/SiteAssetSyncService.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SiteAssetSyncService
|
||||
{
|
||||
/**
|
||||
* Copy a file from Laravel's public disk into usher-site/public so Next.js can serve it as a static asset.
|
||||
*
|
||||
* Returns the relative path under Next.js public/ (no leading slash), e.g. "uploads/articles/images/foo.jpg".
|
||||
* If the Next.js public path is not configured / not present, returns null and callers should fall back
|
||||
* to serving from Laravel storage.
|
||||
*/
|
||||
public static function syncPublicDiskFileToNext(string $publicDiskPath): ?string
|
||||
{
|
||||
$publicDiskPath = ltrim($publicDiskPath, '/');
|
||||
|
||||
$nextPublicRoot = static::resolveNextPublicRoot();
|
||||
if (! $nextPublicRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$source = Storage::disk('public')->path($publicDiskPath);
|
||||
if (! is_file($source)) {
|
||||
Log::warning("Site asset sync skipped (source missing): {$publicDiskPath}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$prefix = config('services.nextjs.public_upload_prefix', 'uploads');
|
||||
$prefix = trim((string) $prefix, '/');
|
||||
if ($prefix === '') {
|
||||
$prefix = 'uploads';
|
||||
}
|
||||
|
||||
$relativeDest = $prefix.'/'.$publicDiskPath;
|
||||
$dest = rtrim($nextPublicRoot, '/').'/'.$relativeDest;
|
||||
|
||||
$destDir = dirname($dest);
|
||||
if (! is_dir($destDir) && ! @mkdir($destDir, 0755, true) && ! is_dir($destDir)) {
|
||||
Log::warning("Site asset sync failed (mkdir): {$destDir}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! @copy($source, $dest)) {
|
||||
Log::warning("Site asset sync failed (copy): {$publicDiskPath} -> {$dest}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Optionally commit+push the updated assets into the usher-site repo.
|
||||
NextjsRepoSyncService::scheduleAssetsPush();
|
||||
|
||||
return $relativeDest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a previously synced Next.js public asset (best-effort).
|
||||
*/
|
||||
public static function deleteNextPublicFile(?string $nextPublicRelativePath): void
|
||||
{
|
||||
if (! $nextPublicRelativePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nextPublicRoot = static::resolveNextPublicRoot();
|
||||
if (! $nextPublicRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rel = ltrim($nextPublicRelativePath, '/');
|
||||
$path = rtrim($nextPublicRoot, '/').'/'.$rel;
|
||||
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
|
||||
// Stage deletion via scheduled push (best-effort).
|
||||
NextjsRepoSyncService::scheduleAssetsPush();
|
||||
}
|
||||
|
||||
private static function resolveNextPublicRoot(): ?string
|
||||
{
|
||||
$configured = config('services.nextjs.public_path');
|
||||
if (is_string($configured) && $configured !== '' && is_dir($configured)) {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
// Local-dev default: sibling repo next to this Laravel repo.
|
||||
$guess = base_path('../usher-site/public');
|
||||
if (is_dir($guess)) {
|
||||
return $guess;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
48
app/Services/SiteRevalidationService.php
Normal file
48
app/Services/SiteRevalidationService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SiteRevalidationService
|
||||
{
|
||||
/**
|
||||
* Revalidate article cache on the Next.js frontend.
|
||||
*/
|
||||
public static function revalidateArticle(?string $slug = null): void
|
||||
{
|
||||
static::revalidate('article', $slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate page cache on the Next.js frontend.
|
||||
*/
|
||||
public static function revalidatePage(?string $slug = null): void
|
||||
{
|
||||
static::revalidate('page', $slug);
|
||||
}
|
||||
|
||||
private static function revalidate(string $type, ?string $slug = null): void
|
||||
{
|
||||
$url = config('services.nextjs.revalidate_url');
|
||||
$token = config('services.nextjs.revalidate_token');
|
||||
|
||||
if (! $url || ! $token) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = ['type' => $type];
|
||||
if ($slug) {
|
||||
$payload['slug'] = $slug;
|
||||
}
|
||||
|
||||
Http::timeout(5)
|
||||
->withHeaders(['x-revalidate-token' => $token])
|
||||
->post($url, $payload);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning("Site revalidation failed for {$type}/{$slug}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user