25 Commits

Author SHA1 Message Date
paranarimasu c0464406d3 feat: add configurable throttling to video (#536) 2023-03-01 08:29:01 -06:00
paranarimasu 134a82ecc9 test: fix value boundary in billing factories & backfill anime studios needs unique value from external api (#514) 2023-02-07 23:58:45 -06:00
paranarimasu 3e71ff8ace refactor(admin): reduce number of queries for upload actions (#504)
* refactor(admin): reduce number of queries for upload actions

* style: fix phpdoc description for upload audio action

* style: fix audio attribute constants
2023-01-15 23:13:48 -06:00
paranarimasu 39250632b2 fix(api): fix anime video script allowed include path (#501) 2022-12-30 23:52:30 -06:00
Mani 3cdf824e47 feat(api): Added videoscript include path to anime (#499) 2022-12-30 21:26:23 -06:00
paranarimasu 681f07ecc9 chore: applying framework updates (#498) 2022-12-30 16:41:43 -06:00
paranarimasu 9028557f4c chore: bump dependencies for php8.2 (#500) 2022-12-30 16:40:26 -06:00
paranarimasu cea28e61f1 feat(admin): attach resource from detail/inline (#496) 2022-12-23 16:05:57 -06:00
paranarimasu fed083b5d3 feat(admin): allow video attributes to be set on upload (#495) 2022-12-23 11:53:17 -06:00
paranarimasu 2e97bb78ea chore: bump dependencies (#486) 2022-12-20 23:20:48 -06:00
paranarimasu b320882d6d feat: add static link for latest db dumps (#485) 2022-12-20 23:05:11 -06:00
paranarimasu a8801edec9 Chore 2022 11 07 (#479)
* chore: bump dependencies

* style: fix StyleCI finding
2022-12-20 22:22:26 -06:00
paranarimasu ed33d4154a chore: updating dependencies (#475) 2022-12-19 22:57:54 -06:00
paranarimasu d3a8d58cd2 #490 fix(admin): update pattern for mal studio cover image (#492) 2022-12-12 00:58:11 -06:00
paranarimasu 4cf68a96ac chore: updating dependencies & adding monitoring logs (#466) 2022-09-24 12:37:28 -05:00
paranarimasu 8b6b196c99 feat(admin): migrate db dumps from github to platform (#465)
* feat: migrate encoding scripts from github to platform

* style: fix StyleCI finding

* fix: don't concatenate updated videos when attempting to attach entry on video upload

* fix(api): fix field dependencies & feat(admin): upload script from video detail screen
2022-09-24 00:01:37 -05:00
paranarimasu 99cf085666 feat(admin): support attaching entry when uploading video & fix(admin): support anidb change requests for artist & studio resources (#464)
* feat(admin): support attaching entry when uploading video & fix(admin): support anidb change requests for artist & studio resources

* style: fix StyleCI finding
2022-09-20 15:21:09 -05:00
paranarimasu 1d93000c6f feat(admin): migrate db dumps from github to platform (#463)
* feat(admin): migrate db dumps from github to platform

* style: fix StyleCI findings
2022-09-18 22:31:29 -05:00
paranarimasu 57cc3d508d chore: prune redundant auditing dependency (#462) 2022-09-06 14:31:37 -05:00
paranarimasu 2792961a06 chore: updating dependencies (#461) 2022-09-05 23:41:55 -05:00
paranarimasu 95134a384c fix(api): validate untrimmed and empty values with custom delimited validation rule (#460)
* fix(api): validate untrimmed and empty values with custom delimited validation rule

* style: fix StyleCI findings
2022-09-05 23:01:54 -05:00
paranarimasu 16c8d1781b fix(admin): enforce stricter normalization of external resource links (#459)
* fix(admin): enforce stricter normalization of external resource links

* fix(admin): value safety when resolving site for resource link format rule
2022-08-31 21:09:28 -05:00
paranarimasu 23afebc70e fix(api): validate null values for filter and sort parameters (#458)
* fix(api): validate null values for filter and sort parameters

* refactor(api): consistency of sort type array check
2022-08-29 15:35:52 -05:00
paranarimasu 7d5cd09768 fix(admin): model action query & fix(api): restricted type validation rules (#457) 2022-08-28 23:29:11 -05:00
paranarimasu b1e2a65b85 fix(admin): theme slug type safety for autocomplete (#456) 2022-08-27 13:16:58 -05:00
472 changed files with 18809 additions and 7342 deletions
+2 -2
View File
@@ -3,9 +3,9 @@ root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
+31
View File
@@ -73,6 +73,12 @@ REDIS_SCHEME=tcp
REDIS_DB=0
REDIS_CACHE_DB=1
# dump
DUMP_DISK=dumps
DUMP_DISK_ROOT=
DUMP_URL=http://localhost
DUMP_PATH=
# elastic client
ELASTIC_CONNECTION=default
ELASTIC_HOST=http://localhost:9200
@@ -136,11 +142,31 @@ AUDIO_FRA_STREAM_READS=
AUDIO_FRA_DISABLE_ASSERTS=
AUDIO_FRA_VISIBILITY=
DUMP_ACCESS_KEY_ID=
DUMP_SECRET_ACCESS_KEY=
DUMP_DEFAULT_REGION=
DUMP_ENDPOINT=
DUMP_BUCKET=
DUMP_STREAM_READS=
DUMP_DISABLE_ASSERTS=
DUMP_VISIBILITY=
SCRIPT_ACCESS_KEY_ID=
SCRIPT_SECRET_ACCESS_KEY=
SCRIPT_DEFAULT_REGION=
SCRIPT_ENDPOINT=
SCRIPT_BUCKET=
SCRIPT_STREAM_READS=
SCRIPT_DISABLE_ASSERTS=
SCRIPT_VISIBILITY=
# flags
ALLOW_VIDEO_STREAMS=false
ALLOW_AUDIO_STREAMS=false
ALLOW_DISCORD_NOTIFICATIONS=false
ALLOW_VIEW_RECORDING=false
ALLOW_DUMP_DOWNLOADING=false
ALLOW_SCRIPT_DOWNLOADING=false
# fortify
FORTIFY_PATH=
@@ -241,6 +267,11 @@ VIDEO_NGINX_REDIRECT=
VIDEO_ENCODER_VERSION=
VIDEO_UPLOAD_DISKS=
SCRIPT_DISK=scripts
SCRIPT_DISK_ROOT=
SCRIPT_URL=http://localhost
SCRIPT_PATH=/videoscript
# web
WEB_URL=http://localhost
WEB_PATH=
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.2'
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
- uses: actions/checkout@v3
- name: Copy .env
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
php-version: '8.2'
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
- uses: actions/checkout@v3
- name: Copy .env
+7
View File
@@ -2,15 +2,22 @@
/public/hot
/public/storage
/public/build
/public/audios
/public/images
/public/videos
/public/dumps
/public/scripts
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/.fleet
.vscode
.idea
auth.json
+27
View File
@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Actions;
use App\Enums\Actions\ActionStatus;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
/**
* Class ActionResult.
@@ -50,4 +52,29 @@ class ActionResult
{
return ActionStatus::FAILED()->is($this->status);
}
/**
* Write results to log.
*
* @return void
*/
public function toLog(): void
{
$this->hasFailed()
? Log::error($this->getMessage())
: Log::info($this->getMessage());
}
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void
{
$this->hasFailed()
? $command->error($this->getMessage())
: $command->info($this->getMessage());
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Admin\Dump;
use App\Actions\Http\DownloadAction;
use App\Constants\Config\DumpConstants;
use App\Models\Admin\Dump;
use Illuminate\Support\Facades\Config;
/**
* Class DumpDownloadAction.
*
* @extends DownloadAction<Dump>
*/
class DumpDownloadAction extends DownloadAction
{
/**
* Create a new action instance.
*
* @param Dump $dump
*/
public function __construct(Dump $dump)
{
parent::__construct($dump);
}
/**
* Get the path of the resource in storage.
*
* @return string
*/
protected function path(): string
{
return $this->model->path;
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(DumpConstants::DISK_QUALIFIED);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http;
use App\Contracts\Storage\InteractsWithDisk;
use App\Models\BaseModel;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Class DownloadAction.
*
* @template TModel of \App\Models\BaseModel
*/
abstract class DownloadAction implements InteractsWithDisk
{
/**
* Create a new action instance.
*
* @param TModel $model
*/
public function __construct(protected readonly BaseModel $model)
{
}
/**
* Download the resource.
*
* @return StreamedResponse
*/
public function download(): StreamedResponse
{
/** @var FilesystemAdapter $fs */
$fs = Storage::disk($this->disk());
return $fs->download($this->path());
}
/**
* Get the path of the resource in storage.
*
* @return string
*/
abstract protected function path(): string;
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class NginxStreamAction.
*/
abstract class NginxStreamAction extends StreamAction
{
/**
* Stream the resource.
*
* @return Response
*/
public function stream(): Response
{
$fs = Storage::disk($this->disk());
// Generate temporary link for the object
$temporaryURL = $fs->temporaryUrl($this->streamable->path(), now()->addMinutes(5));
// Get the url information
$url_scheme = parse_url($temporaryURL, PHP_URL_SCHEME);
$url_host = parse_url($temporaryURL, PHP_URL_HOST);
$url_path_query = parse_url($temporaryURL, PHP_URL_PATH).'?'.parse_url($temporaryURL, PHP_URL_QUERY);
// Construct the new link for the redirect
$link = "{$this->nginxRedirect()}$url_scheme/$url_host$url_path_query";
$response = new Response();
$disposition = $response->headers->makeDisposition('inline', $this->streamable->basename());
return $response->withHeaders([
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->streamable->mimetype(),
'Content-Length' => $this->streamable->size(),
'Content-Disposition' => $disposition,
'X-Accel-Redirect' => $link,
]);
}
/**
* Get the location of the nginx internal redirect.
*
* @return string
*/
abstract protected function nginxRedirect(): string;
}
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Class ResponseStreamAction.
*/
abstract class ResponseStreamAction extends StreamAction
{
/**
* Stream the resource.
*
* @return StreamedResponse
*/
public function stream(): StreamedResponse
{
$response = new StreamedResponse();
$disposition = $response->headers->makeDisposition('inline', $this->streamable->basename());
$response->headers->replace([
'Accept-Ranges' => 'bytes',
'Content-Type' => $this->streamable->mimetype(),
'Content-Length' => $this->streamable->size(),
'Content-Disposition' => $disposition,
]);
$fs = Storage::disk($this->disk());
$response->setCallback(function () use ($fs) {
$stream = $fs->readStream($this->streamable->path());
fpassthru($stream);
fclose($stream);
});
return $response;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http;
use App\Contracts\Models\Streamable;
use App\Contracts\Storage\InteractsWithDisk;
use Symfony\Component\HttpFoundation\Response;
/**
* Class StreamAction.
*/
abstract class StreamAction implements InteractsWithDisk
{
/**
* Create a new action instance.
*
* @param Streamable $streamable
*/
public function __construct(protected readonly Streamable $streamable)
{
}
/**
* Stream the resource.
*
* @return Response
*/
abstract public function stream(): Response;
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Wiki\Audio;
use App\Actions\Http\NginxStreamAction;
use App\Constants\Config\AudioConstants;
use App\Models\Wiki\Audio;
use Illuminate\Support\Facades\Config;
/**
* Class AudioNginxStreamAction.
*/
class AudioNginxStreamAction extends NginxStreamAction
{
/**
* Create a new action instance.
*
* @param Audio $audio
*/
public function __construct(Audio $audio)
{
parent::__construct($audio);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Get the location of the nginx internal redirect.
*
* @return string
*/
protected function nginxRedirect(): string
{
return Config::get(AudioConstants::NGINX_REDIRECT_QUALIFIED);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Wiki\Audio;
use App\Actions\Http\ResponseStreamAction;
use App\Constants\Config\AudioConstants;
use App\Models\Wiki\Audio;
use Illuminate\Support\Facades\Config;
/**
* Class AudioResponseStreamAction.
*/
class AudioResponseStreamAction extends ResponseStreamAction
{
/**
* Create a new action instance.
*
* @param Audio $audio
*/
public function __construct(Audio $audio)
{
parent::__construct($audio);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
}
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Wiki\Video\Script;
use App\Actions\Http\DownloadAction;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Facades\Config;
/**
* Class ScriptDownloadAction.
*
* @extends DownloadAction<VideoScript>
*/
class ScriptDownloadAction extends DownloadAction
{
/**
* Create a new action instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* Get the path of the resource in storage.
*
* @return string
*/
protected function path(): string
{
return $this->model->path;
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Wiki\Video;
use App\Actions\Http\NginxStreamAction;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video;
use Illuminate\Support\Facades\Config;
/**
* Class VideoNginxStreamAction.
*/
class VideoNginxStreamAction extends NginxStreamAction
{
/**
* Create a new action instance.
*
* @param Video $video
*/
public function __construct(Video $video)
{
parent::__construct($video);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Get the location of the nginx internal redirect.
*
* @return string
*/
protected function nginxRedirect(): string
{
return Config::get(VideoConstants::NGINX_REDIRECT_QUALIFIED);
}
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Actions\Http\Wiki\Video;
use App\Actions\Http\ResponseStreamAction;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video;
use Illuminate\Support\Facades\Config;
/**
* Class VideoResponseStreamAction.
*/
class VideoResponseStreamAction extends ResponseStreamAction
{
/**
* Create a new action instance.
*
* @param Video $video
*/
public function __construct(Video $video)
{
parent::__construct($video);
}
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
}
@@ -53,6 +53,6 @@ class BackfillLargeCoverImageAction extends BackfillStudioImageAction
*/
protected function getMalImage(ExternalResource $malResource): ?Image
{
return $this->createImage("https://cdn.myanimelist.net/img/common/companies/$malResource->external_id.png");
return $this->createImage("https://cdn.myanimelist.net/images/company/$malResource->external_id.png");
}
}
@@ -7,7 +7,7 @@ namespace App\Actions\Models\Wiki\Video\Audio;
use App\Actions\ActionResult;
use App\Actions\Models\BaseAction;
use App\Actions\Repositories\ReconcileResults;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositoriesAction;
use App\Constants\Config\AudioConstants;
use App\Constants\Config\VideoConstants;
use App\Contracts\Repositories\RepositoryInterface;
@@ -256,7 +256,7 @@ class BackfillAudioAction extends BaseAction
*/
protected function reconcileAudio(string $audioPath): ReconcileResults
{
$action = new ReconcileAudioRepositories();
$action = new ReconcileAudioRepositoriesAction();
/** @var RepositoryInterface $sourceRepository */
$sourceRepository = App::make(AudioSourceRepository::class);
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Admin\Dump;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Admin\Dump;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class ReconcileDumpRepositories.
*
* @extends ReconcileRepositoriesAction<Dump>
*/
class ReconcileDumpRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
Dump::ATTRIBUTE_ID,
Dump::ATTRIBUTE_PATH,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (Dump $first, Dump $second) => [$first->path] <=> [$second->path];
}
/**
* The columns used for update set operation.
*
* @return string[]
*/
protected function columnsForUpdate(): array
{
return ['*'];
}
/**
* Callback for update set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForUpdate(): Closure
{
return fn () => 0;
}
/**
* Get source model that has been updated for destination model.
*
* @param Collection $sourceModels
* @param Model $destinationModel
* @return Model|null
*/
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
{
return null;
}
/**
* Get reconciliation results.
*
* @param Collection $created
* @param Collection $deleted
* @param Collection $updated
* @return ReconcileResults
*/
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
{
return new ReconcileDumpResults($created, $deleted, $updated);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Admin\Dump;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Admin\Dump;
/**
* Class ReconcileDumpResults.
*
* @extends ReconcileResults<Dump>
*/
class ReconcileDumpResults extends ReconcileResults
{
/**
* Get the model of the reconciliation results.
*
* @return class-string<Dump>
*/
protected function model(): string
{
return Dump::class;
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Repositories\Billing\Balance;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Billing\Balance;
@@ -15,9 +15,9 @@ use Illuminate\Support\Collection;
/**
* Class ReconcileBalanceRepositories.
*
* @extends ReconcileRepositories<Balance>
* @extends ReconcileRepositoriesAction<Balance>
*/
class ReconcileBalanceRepositories extends ReconcileRepositories
class ReconcileBalanceRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Repositories\Billing\Transaction;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Billing\Transaction;
@@ -15,9 +15,9 @@ use Illuminate\Support\Collection;
/**
* Class ReconcileTransactionRepositories.
*
* @extends ReconcileRepositories<Transaction>
* @extends ReconcileRepositoriesAction<Transaction>
*/
class ReconcileTransactionRepositories extends ReconcileRepositories
class ReconcileTransactionRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
@@ -14,7 +14,7 @@ use Illuminate\Support\Collection;
*
* @template TModel of \App\Models\BaseModel
*/
abstract class ReconcileRepositories
abstract class ReconcileRepositoriesAction
{
/**
* Perform set reconciliation between source and destination repositories.
+40 -11
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Actions\Repositories;
use App\Actions\ActionResult;
use App\Enums\Actions\ActionStatus;
use App\Models\BaseModel;
use Countable;
use Illuminate\Console\Command;
@@ -16,7 +18,7 @@ use Illuminate\Support\Str;
*
* @template TModel of \App\Models\BaseModel
*/
abstract class ReconcileResults
abstract class ReconcileResults extends ActionResult
{
/**
* Create a new results instance.
@@ -26,10 +28,11 @@ abstract class ReconcileResults
* @param Collection $updated
*/
public function __construct(
protected readonly Collection $created,
protected readonly Collection $deleted,
protected readonly Collection $updated
protected readonly Collection $created = new Collection(),
protected readonly Collection $deleted = new Collection(),
protected readonly Collection $updated = new Collection()
) {
parent::__construct(ActionStatus::PASSED());
}
/**
@@ -42,12 +45,32 @@ abstract class ReconcileResults
return $this->created;
}
/**
* Get deleted models.
*
* @return Collection
*/
public function getDeleted(): Collection
{
return $this->deleted;
}
/**
* Get updated models.
*
* @return Collection
*/
public function getUpdated(): Collection
{
return $this->updated;
}
/**
* Determines if any successful changes were made during reconciliation.
*
* @return bool
*/
protected function hasChanges(): bool
public function hasChanges(): bool
{
return $this->created->isNotEmpty() || $this->deleted->isNotEmpty() || $this->updated->isNotEmpty();
}
@@ -56,6 +79,8 @@ abstract class ReconcileResults
* Write reconcile results to log.
*
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toLog(): void
{
@@ -63,7 +88,7 @@ abstract class ReconcileResults
$this->deleted->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' deleted"));
$this->updated->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' updated"));
Log::info($this->summary());
Log::info($this->getMessage());
}
/**
@@ -71,6 +96,8 @@ abstract class ReconcileResults
*
* @param Command $command
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function toConsole(Command $command): void
{
@@ -78,15 +105,17 @@ abstract class ReconcileResults
$this->deleted->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' deleted"));
$this->updated->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' updated"));
$command->info($this->summary());
$command->info($this->getMessage());
}
/**
* Get the summary line.
* Get the action result message.
*
* @return string
* @return string|null
*
* @noinspection PhpMissingParentCallCommonInspection
*/
public function summary(): string
public function getMessage(): ?string
{
if ($this->hasChanges()) {
return "{$this->created->count()} {$this->label($this->created)} created, {$this->deleted->count()} {$this->label($this->deleted)} deleted, {$this->updated->count()} {$this->label($this->updated)} updated";
@@ -110,6 +139,6 @@ abstract class ReconcileResults
*/
protected function label(int|array|Countable $models = 1): string
{
return Str::plural(class_basename($this->model()), $models);
return Str::plural(Str::headline(class_basename($this->model())), $models);
}
}
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Audio;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Audio;
use Closure;
@@ -14,9 +14,9 @@ use Illuminate\Support\Collection;
/**
* Class ReconcileAudioRepositories.
*
* @extends ReconcileRepositories<Audio>
* @extends ReconcileRepositoriesAction<Audio>
*/
class ReconcileAudioRepositories extends ReconcileRepositories
class ReconcileAudioRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Video;
use Closure;
@@ -14,9 +14,9 @@ use Illuminate\Support\Collection;
/**
* Class ReconcileVideoRepositories.
*
* @extends ReconcileRepositories<Video>
* @extends ReconcileRepositoriesAction<Video>
*/
class ReconcileVideoRepositories extends ReconcileRepositories
class ReconcileVideoRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Video\Script;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Video\VideoScript;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Class ReconcileScriptRepositoriesAction.
*
* @extends ReconcileRepositoriesAction<VideoScript>
*/
class ReconcileScriptRepositoriesAction extends ReconcileRepositoriesAction
{
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
VideoScript::ATTRIBUTE_ID,
VideoScript::ATTRIBUTE_PATH,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (VideoScript $first, VideoScript $second) => [$first->path] <=> [$second->path];
}
/**
* The columns used for update set operation.
*
* @return string[]
*/
protected function columnsForUpdate(): array
{
return ['*'];
}
/**
* Callback for update set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForUpdate(): Closure
{
return fn () => 0;
}
/**
* Get source model that has been updated for destination model.
*
* @param Collection $sourceModels
* @param Model $destinationModel
* @return Model|null
*/
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
{
return null;
}
/**
* Get reconciliation results.
*
* @param Collection $created
* @param Collection $deleted
* @param Collection $updated
* @return ReconcileResults
*/
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
{
return new ReconcileScriptResults($created, $deleted, $updated);
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Actions\Repositories\Wiki\Video\Script;
use App\Actions\Repositories\ReconcileResults;
use App\Models\Wiki\Video\VideoScript;
/**
* Class ReconcileScriptResults.
*
* @extends ReconcileResults<VideoScript>
*/
class ReconcileScriptResults extends ReconcileResults
{
/**
* Get the model of the reconciliation results.
*
* @return class-string<VideoScript>
*/
protected function model(): string
{
return VideoScript::class;
}
}
@@ -2,21 +2,23 @@
declare(strict_types=1);
namespace App\Console\Commands;
namespace App\Actions\Storage\Admin\Dump;
use App\Actions\ActionResult;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Constants\Config\DumpConstants;
use App\Enums\Actions\ActionStatus;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Database\Connection;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Support\Facades\Date;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use PDO;
use RuntimeException;
use Spatie\DbDumper\Databases\MySql;
@@ -26,29 +28,29 @@ use Spatie\DbDumper\DbDumper;
use Spatie\DbDumper\Exceptions\CannotSetParameter;
/**
* Class DatabaseDumpCommand.
* Class DumpAction.
*/
abstract class DatabaseDumpCommand extends Command
abstract class DumpAction
{
use ReconcilesRepositories;
/**
* Execute the console command.
* Create a new action instance.
*
* @return int
* @param array<string, mixed> $options
*/
public function handle(): int
public function __construct(protected readonly array $options = [])
{
$validator = Validator::make($this->options(), [
'default-character-set' => ['string'],
'set-gtid-purged' => [Rule::in(['OFF', 'ON', 'AUTO'])->__toString()],
]);
}
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$this->error($error);
}
return 1;
}
/**
* Handle action.
*
* @return ActionResult
*/
public function handle(): ActionResult
{
$dumpFile = $this->getDumpFile();
try {
/** @var Connection $connection */
@@ -56,28 +58,38 @@ abstract class DatabaseDumpCommand extends Command
$dumper = $this->getDumper($connection);
if ($dumper === null) {
Log::error("Unrecognized connection '{$connection->getName()}'");
$this->error("Unrecognized connection '{$connection->getName()}'");
return 1;
return new ActionResult(
ActionStatus::FAILED(),
"Unrecognized connection '{$connection->getName()}'"
);
}
$dumpFile = $this->getDumpFile();
// First, Dump file to temporary location
// Writing to disks is not fully supported
$dumper->dumpToFile($dumpFile);
// Assume success if no exceptions were thrown
// The library will check if the file exists and is not empty
Log::info("Database dump '$dumpFile' has been created");
$this->info("Database dump '$dumpFile' has been created");
} catch (Exception $e) {
Log::error($e->getMessage());
$this->error($e->getMessage());
// Then, store dump file in intended location
/** @var FilesystemAdapter $fs */
$fs = Storage::disk(Config::get(DumpConstants::DISK_QUALIFIED));
$fs->putFileAs('', $dumpFile, File::basename($dumpFile));
return 1;
// Finally, delete the temporary file
File::delete($dumpFile);
} catch (Exception $e) {
return new ActionResult(
ActionStatus::FAILED(),
$e->getMessage()
);
}
return 0;
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
return new ActionResult(
ActionStatus::PASSED(),
"Database dump '$dumpFile' has been created",
);
}
/**
@@ -244,31 +256,35 @@ abstract class DatabaseDumpCommand extends Command
}
/**
* The target path for the database dump.
* Pattern: "/path/to/project/storage/db-dumps/{path}/animethemes-db-dump-{milliseconds from epoch}.sql".
* Determine if the string option is set.
*
* @return string
* @param string $key
* @return bool
*/
protected function getDumpFile(): string
protected function hasOption(string $key): bool
{
$filesystem = Storage::disk('db-dumps');
$filesystem->makeDirectory($this->getDumpFilePath());
return Str::of($filesystem->path($this->getDumpFilePath()))
->append(DIRECTORY_SEPARATOR)
->append('animethemes-db-dump-')
->append(intval(Date::now()->valueOf()))
->append('.sql')
->__toString();
return is_string(Arr::get($this->options, $key));
}
/**
* The directory that the file should be dumped to.
* Get the option by key.
*
* @param string $key
* @return mixed
*/
protected function option(string $key): mixed
{
return Arr::get($this->options, $key);
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-{concern}-{milliseconds from epoch}.sql".
*
* @return string
*/
abstract protected function getDumpFilePath(): string;
abstract protected function getDumpFile(): string;
/**
* The list of tables to include in the dump.
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\Document\Page;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpDocumentAction.
*/
class DumpDocumentAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-document-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Page::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-document-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpDocumentAction::FILENAME_PREFIX)
->append(intval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeSynonym;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Audio;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use App\Models\Wiki\Series;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use App\Pivots\AnimeImage;
use App\Pivots\AnimeResource;
use App\Pivots\AnimeSeries;
use App\Pivots\AnimeStudio;
use App\Pivots\AnimeThemeEntryVideo;
use App\Pivots\ArtistImage;
use App\Pivots\ArtistMember;
use App\Pivots\ArtistResource;
use App\Pivots\ArtistSong;
use App\Pivots\StudioImage;
use App\Pivots\StudioResource;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class DumpWikiAction.
*/
class DumpWikiAction extends DumpAction
{
use ReconcilesDumpRepositories;
final public const FILENAME_PREFIX = 'animethemes-db-dump-wiki-';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Anime::TABLE,
AnimeImage::TABLE,
AnimeResource::TABLE,
AnimeSeries::TABLE,
AnimeStudio::TABLE,
AnimeSynonym::TABLE,
AnimeTheme::TABLE,
AnimeThemeEntry::TABLE,
AnimeThemeEntryVideo::TABLE,
Artist::TABLE,
ArtistImage::TABLE,
ArtistMember::TABLE,
ArtistResource::TABLE,
ArtistSong::TABLE,
Audio::TABLE,
ExternalResource::TABLE,
Image::TABLE,
Series::TABLE,
Song::TABLE,
Studio::TABLE,
StudioImage::TABLE,
StudioResource::TABLE,
Video::TABLE,
VideoScript::TABLE,
];
}
/**
* The temporary path for the database dump.
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
* Pattern: "animethemes-db-dump-wiki-{milliseconds from epoch}.sql".
*
* @return string
*/
protected function getDumpFile(): string
{
$filesystem = Storage::disk('local');
return Str::of($filesystem->path(''))
->append(DumpWikiAction::FILENAME_PREFIX)
->append(intval(Date::now()->valueOf()))
->append('.sql')
->__toString();
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Admin\Dump;
use App\Actions\Storage\Base\PruneAction;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Constants\Config\DumpConstants;
use Illuminate\Support\Facades\Config;
/**
* Class PruneDumpAction.
*/
class PruneDumpAction extends PruneAction
{
use ReconcilesDumpRepositories;
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(DumpConstants::DISK_QUALIFIED);
}
}
+19 -44
View File
@@ -4,11 +4,11 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileResults;
use App\Actions\Storage\StorageAction;
use App\Actions\Storage\StorageResults;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Contracts\Actions\Storage\StorageAction;
use App\Contracts\Actions\Storage\StorageResults;
use App\Contracts\Repositories\RepositoryInterface;
use App\Contracts\Storage\InteractsWithDisks;
use App\Models\BaseModel;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\File;
@@ -19,8 +19,10 @@ use Illuminate\Support\Facades\Storage;
*
* @template TModel of \App\Models\BaseModel
*/
abstract class DeleteAction extends StorageAction
abstract class DeleteAction implements InteractsWithDisks, StorageAction
{
use ReconcilesRepositories;
/**
* Create a new action instance.
*
@@ -48,65 +50,38 @@ abstract class DeleteAction extends StorageAction
$results[$disk] = $result;
}
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
return new DeleteResults($this->model, $results);
}
/**
* Reconcile storage repositories.
*
* @return ReconcileResults
*/
protected function reconcileRepositories(): ReconcileResults
{
$action = $this->action();
$sourceRepository = $this->getSourceRepository();
$destinationRepository = $this->getDestinationRepository();
$this->handleFilters($sourceRepository, $destinationRepository);
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
abstract protected function getSourceRepository(): RepositoryInterface;
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
abstract protected function getDestinationRepository(): RepositoryInterface;
/**
* Apply filters to repositories before reconciliation.
*
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @param array $data
* @return void
*/
protected function handleFilters(
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository
RepositoryInterface $destinationRepository,
array $data = []
): void {
$sourceRepository->handleFilter('path', File::dirname($this->path()));
$destinationRepository->handleFilter('path', File::dirname($this->path()));
}
/**
* Get the reconcile action.
* Processes to be completed after handling action.
*
* @return ReconcileRepositories
* @param StorageResults $storageResults
* @return void
*/
abstract protected function action(): ReconcileRepositories;
public function then(StorageResults $storageResults): void
{
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
}
/**
* Get the path to delete.
+21 -2
View File
@@ -5,16 +5,17 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\ActionResult;
use App\Actions\Storage\StorageResults;
use App\Contracts\Actions\Storage\StorageResults;
use App\Enums\Actions\ActionStatus;
use App\Models\BaseModel;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class DeleteResults.
*/
class DeleteResults extends StorageResults
class DeleteResults implements StorageResults
{
/**
* Create a new action result instance.
@@ -43,6 +44,24 @@ class DeleteResults extends StorageResults
}
}
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void
{
if (empty($this->deletions)) {
$command->error('No deletions were attempted.');
}
foreach ($this->deletions as $fs => $result) {
$result
? $command->info("Deleted '{$this->model->getName()}' from disk '$fs'")
: $command->error("Failed to delete '{$this->model->getName()}' from disk '$fs'");
}
}
/**
* Transform to Action Result.
*
+15 -5
View File
@@ -4,8 +4,9 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\Storage\StorageAction;
use App\Actions\Storage\StorageResults;
use App\Contracts\Actions\Storage\StorageAction;
use App\Contracts\Actions\Storage\StorageResults;
use App\Contracts\Storage\InteractsWithDisks;
use App\Models\BaseModel;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
@@ -15,7 +16,7 @@ use Illuminate\Support\Facades\Storage;
*
* @template TModel of \App\Models\BaseModel
*/
abstract class MoveAction extends StorageAction
abstract class MoveAction implements InteractsWithDisks, StorageAction
{
/**
* Create a new action instance.
@@ -46,11 +47,20 @@ abstract class MoveAction extends StorageAction
$results[$disk] = $result;
}
$this->update();
return new MoveResults($this->model, $from, $this->to, $results);
}
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void
{
$this->update();
}
/**
* Get the path to move from.
*
+21 -2
View File
@@ -5,16 +5,17 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\ActionResult;
use App\Actions\Storage\StorageResults;
use App\Contracts\Actions\Storage\StorageResults;
use App\Enums\Actions\ActionStatus;
use App\Models\BaseModel;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class MoveResults.
*/
class MoveResults extends StorageResults
class MoveResults implements StorageResults
{
/**
* Create a new action result instance.
@@ -49,6 +50,24 @@ class MoveResults extends StorageResults
}
}
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void
{
if (empty($this->moves)) {
$command->error('No moves were attempted.');
}
foreach ($this->moves as $fs => $result) {
$result
? $command->info("Moved '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'")
: $command->error("Failed to move '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'");
}
}
/**
* Transform to Action Result.
*
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Contracts\Actions\Storage\StorageAction;
use App\Contracts\Actions\Storage\StorageResults;
use App\Contracts\Storage\InteractsWithDisk;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Storage;
/**
* Class PruneAction.
*/
abstract class PruneAction implements InteractsWithDisk, StorageAction
{
use ReconcilesRepositories;
/**
* Create a new action instance.
*
* @param int $hours
*/
public function __construct(protected readonly int $hours = 72)
{
}
/**
* Handle action.
*
* @return StorageResults
*/
public function handle(): StorageResults
{
$fs = Storage::disk($this->disk());
$pruneDate = Date::now()->subHours($this->hours);
$results = [];
foreach ($fs->allFiles() as $path) {
$lastModified = Date::createFromTimestamp($fs->lastModified($path));
if ($lastModified->isBefore($pruneDate)) {
$result = $fs->delete($path);
$results[$path] = $result;
}
}
return new PruneResults($this->disk(), $results);
}
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void
{
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\ActionResult;
use App\Contracts\Actions\Storage\StorageResults;
use App\Enums\Actions\ActionStatus;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class PruneResults.
*/
class PruneResults implements StorageResults
{
/**
* Create a new action result instance.
*
* @param string $fs
* @param array<string, bool> $prunings
*/
public function __construct(protected readonly string $fs, protected readonly array $prunings = [])
{
}
/**
* Write results to log.
*
* @return void
*/
public function toLog(): void
{
if (empty($this->prunings)) {
Log::error('No prunings were attempted.');
}
foreach ($this->prunings as $path => $result) {
$result === false
? Log::error("Failed to prune '$path' from disk '$this->fs'")
: Log::info("Pruned '$path' from disk '$this->fs'");
}
}
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void
{
if (empty($this->prunings)) {
$command->error('No prunings were attempted.');
}
foreach ($this->prunings as $path => $result) {
$result === false
? $command->error("Failed to prune '$path' from disk '$this->fs'")
: $command->info("Pruned '$path' from disk '$this->fs'");
}
}
/**
* Transform to Action Result.
*
* @return ActionResult
*/
public function toActionResult(): ActionResult
{
if (empty($this->prunings)) {
return new ActionResult(
ActionStatus::FAILED(),
'No prunings were attempted.'
);
}
/** @var Collection $passed */
/** @var Collection $failed */
[$passed, $failed] = collect($this->prunings)->partition(fn (bool $result, string $path) => $result);
if ($failed->isNotEmpty()) {
return new ActionResult(
ActionStatus::FAILED(),
"Failed to prune {$failed->keys()->join(', ', ' & ')} from disk '$this->fs'."
);
}
return new ActionResult(
ActionStatus::PASSED(),
"Pruned {$passed->keys()->join(', ', ' & ')} from disk '$this->fs'."
);
}
}
+4 -63
View File
@@ -4,11 +4,9 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\ReconcileResults;
use App\Actions\Storage\StorageAction;
use App\Actions\Storage\StorageResults;
use App\Contracts\Repositories\RepositoryInterface;
use App\Contracts\Actions\Storage\StorageAction;
use App\Contracts\Actions\Storage\StorageResults;
use App\Contracts\Storage\InteractsWithDisks;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
@@ -16,7 +14,7 @@ use Illuminate\Support\Facades\Storage;
/**
* Class UploadAction.
*/
abstract class UploadAction extends StorageAction
abstract class UploadAction implements InteractsWithDisks, StorageAction
{
/**
* Create a new action instance.
@@ -46,63 +44,6 @@ abstract class UploadAction extends StorageAction
$results[$disk] = $result;
}
$reconcileResults = $this->reconcileRepositories();
$reconcileResults->toLog();
return new UploadResults($results);
}
/**
* Reconcile storage repositories.
*
* @return ReconcileResults
*/
protected function reconcileRepositories(): ReconcileResults
{
$action = $this->action();
$sourceRepository = $this->getSourceRepository();
$destinationRepository = $this->getDestinationRepository();
$this->handleFilters($sourceRepository, $destinationRepository);
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
abstract protected function getSourceRepository(): RepositoryInterface;
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
abstract protected function getDestinationRepository(): RepositoryInterface;
/**
* Apply filters to repositories before reconciliation.
*
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @return void
*/
protected function handleFilters(
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository
): void {
$sourceRepository->handleFilter('path', $this->path);
$destinationRepository->handleFilter('path', $this->path);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositories
*/
abstract protected function action(): ReconcileRepositories;
}
+21 -2
View File
@@ -5,15 +5,16 @@ declare(strict_types=1);
namespace App\Actions\Storage\Base;
use App\Actions\ActionResult;
use App\Actions\Storage\StorageResults;
use App\Contracts\Actions\Storage\StorageResults;
use App\Enums\Actions\ActionStatus;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/**
* Class UploadResults.
*/
class UploadResults extends StorageResults
class UploadResults implements StorageResults
{
/**
* Create a new action result instance.
@@ -41,6 +42,24 @@ class UploadResults extends StorageResults
}
}
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void
{
if (empty($this->uploads)) {
$command->error('No uploads were attempted.');
}
foreach ($this->uploads as $fs => $result) {
$result === false
? $command->error("Failed to upload to disk '$fs'")
: $command->info("Uploaded '$result' to disk '$fs'");
}
}
/**
* Transform to Action Result.
*
-25
View File
@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage;
/**
* Class StorageAction.
*/
abstract class StorageAction
{
/**
* Handle action.
*
* @return StorageResults
*/
abstract public function handle(): StorageResults;
/**
* Get the disks to update.
*
* @return array
*/
abstract protected function disks(): array;
}
-27
View File
@@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage;
use App\Actions\ActionResult;
/**
* Class StorageResults.
*/
abstract class StorageResults
{
/**
* Write results to log.
*
* @return void
*/
abstract public function toLog(): void;
/**
* Transform to Action Result.
*
* @return ActionResult
*/
abstract public function toActionResult(): ActionResult;
}
@@ -4,15 +4,10 @@ declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Audio;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
use App\Actions\Storage\Base\DeleteAction;
use App\Concerns\Repositories\Wiki\ReconcilesAudioRepositories;
use App\Constants\Config\AudioConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Models\Wiki\Audio;
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
/**
@@ -22,6 +17,8 @@ use Illuminate\Support\Facades\Config;
*/
class DeleteAudioAction extends DeleteAction
{
use ReconcilesAudioRepositories;
/**
* Create a new action instance.
*
@@ -33,45 +30,15 @@ class DeleteAudioAction extends DeleteAction
}
/**
* Get the disks to update.
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(AudioConstants::DISKS_QUALIFIED);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
protected function getSourceRepository(): RepositoryInterface
{
return App::make(AudioSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
protected function getDestinationRepository(): RepositoryInterface
{
return App::make(AudioDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositories
*/
protected function action(): ReconcileRepositories
{
return new ReconcileAudioRepositories();
}
/**
* Get the path to delete.
*
@@ -29,11 +29,11 @@ class MoveAudioAction extends MoveAction
}
/**
* Get the disks to update.
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(AudioConstants::DISKS_QUALIFIED);
}
@@ -4,15 +4,13 @@ declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Audio;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
use App\Actions\Storage\Base\UploadAction;
use App\Constants\Config\AudioConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
use Illuminate\Support\Facades\App;
use App\Contracts\Actions\Storage\StorageResults;
use App\Models\Wiki\Audio;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* Class UploadAudioAction.
@@ -20,42 +18,54 @@ use Illuminate\Support\Facades\Config;
class UploadAudioAction extends UploadAction
{
/**
* Get the disks to update.
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void
{
if ($storageResults->toActionResult()->hasFailed()) {
return;
}
$this->getOrCreateAudio();
}
/**
* Get existing or create new audio for file upload.
*
* @return Audio
*/
protected function getOrCreateAudio(): Audio
{
$path = Str::of($this->path)
->finish(DIRECTORY_SEPARATOR)
->append($this->file->getClientOriginalName())
->__toString();
$attributes = [
Audio::ATTRIBUTE_FILENAME => File::name($this->file->getClientOriginalName()),
Audio::ATTRIBUTE_MIMETYPE => $this->file->getMimeType(),
Audio::ATTRIBUTE_PATH => $path,
Audio::ATTRIBUTE_SIZE => $this->file->getSize(),
];
return Audio::updateOrCreate(
[
Audio::ATTRIBUTE_BASENAME => $this->file->getClientOriginalName(),
],
$attributes
);
}
/**
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(AudioConstants::DISKS_QUALIFIED);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
protected function getSourceRepository(): RepositoryInterface
{
return App::make(AudioSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
protected function getDestinationRepository(): RepositoryInterface
{
return App::make(AudioDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositories
*/
protected function action(): ReconcileRepositories
{
return new ReconcileAudioRepositories();
}
}
@@ -4,15 +4,10 @@ declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
use App\Actions\Storage\Base\DeleteAction;
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
use App\Constants\Config\VideoConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Models\Wiki\Video;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
/**
@@ -22,6 +17,8 @@ use Illuminate\Support\Facades\Config;
*/
class DeleteVideoAction extends DeleteAction
{
use ReconcilesVideoRepositories;
/**
* Create a new action instance.
*
@@ -33,45 +30,15 @@ class DeleteVideoAction extends DeleteAction
}
/**
* Get the disks to update.
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(VideoConstants::DISKS_QUALIFIED);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
protected function getSourceRepository(): RepositoryInterface
{
return App::make(VideoSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
protected function getDestinationRepository(): RepositoryInterface
{
return App::make(VideoDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositories
*/
protected function action(): ReconcileRepositories
{
return new ReconcileVideoRepositories();
}
/**
* Get the path to delete.
*
@@ -29,11 +29,11 @@ class MoveVideoAction extends MoveAction
}
/**
* Get the disks to update.
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(VideoConstants::DISKS_QUALIFIED);
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Base\DeleteAction;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class DeleteScriptAction.
*
* @extends DeleteAction<VideoScript>
*/
class DeleteScriptAction extends DeleteAction
{
use ReconcilesScriptRepositories;
/**
* Create a new action instance.
*
* @param VideoScript $script
*/
public function __construct(VideoScript $script)
{
parent::__construct($script);
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
}
/**
* Get the path to delete.
*
* @return string
*/
protected function path(): string
{
return $this->model->path;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Base\MoveAction;
use App\Constants\Config\VideoConstants;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
/**
* Class MoveVideoAction.
*
* @extends MoveAction<VideoScript>
*/
class MoveScriptAction extends MoveAction
{
/**
* Create a new action instance.
*
* @param VideoScript $script
* @param string $to
*/
public function __construct(VideoScript $script, string $to)
{
parent::__construct($script, $to);
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
}
/**
* Get the path to move from.
*
* @return string
*/
protected function from(): string
{
return $this->model->path;
}
/**
* Update underlying model.
* We want to apply these updates through Eloquent to preserve relations when renaming.
* Otherwise, reconciliation would destroy the old model and create a new model for the new name.
*
* @return void
*/
protected function update(): void
{
$this->model->update([
VideoScript::ATTRIBUTE_PATH => $this->to,
]);
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video\Script;
use App\Actions\Storage\Base\UploadAction;
use App\Constants\Config\VideoConstants;
use App\Contracts\Actions\Storage\StorageResults;
use App\Models\Wiki\Video;
use App\Models\Wiki\Video\VideoScript;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
/**
* Class UploadScriptAction.
*/
class UploadScriptAction extends UploadAction
{
/**
* Create a new action instance.
*
* @param UploadedFile $file
* @param string $path
* @param Video|null $video
*/
public function __construct(UploadedFile $file, string $path, protected ?Video $video = null)
{
parent::__construct($file, $path);
}
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void
{
if ($storageResults->toActionResult()->hasFailed()) {
return;
}
$this->getOrCreateScript();
}
/**
* Get existing or create new script for file upload.
*
* @return VideoScript
*/
protected function getOrCreateScript(): VideoScript
{
$path = Str::of($this->path)
->finish(DIRECTORY_SEPARATOR)
->append($this->file->getClientOriginalName())
->__toString();
$attributes = [
VideoScript::ATTRIBUTE_PATH => $path,
];
if ($this->video !== null) {
$attributes[VideoScript::ATTRIBUTE_VIDEO] = $this->video->getKey();
}
return VideoScript::updateOrCreate(
[
VideoScript::ATTRIBUTE_PATH => $path,
],
$attributes
);
}
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array
{
return Arr::wrap(Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED));
}
}
@@ -4,15 +4,19 @@ declare(strict_types=1);
namespace App\Actions\Storage\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
use App\Actions\Storage\Base\UploadAction;
use App\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
use App\Constants\Config\VideoConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
use Illuminate\Support\Facades\App;
use App\Contracts\Actions\Storage\StorageResults;
use App\Enums\Models\Wiki\VideoOverlap;
use App\Enums\Models\Wiki\VideoSource;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Video;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
/**
* Class UploadVideoAction.
@@ -20,42 +24,132 @@ use Illuminate\Support\Facades\Config;
class UploadVideoAction extends UploadAction
{
/**
* Get the disks to update.
* Create a new action instance.
*
* @param UploadedFile $file
* @param string $path
* @param array $attributes
* @param AnimeThemeEntry|null $entry
* @param UploadedFile|null $script
*/
public function __construct(
UploadedFile $file,
string $path,
protected array $attributes = [],
protected ?AnimeThemeEntry $entry = null,
protected readonly ?UploadedFile $script = null
) {
parent::__construct($file, $path);
}
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void
{
if ($storageResults->toActionResult()->hasFailed()) {
return;
}
$video = $this->getOrCreateVideo();
$this->attachEntry($video);
$this->uploadScript($video);
}
/**
* Get existing or create new video for file upload.
*
* @return Video
*/
protected function getOrCreateVideo(): Video
{
$path = Str::of($this->path)
->finish(DIRECTORY_SEPARATOR)
->append($this->file->getClientOriginalName())
->__toString();
$attributes = [
Video::ATTRIBUTE_FILENAME => File::name($this->file->getClientOriginalName()),
Video::ATTRIBUTE_MIMETYPE => $this->file->getMimeType(),
Video::ATTRIBUTE_PATH => $path,
Video::ATTRIBUTE_SIZE => $this->file->getSize(),
];
if (Arr::has($this->attributes, Video::ATTRIBUTE_RESOLUTION)) {
$attributes[Video::ATTRIBUTE_RESOLUTION] = Arr::get($this->attributes, Video::ATTRIBUTE_RESOLUTION);
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_NC)) {
$attributes[Video::ATTRIBUTE_NC] = Arr::get($this->attributes, Video::ATTRIBUTE_NC);
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_SUBBED)) {
$attributes[Video::ATTRIBUTE_SUBBED] = Arr::get($this->attributes, Video::ATTRIBUTE_SUBBED);
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_LYRICS)) {
$attributes[Video::ATTRIBUTE_LYRICS] = Arr::get($this->attributes, Video::ATTRIBUTE_LYRICS);
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_UNCEN)) {
$attributes[Video::ATTRIBUTE_UNCEN] = Arr::get($this->attributes, Video::ATTRIBUTE_UNCEN);
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_UNCEN)) {
$overlap = VideoOverlap::unstrictCoerce(Arr::get($this->attributes, Video::ATTRIBUTE_OVERLAP));
if ($overlap !== null) {
$attributes[Video::ATTRIBUTE_OVERLAP] = $overlap;
}
}
if (Arr::has($this->attributes, Video::ATTRIBUTE_SOURCE)) {
$attributes[Video::ATTRIBUTE_SOURCE] = VideoSource::unstrictCoerce(Arr::get($this->attributes, Video::ATTRIBUTE_SOURCE));
}
return Video::updateOrCreate(
[
Video::ATTRIBUTE_BASENAME => $this->file->getClientOriginalName(),
],
$attributes
);
}
/**
* Attach entry to created video if uploaded from entry detail screen.
*
* @param Video $video
* @return void
*/
protected function attachEntry(Video $video): void
{
if ($this->entry !== null && $video->wasRecentlyCreated) {
$video->animethemeentries()->attach($this->entry);
}
}
/**
* Upload & Associate Script if video upload was successful.
*
* @param Video $video
* @return void
*/
protected function uploadScript(Video $video): void
{
if ($this->script !== null) {
$uploadScript = new UploadScriptAction($this->script, $this->path, $video);
$scriptResult = $uploadScript->handle();
$uploadScript->then($scriptResult);
}
}
/**
* The list of disk names.
*
* @return array
*/
protected function disks(): array
public function disks(): array
{
return Config::get(VideoConstants::DISKS_QUALIFIED);
}
/**
* Get source repository for action.
*
* @return RepositoryInterface
*/
protected function getSourceRepository(): RepositoryInterface
{
return App::make(VideoSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @return RepositoryInterface
*/
protected function getDestinationRepository(): RepositoryInterface
{
return App::make(VideoDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositories
*/
protected function action(): ReconcileRepositories
{
return new ReconcileVideoRepositories();
}
}
@@ -11,11 +11,11 @@ use App\Enums\Http\Api\Filter\UnaryLogicalOperator;
use App\Http\Api\Criteria\Filter\Criteria;
use App\Http\Api\Filter\Filter;
use App\Http\Api\Parser\FilterParser;
use App\Rules\Api\DelimitedRule;
use Illuminate\Support\Arr;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator;
use Spatie\ValidationRules\Rules\Delimited;
/**
* Trait ValidatesFilters.
@@ -113,7 +113,7 @@ trait ValidatesFilters
$validator->sometimes(
$formattedParameter,
$filter->getRules(),
fn (Fluent $fluent) => is_string(Arr::get($fluent->toArray(), $formattedParameter)) && ! Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
fn (Fluent $fluent) => Arr::has($fluent->toArray(), $formattedParameter) && ! Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
);
}
}
@@ -121,7 +121,7 @@ trait ValidatesFilters
$multiValueRules = [];
foreach ($filter->getRules() as $rule) {
$multiValueRules[] = new Delimited($rule);
$multiValueRules[] = new DelimitedRule($rule);
}
$multiValueFilterFormats = $this->getFilterFormats($filter, UnaryLogicalOperator::getInstances());
@@ -131,7 +131,7 @@ trait ValidatesFilters
$validator->sometimes(
$formattedParameter,
$multiValueRules,
fn (Fluent $fluent) => is_string(Arr::get($fluent->toArray(), $formattedParameter)) && Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
fn (Fluent $fluent) => Arr::has($fluent->toArray(), $formattedParameter) && Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
);
}
}
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Rules\Api\DelimitedRule;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Validation\Rule;
use Spatie\ValidationRules\Rules\Delimited;
/**
* Trait ValidatesParameters.
@@ -30,7 +30,8 @@ trait ValidatesParameters
return [
$param => [
'nullable',
'sometimes',
'required',
"array:$types",
],
];
@@ -48,7 +49,13 @@ trait ValidatesParameters
{
return [
$param => array_merge(
['bail', 'sometimes', 'required', 'string', new Delimited(Rule::in($values))],
[
'bail',
'sometimes',
'required',
'string',
new DelimitedRule(['required', Rule::in($values)->__toString()]),
],
$customRules,
),
];
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Admin;
use App\Actions\Repositories\Admin\Dump\ReconcileDumpRepositoriesAction;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Admin\DumpRepository as DumpDestinationRepository;
use App\Repositories\Storage\Admin\DumpRepository as DumpSourceRepository;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesDumpRepositories.
*/
trait ReconcilesDumpRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
return App::make(DumpSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
return App::make(DumpDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileDumpRepositoriesAction();
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Billing;
use App\Actions\Repositories\Billing\Balance\ReconcileBalanceRepositoriesAction;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Models\Billing\Service;
use App\Repositories\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
use App\Repositories\Eloquent\Billing\DigitalOceanBalanceRepository as DigitalOceanDestinationRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesBalanceRepositories.
*/
trait ReconcilesBalanceRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
return match ($service?->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
return match ($service?->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileBalanceRepositoriesAction();
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Billing;
use App\Actions\Repositories\Billing\Transaction\ReconcileTransactionRepositoriesAction;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Models\Billing\Service;
use App\Repositories\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
use App\Repositories\Eloquent\Billing\DigitalOceanTransactionRepository as DigitalOceanDestinationRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesTransactionRepositories.
*/
trait ReconcilesTransactionRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
return match ($service?->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
return match ($service?->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileTransactionRepositoriesAction();
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories;
use App\Actions\ActionResult;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Actions\ActionStatus;
/**
* Trait ReconcilesRepositories.
*/
trait ReconcilesRepositories
{
/**
* Reconcile repositories.
*
* @param array $data
* @return ActionResult
*/
protected function reconcileRepositories(array $data = []): ActionResult
{
$sourceRepository = $this->getSourceRepository($data);
if ($sourceRepository === null) {
return new ActionResult(
ActionStatus::FAILED(),
'Could not find source repository'
);
}
$destinationRepository = $this->getDestinationRepository($data);
if ($destinationRepository === null) {
return new ActionResult(
ActionStatus::FAILED(),
'Could not find destination repository'
);
}
$this->handleFilters($sourceRepository, $destinationRepository, $data);
$action = $this->action();
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
abstract protected function action(): ReconcileRepositoriesAction;
/**
* Get source repository.
*
* @param array $data
* @return RepositoryInterface|null
*/
abstract protected function getSourceRepository(array $data = []): ?RepositoryInterface;
/**
* Get destination repository.
*
* @param array $data
* @return RepositoryInterface|null
*/
abstract protected function getDestinationRepository(array $data = []): ?RepositoryInterface;
/**
* Apply filters to repositories before reconciliation.
*
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @param array $data
* @return void
*/
protected function handleFilters(
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository,
array $data = []
): void {
// Not supported by default
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Wiki;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesAudioRepositories.
*/
trait ReconcilesAudioRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
return App::make(AudioSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
return App::make(AudioDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileAudioRepositoriesAction();
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Wiki;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesVideoRepositories.
*/
trait ReconcilesVideoRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
return App::make(VideoSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
return App::make(VideoDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileVideoRepositoriesAction();
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositoriesAction;
use App\Actions\Repositories\Wiki\Video\Script\ReconcileScriptRepositoriesAction;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\Video\ScriptRepository as ScriptDestinationRepository;
use App\Repositories\Storage\Wiki\Video\ScriptRepository as ScriptSourceRepository;
use Illuminate\Support\Facades\App;
/**
* Trait ReconcilesScriptRepositories.
*/
trait ReconcilesScriptRepositories
{
/**
* Get source repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $data = []): ?RepositoryInterface
{
return App::make(ScriptSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $data
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
{
return App::make(ScriptDestinationRepository::class);
}
/**
* Get the reconcile action.
*
* @return ReconcileRepositoriesAction
*/
protected function action(): ReconcileRepositoriesAction
{
return new ReconcileScriptRepositoriesAction();
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Console\Input\InputOption;
/**
* Class BaseCommand.
*/
abstract class BaseCommand extends Command implements Isolatable
{
/**
* Execute the console command.
*
* @return int
*
* @throws ValidationException
*/
abstract public function handle(): int;
/**
* Get the validator for options.
*
* @return Validator
*/
abstract protected function validator(): Validator;
/**
* Configure the console command for isolation.
* Note: Overrides default framework behavior which disables isolation by default.
*
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function configureIsolation(): void
{
$this->getDefinition()->addOption(new InputOption(
'isolated',
null,
InputOption::VALUE_OPTIONAL,
'Do not run the command if another instance of the command is already running',
true
));
}
}
@@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing\Balance;
use App\Actions\Repositories\Billing\Balance\ReconcileBalanceRepositories;
use App\Actions\Repositories\ReconcileRepositories;
use App\Console\Commands\Billing\ServiceReconcileCommand;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Models\Billing\Service;
use App\Repositories\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
use App\Repositories\Eloquent\Billing\DigitalOceanBalanceRepository as DigitalOceanDestinationRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
/**
* Class BalanceReconcileCommand.
*/
class BalanceReconcileCommand extends ServiceReconcileCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:balance {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and balance database';
/**
* Get the reconciliation action.
*
* @return ReconcileRepositories
*/
protected function getAction(): ReconcileRepositories
{
return new ReconcileBalanceRepositories();
}
/**
* Get source repository for service.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $validated): ?RepositoryInterface
{
return match (Arr::get($validated, 'service')) {
Service::DIGITALOCEAN()->key => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for service.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $validated): ?RepositoryInterface
{
return match (Arr::get($validated, 'service')) {
Service::DIGITALOCEAN()->key => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
}
@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing;
use App\Console\Commands\ReconcileCommand;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Models\Billing\Service;
use BenSampo\Enum\Rules\EnumKey;
use Illuminate\Contracts\Validation\Validator;
/**
* Class ServiceReconcileCommand.
*/
abstract class ServiceReconcileCommand extends ReconcileCommand
{
/**
* Get the validator for options.
*
* @return Validator
*/
protected function validator(): Validator
{
return \Illuminate\Support\Facades\Validator::make($this->arguments(), [
'service' => ['required', new EnumKey(Service::class)],
]);
}
/**
* Apply filters to repositories before reconciliation.
*
* @param array $validated
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @return void
*/
protected function handleFilters(
array $validated,
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository
): void {
// Not supported
}
}
@@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing\Transaction;
use App\Actions\Repositories\Billing\Transaction\ReconcileTransactionRepositories;
use App\Actions\Repositories\ReconcileRepositories;
use App\Console\Commands\Billing\ServiceReconcileCommand;
use App\Contracts\Repositories\RepositoryInterface;
use App\Enums\Models\Billing\Service;
use App\Repositories\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
use App\Repositories\Eloquent\Billing\DigitalOceanTransactionRepository as DigitalOceanDestinationRepository;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\App;
/**
* Class TransactionReconcileCommand.
*/
class TransactionReconcileCommand extends ServiceReconcileCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:transaction {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and transaction database';
/**
* Get the reconciliation action.
*
* @return ReconcileRepositories
*/
protected function getAction(): ReconcileRepositories
{
return new ReconcileTransactionRepositories();
}
/**
* Get source repository for service.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $validated): ?RepositoryInterface
{
return match (Arr::get($validated, 'service')) {
Service::DIGITALOCEAN()->key => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for service.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $validated): ?RepositoryInterface
{
return match (Arr::get($validated, 'service')) {
Service::DIGITALOCEAN()->key => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
}
@@ -1,150 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class PruneDatabaseDumpsCommand.
*/
class PruneDatabaseDumpsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:prune-dumps {--H|hours=72 : The number of hours to retain sanitized database dumps}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune stale database dumps from local storage';
/**
* The number of dumps deleted.
*
* @var int
*/
protected int $deleted = 0;
/**
* The number of dumps whose deletion failed.
*
* @var int
*/
protected int $deletedFailed = 0;
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$hours = $this->option('hours');
if (! is_numeric($hours)) {
Log::error("Invalid hours value '$hours'");
$this->error("Invalid hours value '$hours'");
return 1;
}
$this->prune(
Storage::disk('db-dumps'),
Date::now()->subHours(intval($hours))
);
$this->printResults();
return 0;
}
/**
* Prune database dumps in filesystem against date.
*
* @param Filesystem $filesystem
* @param Carbon $pruneDate
* @return void
*/
protected function prune(Filesystem $filesystem, Carbon $pruneDate): void
{
foreach ($filesystem->allFiles() as $path) {
$lastModified = Date::createFromTimestamp($filesystem->lastModified($path));
if (Str::contains($path, 'animethemes-db-dump') && $lastModified->isBefore($pruneDate)) {
$result = $filesystem->delete($path);
if ($result) {
$this->deleted++;
Log::info("Deleted database dump '$path'");
$this->info("Deleted database dump '$path'");
} else {
$this->deletedFailed++;
Log::error("Failed to delete database dump '$path'");
$this->error("Failed to delete database dump '$path'");
}
}
}
}
/**
* Print results to console and logs.
*
* @return void
*/
protected function printResults(): void
{
if ($this->hasResults()) {
if ($this->hasDeletions()) {
Log::info("$this->deleted database dumps deleted");
$this->info("$this->deleted database dumps deleted");
}
if ($this->hasFailures()) {
Log::error("Failed to delete $this->deletedFailed database dumps");
$this->error("Failed to delete $this->deletedFailed database dumps");
}
} else {
Log::info('No database dumps deleted');
$this->info('No database dumps deleted');
}
}
/**
* Determines if any deletions, successful or not, were made during pruning.
*
* @return bool
*/
protected function hasResults(): bool
{
return $this->hasDeletions() || $this->hasFailures();
}
/**
* Determines if any successful deletions were made during pruning.
*
* @return bool
*/
protected function hasDeletions(): bool
{
return $this->deleted > 0;
}
/**
* Determines if any unsuccessful deletions were attempted during pruning.
*
* @return bool
*/
protected function hasFailures(): bool
{
return $this->deletedFailed > 0;
}
}
-108
View File
@@ -1,108 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Actions\Repositories\ReconcileRepositories;
use App\Contracts\Repositories\RepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
/**
* Class ReconcileCommand.
*/
abstract class ReconcileCommand extends Command
{
/**
* Execute the console command.
*
* @return int
*
* @throws ValidationException
*/
public function handle(): int
{
$validator = $this->validator();
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$this->error($error);
}
return 1;
}
$sourceRepository = $this->getSourceRepository($validator->validated());
if ($sourceRepository === null) {
Log::error('Could not find source repository', $validator->validated());
$this->error('Could not find source repository');
return 1;
}
$destinationRepository = $this->getDestinationRepository($validator->validated());
if ($destinationRepository === null) {
Log::error('Could not find destination repository', $validator->validated());
$this->error('Could not find destination repository');
return 1;
}
$this->handleFilters($validator->validated(), $sourceRepository, $destinationRepository);
$action = $this->getAction();
$results = $action->reconcileRepositories($sourceRepository, $destinationRepository);
$results->toLog();
$results->toConsole($this);
return 0;
}
/**
* Get the validator for options.
*
* @return Validator
*/
abstract protected function validator(): Validator;
/**
* Get source repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
abstract protected function getSourceRepository(array $validated): ?RepositoryInterface;
/**
* Get destination repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
abstract protected function getDestinationRepository(array $validated): ?RepositoryInterface;
/**
* Apply filters to repositories before reconciliation.
*
* @param array $validated
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @return void
*/
abstract protected function handleFilters(
array $validated,
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository
): void;
/**
* Get the reconciliation action.
*
* @return ReconcileRepositories
*/
abstract protected function getAction(): ReconcileRepositories;
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Billing\Balance;
use App\Concerns\Repositories\Billing\ReconcilesBalanceRepositories;
use App\Console\Commands\Repositories\Billing\ServiceReconcileCommand;
/**
* Class BalanceReconcileCommand.
*/
class BalanceReconcileCommand extends ServiceReconcileCommand
{
use ReconcilesBalanceRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:balance {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and balance database';
}
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Billing;
use App\Console\Commands\Repositories\ReconcileCommand;
use App\Enums\Models\Billing\Service;
use BenSampo\Enum\Rules\EnumKey;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
/**
* Class ServiceReconcileCommand.
*/
abstract class ServiceReconcileCommand extends ReconcileCommand
{
/**
* Get the validator for options.
*
* @return Validator
*/
protected function validator(): Validator
{
return ValidatorFacade::make($this->arguments(), [
'service' => ['required', new EnumKey(Service::class)],
]);
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Billing\Transaction;
use App\Concerns\Repositories\Billing\ReconcilesTransactionRepositories;
use App\Console\Commands\Repositories\Billing\ServiceReconcileCommand;
/**
* Class TransactionReconcileCommand.
*/
class TransactionReconcileCommand extends ServiceReconcileCommand
{
use ReconcilesTransactionRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:transaction {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and transaction database';
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Console\Commands\BaseCommand;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
/**
* Class ReconcileCommand.
*/
abstract class ReconcileCommand extends BaseCommand
{
use ReconcilesRepositories;
/**
* Execute the console command.
*
* @return int
*
* @throws ValidationException
*/
public function handle(): int
{
$validator = $this->validator();
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
$this->error($error);
}
return 1;
}
$result = $this->reconcileRepositories($validator->validated());
$result->toLog();
$result->toConsole($this);
return $result->hasFailed() ? 1 : 0;
}
/**
* Get the validator for options.
*
* @return Validator
*/
abstract protected function validator(): Validator;
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Storage\Admin;
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
use App\Console\Commands\Repositories\Storage\StorageReconcileCommand;
use App\Constants\Config\DumpConstants;
use Illuminate\Support\Facades\Config;
/**
* Class DumpReconcileCommand.
*/
class DumpReconcileCommand extends StorageReconcileCommand
{
use ReconcilesDumpRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:dump';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and dump database';
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(DumpConstants::DISK_QUALIFIED);
}
}
@@ -2,41 +2,38 @@
declare(strict_types=1);
namespace App\Console\Commands\Wiki;
namespace App\Console\Commands\Repositories\Storage;
use App\Console\Commands\ReconcileCommand;
use App\Console\Commands\Repositories\ReconcileCommand;
use App\Contracts\Repositories\RepositoryInterface;
use App\Contracts\Storage\InteractsWithDisk;
use App\Rules\Storage\StorageDirectoryExistsRule;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
/**
* Class StorageReconcileCommand.
*/
abstract class StorageReconcileCommand extends ReconcileCommand
abstract class StorageReconcileCommand extends ReconcileCommand implements InteractsWithDisk
{
/**
* Get the name of the disk that represents the filesystem.
*
* @return string
*/
abstract protected function disk(): string;
/**
* Apply filters to repositories before reconciliation.
*
* @param array $validated
* @param RepositoryInterface $sourceRepository
* @param RepositoryInterface $destinationRepository
* @param array $data
* @return void
*/
protected function handleFilters(
array $validated,
RepositoryInterface $sourceRepository,
RepositoryInterface $destinationRepository
RepositoryInterface $destinationRepository,
array $data = []
): void {
$path = Arr::get($validated, 'path');
parent::handleFilters($sourceRepository, $destinationRepository, $data);
$path = Arr::get($data, 'path');
if ($path !== null) {
$sourceRepository->handleFilter('path', $path);
$destinationRepository->handleFilter('path', $path);
@@ -52,7 +49,7 @@ abstract class StorageReconcileCommand extends ReconcileCommand
{
$fs = Storage::disk($this->disk());
return \Illuminate\Support\Facades\Validator::make($this->options(), [
return ValidatorFacade::make($this->options(), [
'path' => ['nullable', 'string', 'doesnt_start_with:/', new StorageDirectoryExistsRule($fs)],
]);
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Storage\Wiki;
use App\Concerns\Repositories\Wiki\ReconcilesAudioRepositories;
use App\Console\Commands\Repositories\Storage\StorageReconcileCommand;
use App\Constants\Config\AudioConstants;
use Illuminate\Support\Facades\Config;
/**
* Class AudioReconcileCommand.
*/
class AudioReconcileCommand extends StorageReconcileCommand
{
use ReconcilesAudioRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:audio
{--path= : The directory of audios to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and audios database';
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Storage\Wiki\Video;
use App\Concerns\Repositories\Wiki\Video\ReconcilesScriptRepositories;
use App\Console\Commands\Repositories\Storage\StorageReconcileCommand;
use App\Constants\Config\VideoConstants;
use Illuminate\Support\Facades\Config;
/**
* Class ScriptReconcileCommand.
*/
class ScriptReconcileCommand extends StorageReconcileCommand
{
use ReconcilesScriptRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:script
{--path= : The directory of scripts to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and script database';
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Repositories\Storage\Wiki;
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
use App\Console\Commands\Repositories\Storage\StorageReconcileCommand;
use App\Constants\Config\VideoConstants;
use Illuminate\Support\Facades\Config;
/**
* Class VideoReconcileCommand.
*/
class VideoReconcileCommand extends StorageReconcileCommand
{
use ReconcilesVideoRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:video
{--path= : The directory of videos to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and video database';
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
}
@@ -2,15 +2,15 @@
declare(strict_types=1);
namespace App\Console\Commands\Document;
namespace App\Console\Commands\Storage\Admin;
use App\Console\Commands\DatabaseDumpCommand;
use App\Models\Document\Page;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpDocumentAction;
/**
* Class DocumentDatabaseDumpCommand.
* Class DocumentDumpCommand.
*/
class DocumentDatabaseDumpCommand extends DatabaseDumpCommand
class DocumentDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
@@ -43,24 +43,12 @@ class DocumentDatabaseDumpCommand extends DatabaseDumpCommand
protected $description = 'Produces sanitized database dump, targeting document-related tables for seeding purposes';
/**
* The list of tables to include in the dump.
* Get the underlying action.
*
* @return array
* @return DumpAction
*/
protected function allowedTables(): array
protected function action(): DumpAction
{
return [
Page::TABLE,
];
}
/**
* The directory that the file should be dumped to.
*
* @return string
*/
protected function getDumpFilePath(): string
{
return 'document';
return new DumpDocumentAction($this->options());
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Console\Commands\BaseCommand;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
use Illuminate\Validation\Rule;
/**
* Class DatabaseDumpCommand.
*/
abstract class DumpCommand extends BaseCommand
{
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$validator = $this->validator();
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
Log::error($error);
$this->error($error);
}
return 1;
}
$action = $this->action();
$result = $action->handle();
$result->toLog();
$result->toConsole($this);
return $result->hasFailed() ? 1 : 0;
}
/**
* Get the validator for options.
*
* @return Validator
*/
protected function validator(): Validator
{
return ValidatorFacade::make($this->options(), [
'default-character-set' => ['string'],
'set-gtid-purged' => [Rule::in(['OFF', 'ON', 'AUTO'])->__toString()],
]);
}
/**
* Get the underlying action.
*
* @return DumpAction
*/
abstract protected function action(): DumpAction;
}
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\PruneDumpAction;
use App\Actions\Storage\Base\PruneAction;
use App\Console\Commands\Storage\Base\PruneCommand;
/**
* Class DumpPruneCommand.
*/
class DumpPruneCommand extends PruneCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'prune:dump {--H|hours=72 : The number of hours to retain dumps}';
/**
* Get the underlying action.
*
* @return PruneAction
*/
protected function getAction(): PruneAction
{
$hours = $this->option('hours');
return new PruneDumpAction(intval($hours));
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Admin;
use App\Actions\Storage\Admin\Dump\DumpAction;
use App\Actions\Storage\Admin\Dump\DumpWikiAction;
/**
* Class WikiDumpCommand.
*/
class WikiDumpCommand extends DumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-wiki
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting wiki-related tables for seeding purposes';
/**
* Get the underlying action.
*
* @return DumpAction
*/
protected function action(): DumpAction
{
return new DumpWikiAction($this->options());
}
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage\Base;
use App\Actions\Storage\Base\PruneAction;
use App\Console\Commands\Storage\StorageCommand;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Validator as ValidatorFacade;
/**
* Class PruneCommand.
*/
abstract class PruneCommand extends StorageCommand
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune stale files from storage';
/**
* Get the underlying action.
*
* @return PruneAction
*/
abstract protected function getAction(): PruneAction;
/**
* Get the validator for options.
*
* @return Validator
*/
protected function validator(): Validator
{
return ValidatorFacade::make($this->options(), [
'hours' => ['required', 'integer'],
]);
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Storage;
use App\Console\Commands\BaseCommand;
use App\Contracts\Actions\Storage\StorageAction;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Support\Facades\Log;
/**
* Class StorageCommand.
*/
abstract class StorageCommand extends BaseCommand
{
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$validator = $this->validator();
if ($validator->fails()) {
foreach ($validator->errors()->all() as $error) {
Log::error($error);
$this->error($error);
}
return 1;
}
$action = $this->getAction();
$storageResults = $action->handle();
$storageResults->toLog();
$storageResults->toConsole($this);
$action->then($storageResults);
$result = $storageResults->toActionResult();
return $result->hasFailed() ? 1 : 0;
}
/**
* Get the underlying action.
*
* @return StorageAction
*/
abstract protected function getAction(): StorageAction;
/**
* Get the validator for options.
*
* @return Validator
*/
abstract protected function validator(): Validator;
}
@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Wiki\Audio;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Audio\ReconcileAudioRepositories;
use App\Console\Commands\Wiki\StorageReconcileCommand;
use App\Constants\Config\AudioConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
/**
* Class AudioReconcileCommand.
*/
class AudioReconcileCommand extends StorageReconcileCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:audio
{--path= : The directory of audios to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and audios database';
/**
* Get the name of the disk that represents the filesystem.
*
* @return string
*/
protected function disk(): string
{
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Get source repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $validated): ?RepositoryInterface
{
return App::make(AudioSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $validated): ?RepositoryInterface
{
return App::make(AudioDestinationRepository::class);
}
/**
* Get the reconciliation action.
*
* @return ReconcileRepositories
*/
protected function getAction(): ReconcileRepositories
{
return new ReconcileAudioRepositories();
}
}
@@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Wiki\Video;
use App\Actions\Repositories\ReconcileRepositories;
use App\Actions\Repositories\Wiki\Video\ReconcileVideoRepositories;
use App\Console\Commands\Wiki\StorageReconcileCommand;
use App\Constants\Config\VideoConstants;
use App\Contracts\Repositories\RepositoryInterface;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
use App\Repositories\Storage\Wiki\VideoRepository as VideoSourceRepository;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
/**
* Class VideoReconcileCommand.
*/
class VideoReconcileCommand extends StorageReconcileCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:video
{--path= : The directory of videos to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and video database';
/**
* Get the name of the disk that represents the filesystem.
*
* @return string
*/
protected function disk(): string
{
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
}
/**
* Get source repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getSourceRepository(array $validated): ?RepositoryInterface
{
return App::make(VideoSourceRepository::class);
}
/**
* Get destination repository for action.
*
* @param array $validated
* @return RepositoryInterface|null
*/
protected function getDestinationRepository(array $validated): ?RepositoryInterface
{
return App::make(VideoDestinationRepository::class);
}
/**
* Get the reconciliation action.
*
* @return ReconcileRepositories
*/
protected function getAction(): ReconcileRepositories
{
return new ReconcileVideoRepositories();
}
}
@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Wiki;
use App\Console\Commands\DatabaseDumpCommand;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeSynonym;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\Audio;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use App\Models\Wiki\Series;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use App\Models\Wiki\Video;
use App\Pivots\AnimeImage;
use App\Pivots\AnimeResource;
use App\Pivots\AnimeSeries;
use App\Pivots\AnimeStudio;
use App\Pivots\AnimeThemeEntryVideo;
use App\Pivots\ArtistImage;
use App\Pivots\ArtistMember;
use App\Pivots\ArtistResource;
use App\Pivots\ArtistSong;
use App\Pivots\StudioImage;
use App\Pivots\StudioResource;
/**
* Class WikiDatabaseDumpCommand.
*/
class WikiDatabaseDumpCommand extends DatabaseDumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-wiki
{--comments : Write additional information in the MySQL dump such as program version, server version and host}
{--data-only : Dump only the data without the schema in PostgreSQL dump}
{--default-character-set=utf8 : Specify default character set in MySQL dump}
{--extended-insert : Use multiple-row insert syntax in MySQL dump}
{--inserts : Dump data as INSERT commands rather than COPY in PostgreSQL dump}
{--lock-tables : Lock all tables before dumping them to MySQL dump}
{--no-create-info : Turn off CREATE TABLE statements in MySQL dump}
{--quick : Retrieve rows for a table from the server one row at a time in MySQL dump}
{--set-gtid-purged=AUTO : Add SET GTID_PURGED to output in MySQL dump}
{--single-transaction : Issue a BEGIN SQL statement before dumping data from server for MySQL dump}
{--skip-column-statistics : Turn off ANALYZE table statements in the MySQL dump}
{--skip-comments : Do not write additional information in the MySQL dump}
{--skip-extended-insert : Turn off extended-insert in MySQL dump}
{--skip-lock-tables : Turn off locking tables before dumping to MySQL dump}
{--skip-quick : Do not retrieve rows for a table from the server one row at a time in MySQL dump}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting wiki-related tables for seeding purposes';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Anime::TABLE,
AnimeImage::TABLE,
AnimeResource::TABLE,
AnimeSeries::TABLE,
AnimeStudio::TABLE,
AnimeSynonym::TABLE,
AnimeTheme::TABLE,
AnimeThemeEntry::TABLE,
AnimeThemeEntryVideo::TABLE,
Artist::TABLE,
ArtistImage::TABLE,
ArtistMember::TABLE,
ArtistResource::TABLE,
ArtistSong::TABLE,
Audio::TABLE,
ExternalResource::TABLE,
Image::TABLE,
Series::TABLE,
Song::TABLE,
Studio::TABLE,
StudioImage::TABLE,
StudioResource::TABLE,
Video::TABLE,
];
}
/**
* The directory that the file should be dumped to.
*
* @return string
*/
protected function getDumpFilePath(): string
{
return 'wiki';
}
}
+22 -17
View File
@@ -4,16 +4,18 @@ declare(strict_types=1);
namespace App\Console;
use App\Console\Commands\Billing\Balance\BalanceReconcileCommand;
use App\Console\Commands\Billing\Transaction\TransactionReconcileCommand;
use App\Console\Commands\Document\DocumentDatabaseDumpCommand;
use App\Console\Commands\PruneDatabaseDumpsCommand;
use App\Console\Commands\Wiki\WikiDatabaseDumpCommand;
use App\Console\Commands\Repositories\Billing\Balance\BalanceReconcileCommand;
use App\Console\Commands\Repositories\Billing\Transaction\TransactionReconcileCommand;
use App\Console\Commands\Storage\Admin\DocumentDumpCommand;
use App\Console\Commands\Storage\Admin\DumpPruneCommand;
use App\Console\Commands\Storage\Admin\WikiDumpCommand;
use App\Enums\Models\Billing\Service;
use App\Models\BaseModel;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\MonitorCommand as MonitorDatabaseCommand;
use Illuminate\Database\Console\PruneCommand as PruneModelsCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Queue\Console\MonitorCommand as MonitorQueueCommand;
use Illuminate\Queue\Console\PruneFailedJobsCommand;
use Illuminate\Support\Facades\Config;
use Laravel\Horizon\Console\SnapshotCommand;
@@ -41,33 +43,36 @@ class Kernel extends ConsoleKernel
->storeOutput()
->hourly();
// Managed database requires --single-transaction and --set-gtid-purged=OFF
$schedule->command(DocumentDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF'])
$schedule->command(DocumentDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
// Managed database requires --single-transaction and --set-gtid-purged=OFF
$schedule->command(WikiDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF'])
$schedule->command(WikiDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
// Managed database requires --single-transaction and --set-gtid-purged=OFF
$schedule->command(WikiDatabaseDumpCommand::class, ['--single-transaction', '--set-gtid-purged' => 'OFF', '--no-create-info'])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(PruneDatabaseDumpsCommand::class)
$schedule->command(DumpPruneCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->dailyAt('00:15');
$schedule->command(MonitorDatabaseCommand::class, ['--max' => 100])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->everyMinute();
$schedule->command(MonitorQueueCommand::class, ['queues' => 'default', '--max' => 100])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->everyMinute();
$schedule->command(PruneExpired::class)
->withoutOverlapping()
->runInBackground()
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class DumpConstants.
*/
class DumpConstants
{
final public const DISK_QUALIFIED = 'dump.disk';
final public const PATH_QUALIFIED = 'dump.path';
final public const URL_QUALIFIED = 'dump.url';
}
+8
View File
@@ -24,4 +24,12 @@ class FlagConstants
final public const ALLOW_VIEW_RECORDING_FLAG = 'allow_view_recording';
final public const ALLOW_VIEW_RECORDING_FLAG_QUALIFIED = 'flags.allow_view_recording';
final public const ALLOW_DUMP_DOWNLOADING_FLAG = 'allow_dump_downloading';
final public const ALLOW_DUMP_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_dump_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG = 'allow_script_downloading';
final public const ALLOW_SCRIPT_DOWNLOADING_FLAG_QUALIFIED = 'flags.allow_script_downloading';
}
+8
View File
@@ -21,7 +21,15 @@ class VideoConstants
final public const PATH_QUALIFIED = 'video.path';
final public const RATE_LIMITER_QUALIFIED = 'video.rate_limiter';
final public const STREAMING_METHOD_QUALIFIED = 'video.streaming_method';
final public const URL_QUALIFIED = 'video.url';
final public const SCRIPT_DISK_QUALIFIED = 'video.script.disk';
final public const SCRIPT_PATH_QUALIFIED = 'video.script.path';
final public const SCRIPT_URL_QUALIFIED = 'video.script.url';
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Actions\Storage;
/**
* Interface StorageAction.
*/
interface StorageAction
{
/**
* Handle action.
*
* @return StorageResults
*/
public function handle(): StorageResults;
/**
* Processes to be completed after handling action.
*
* @param StorageResults $storageResults
* @return void
*/
public function then(StorageResults $storageResults): void;
}
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Actions\Storage;
use App\Actions\ActionResult;
use Illuminate\Console\Command;
/**
* Interface StorageResults.
*/
interface StorageResults
{
/**
* Write results to log.
*
* @return void
*/
public function toLog(): void;
/**
* Write results to console output.
*
* @param Command $command
* @return void
*/
public function toConsole(Command $command): void;
/**
* Transform to Action Result.
*
* @return ActionResult
*/
public function toActionResult(): ActionResult;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Storage;
/**
* Class InteractsWithDisk.
*/
interface InteractsWithDisk
{
/**
* The name of the disk.
*
* @return string
*/
public function disk(): string;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Storage;
/**
* Class InteractsWithDisks.
*/
interface InteractsWithDisks
{
/**
* The list of disk names.
*
* @return array
*/
public function disks(): array;
}
+11
View File
@@ -27,4 +27,15 @@ abstract class BaseEnum extends Enum implements LocalizedEnum
fn (BaseEnum $enum) => Str::lower($enum->description) === Str::lower($description)
);
}
/**
* Attempt to instantiate a new Enum using the given key or value.
* Note: library coerce function does not attempt unstrict value comparision.
*/
public static function unstrictCoerce(mixed $enumKeyOrValue): ?static
{
return is_numeric($enumKeyOrValue)
? static::coerce(intval($enumKeyOrValue))
: static::coerce($enumKeyOrValue);
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http;
use App\Enums\BaseEnum;
/**
* Class StreamingMethod.
*/
class StreamingMethod extends BaseEnum
{
public const NGINX = 'nginx';
public const RESPONSE = 'response';
}
+29 -6
View File
@@ -175,12 +175,34 @@ final class ResourceSite extends BaseEnum
public static function formatAnimeResourceLink(ResourceSite $site, int $id, ?string $slug = null): ?string
{
return match ($site->value) {
ResourceSite::ANIDB => "https://anidb.net/anime/$id/",
ResourceSite::ANILIST => "https://anilist.co/anime/$id/",
ResourceSite::TWITTER => "https://twitter.com/$slug",
ResourceSite::ANIDB => "https://anidb.net/anime/$id",
ResourceSite::ANILIST => "https://anilist.co/anime/$id",
ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/anime/$slug",
ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/anime.php?id=$id",
ResourceSite::KITSU => "https://kitsu.io/anime/$slug",
ResourceSite::MAL => "https://myanimelist.net/anime/$id/",
ResourceSite::MAL => "https://myanimelist.net/anime/$id",
default => null,
};
}
/**
* Get the URL of the site for artist resources.
*
* @param ResourceSite $site
* @param int $id
* @param string|null $slug
* @return string|null
*/
public static function formatArtistResourceLink(ResourceSite $site, int $id, ?string $slug = null): ?string
{
return match ($site->value) {
ResourceSite::TWITTER => "https://twitter.com/$slug",
ResourceSite::ANIDB => "https://anidb.net/creator/$id",
ResourceSite::ANILIST => "https://anilist.co/staff/$id",
ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/people/$slug",
ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/people.php?id=$id",
ResourceSite::MAL => "https://myanimelist.net/people/$id",
default => null,
};
}
@@ -196,11 +218,12 @@ final class ResourceSite extends BaseEnum
public static function formatStudioResourceLink(ResourceSite $site, int $id, ?string $slug = null): ?string
{
return match ($site->value) {
ResourceSite::ANIDB => "https://anidb.net/creator/$id/",
ResourceSite::ANILIST => "https://anilist.co/studio/$id/",
ResourceSite::TWITTER => "https://twitter.com/$slug",
ResourceSite::ANIDB => "https://anidb.net/creator/$id",
ResourceSite::ANILIST => "https://anilist.co/studio/$id",
ResourceSite::ANIME_PLANET => "https://www.anime-planet.com/anime/studios/$slug",
ResourceSite::ANN => "https://www.animenewsnetwork.com/encyclopedia/company.php?id=$id",
ResourceSite::MAL => "https://myanimelist.net/anime/producer/$id/",
ResourceSite::MAL => "https://myanimelist.net/anime/producer/$id",
default => null,
};
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Dump;
use App\Events\Base\Admin\AdminCreatedEvent;
use App\Models\Admin\Dump;
/**
* Class DumpCreated.
*
* @extends AdminCreatedEvent<Dump>
*/
class DumpCreated extends AdminCreatedEvent
{
/**
* Create a new event instance.
*
* @param Dump $dump
*/
public function __construct(Dump $dump)
{
parent::__construct($dump);
}
/**
* Get the model that has fired this event.
*
* @return Dump
*/
public function getModel(): Dump
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Dump '**{$this->getModel()->getName()}**' has been created.";
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Dump;
use App\Events\Base\Admin\AdminDeletedEvent;
use App\Models\Admin\Dump;
/**
* Class DumpDeleted.
*
* @extends AdminDeletedEvent<Dump>
*/
class DumpDeleted extends AdminDeletedEvent
{
/**
* Create a new event instance.
*
* @param Dump $dump
*/
public function __construct(Dump $dump)
{
parent::__construct($dump);
}
/**
* Get the model that has fired this event.
*
* @return Dump
*/
public function getModel(): Dump
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Dump '**{$this->getModel()->getName()}**' has been deleted.";
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Dump;
use App\Events\Base\Admin\AdminRestoredEvent;
use App\Models\Admin\Dump;
/**
* Class DumpRestored.
*
* @extends AdminRestoredEvent<Dump>
*/
class DumpRestored extends AdminRestoredEvent
{
/**
* Create a new event instance.
*
* @param Dump $dump
*/
public function __construct(Dump $dump)
{
parent::__construct($dump);
}
/**
* Get the model that has fired this event.
*
* @return Dump
*/
public function getModel(): Dump
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Dump '**{$this->getModel()->getName()}**' has been restored.";
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Dump;
use App\Events\Base\Admin\AdminUpdatedEvent;
use App\Models\Admin\Dump;
/**
* Class DumpUpdated.
*
* @extends AdminUpdatedEvent<Dump>
*/
class DumpUpdated extends AdminUpdatedEvent
{
/**
* Create a new event instance.
*
* @param Dump $dump
*/
public function __construct(Dump $dump)
{
parent::__construct($dump);
$this->initializeEmbedFields($dump);
}
/**
* Get the model that has fired this event.
*
* @return Dump
*/
public function getModel(): Dump
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Dump '**{$this->getModel()->getName()}**' has been updated.";
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ abstract class PivotDeletedEvent extends BasePivotEvent
{
return DiscordMessage::create('', [
'description' => $this->getDiscordMessageDescription(),
'color' => EmbedColor::GREEN,
'color' => EmbedColor::RED,
]);
}
}

Some files were not shown because too many files have changed in this diff Show More