diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index de59ec94..60d78fbf 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -147,6 +147,20 @@ Reports will stick around until deleted, so you are free to compare reports to s You may generate a report through the Control Panel, or by running `php please seo-pro:generate-report`. +### Storage + +By default, reports are stored as YAML files in the `storage/statamic/seopro/reports` directory. Where the filesystem is not shared between processes or does not survive a deploy - containers, serverless platforms, autoscaled instances - a report written by one process is never seen by the next request, and generating one ends on a 404. Point reports at a filesystem disk instead: + +```php +// config/statamic/seo-pro.php + +'reports' => [ + 'disk' => 's3', +], +``` + +The disk is any disk from `config/filesystems.php`; reports are stored under `statamic/seopro/reports` on it. Reports generated before the switch stay on the local disk and are not listed until they are copied over. + ### Queuing Reports Depending on the size of your site, generating a report may take a while. To prevent request timeouts, you may enable queues, and the reports will be truly queued in the background. diff --git a/config/seo-pro.php b/config/seo-pro.php index 6415c6ad..88a6261b 100644 --- a/config/seo-pro.php +++ b/config/seo-pro.php @@ -50,6 +50,7 @@ ], 'reports' => [ + 'disk' => null, 'keep_recent' => 'all', 'queue_chunk_size' => 1000, 'title_length' => [ diff --git a/src/Reporting/Chunk.php b/src/Reporting/Chunk.php index 6424d34e..c0567c94 100644 --- a/src/Reporting/Chunk.php +++ b/src/Reporting/Chunk.php @@ -37,7 +37,7 @@ protected function yamlPath() public function save() { - File::put($this->yamlPath(), YAML::dump([ + File::disk(Report::disk())->put($this->yamlPath(), YAML::dump([ 'ids' => $this->contentIds, ])); @@ -61,7 +61,7 @@ protected function generate($ids) $this->createPage($content[$id] ?? Data::find($id))?->save(); } - File::delete($this->folderPath()); + File::disk(Report::disk())->delete($this->folderPath()); if ($this->wasLastChunk()) { $this->report->finalize(); @@ -70,7 +70,7 @@ protected function generate($ids) protected function wasLastChunk() { - return File::getFolders($this->report->chunksFolder())->isEmpty(); + return File::disk(Report::disk())->getFolders($this->report->chunksFolder())->isEmpty(); } protected function createPage($content) diff --git a/src/Reporting/Page.php b/src/Reporting/Page.php index b811ddb1..4c0cc666 100644 --- a/src/Reporting/Page.php +++ b/src/Reporting/Page.php @@ -146,7 +146,7 @@ public function save() 'results' => $this->results, ]; - File::put($this->path(), YAML::dump($data)); + File::disk(Report::disk())->put($this->path(), YAML::dump($data)); } protected function path() @@ -154,9 +154,9 @@ protected function path() $key = md5($this->id); $parts = array_slice(str_split($key, 2), 0, 2); - return storage_path(vsprintf('statamic/seopro/reports/%s/pages/%s/%s.yaml', [ + return vsprintf('statamic/seopro/reports/%s/pages/%s/%s.yaml', [ $this->report->id(), implode('/', $parts), $key, - ])); + ]); } public function model() diff --git a/src/Reporting/Report.php b/src/Reporting/Report.php index a781ad49..189fae09 100644 --- a/src/Reporting/Report.php +++ b/src/Reporting/Report.php @@ -7,9 +7,9 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Storage; use Statamic\Facades\Entry; use Statamic\Facades\File; -use Statamic\Facades\Folder; use Statamic\Facades\Term; use Statamic\Facades\YAML; @@ -112,8 +112,8 @@ public function generate() protected function hasRemainingChunks() { - return File::exists($this->chunksFolder()) - && File::getFolders($this->chunksFolder())->isNotEmpty(); + return File::disk(static::disk())->exists($this->chunksFolder()) + && File::disk(static::disk())->getFolders($this->chunksFolder())->isNotEmpty(); } protected function chunks() @@ -139,8 +139,8 @@ protected function chunks() public function finalize() { - if (File::exists($dir = $this->chunksFolder())) { - File::delete($dir); + if (File::disk(static::disk())->exists($dir = $this->chunksFolder())) { + File::disk(static::disk())->delete($dir); } $this @@ -284,12 +284,12 @@ public function withPages($preferCache = false) return $this; } - $dir = storage_path(sprintf('statamic/seopro/reports/%s/pages', $this->id)); + $dir = sprintf('statamic/seopro/reports/%s/pages', $this->id); - $files = Folder::getFilesRecursively($dir); + $files = File::disk(static::disk())->getFilesRecursively($dir); $this->pages = collect($files) - ->map(fn ($file) => YAML::parse(File::get($file))) + ->map(fn ($file) => YAML::parse(File::disk(static::disk())->get($file))) ->map(fn ($yaml) => (new Page($yaml['id'], $yaml['data'], $this))->setResults($yaml['results'])) ->sortBy ->url() @@ -326,7 +326,7 @@ public function generateIfNecessary(): self public static function all() { - $folders = collect(Folder::getFolders(static::preparePath())); + $folders = collect(File::disk(static::disk())->getFolders('statamic/seopro/reports')); if ($folders->isEmpty()) { return $folders; @@ -369,7 +369,7 @@ public function fresh() public function save() { - File::put($this->path(), YAML::dump($this->raw = [ + File::disk(static::disk())->put($this->path(), YAML::dump($this->raw = [ 'date' => $this->date ?? now()->timestamp, 'status' => $this->status(), 'score' => $this->score(), @@ -385,34 +385,46 @@ public function save() public function delete() { - File::delete($this->parentFolder()); + File::disk(static::disk())->delete($this->parentFolder()); return $this->clearCaches(); } + /** + * The disk for File::disk(): the one configured in `reports.disk`, or else the local + * storage directory. Reports live under statamic/seopro/reports on it. + */ + public static function disk() + { + return config('statamic.seo-pro.reports.disk') ?: Storage::build([ + 'driver' => 'local', + 'root' => storage_path(), + ]); + } + public function parentFolder() { - return storage_path('statamic/seopro/reports/'.$this->id); + return 'statamic/seopro/reports/'.$this->id; } public function chunksFolder() { - return storage_path('statamic/seopro/reports/'.$this->id.'/chunks'); + return 'statamic/seopro/reports/'.$this->id.'/chunks'; } public function path() { - return storage_path('statamic/seopro/reports/'.$this->id.'/report.yaml'); + return 'statamic/seopro/reports/'.$this->id.'/report.yaml'; } public function exists() { - return File::exists($this->path()); + return File::disk(static::disk())->exists($this->path()); } public function load() { - $this->raw = YAML::parse(File::get($this->path())); + $this->raw = YAML::parse(File::disk(static::disk())->get($this->path())); $this->date = $this->raw['date']; $this->results = $this->raw['results']; @@ -499,17 +511,6 @@ protected function generateScore() return $this; } - public static function preparePath($path = null) - { - $path = collect([storage_path('statamic/seopro/reports'), $path])->filter()->implode('/'); - - if (! File::exists($path)) { - File::makeDirectory($path, 0755, true); - } - - return $path; - } - protected function allContent() { $content = collect() @@ -562,7 +563,7 @@ public static function deleteOldReports() return false; } - $reports = collect(app('files')->directories(storage_path('statamic/seopro/reports'))); + $reports = collect(File::disk(static::disk())->getFolders('statamic/seopro/reports')); $keepReports = $reports ->keyBy(fn ($path) => pathinfo($path)['basename']) @@ -571,7 +572,7 @@ public static function deleteOldReports() $reports ->reject(fn ($path) => $keepReports->contains($path)) - ->each(fn ($path) => app('files')->deleteDirectory($path)); + ->each(fn ($path) => File::disk(static::disk())->delete($path)); return true; } diff --git a/tests/ReportTest.php b/tests/ReportTest.php index df384a9c..c5e5cf46 100644 --- a/tests/ReportTest.php +++ b/tests/ReportTest.php @@ -3,6 +3,7 @@ namespace Tests; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Storage; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Blink; use Statamic\Facades\Config; @@ -68,6 +69,32 @@ public function it_increments_report_folder_numbers() $this->assertFileExists($this->reportsPath('3')); } + #[Test] + public function it_stores_reports_on_the_configured_disk() + { + config(['statamic.seo-pro.reports.disk' => 'reports']); + Storage::fake('reports'); + + $this->generateEntries(3); + + Report::create()->save()->generate(); + + Storage::disk('reports')->assertExists('statamic/seopro/reports/1/report.yaml'); + $this->assertCount(3, Storage::disk('reports')->allFiles('statamic/seopro/reports/1/pages')); + $this->assertCount(0, Storage::disk('reports')->directories('statamic/seopro/reports/1/chunks')); + $this->assertFileDoesNotExist($this->reportsPath()); + + $this->assertCount(1, Report::all()); + $this->assertSame(1, Report::find(1)->id()); + $this->assertSame(3, Report::find(1)->withPages()->pagesCrawled()); + $this->assertSame(2, Report::nextId()); + + Report::find(1)->delete(); + + Storage::disk('reports')->assertMissing('statamic/seopro/reports/1/report.yaml'); + $this->assertCount(0, Report::all()); + } + #[Test] public function it_can_generate_a_report() {