mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0464406d3 | |||
| 134a82ecc9 | |||
| 3e71ff8ace | |||
| 39250632b2 | |||
| 3cdf824e47 | |||
| 681f07ecc9 | |||
| 9028557f4c | |||
| cea28e61f1 | |||
| fed083b5d3 | |||
| 2e97bb78ea | |||
| b320882d6d | |||
| a8801edec9 | |||
| ed33d4154a | |||
| d3a8d58cd2 | |||
| 4cf68a96ac | |||
| 8b6b196c99 | |||
| 99cf085666 | |||
| 1d93000c6f | |||
| 57cc3d508d | |||
| 2792961a06 | |||
| 95134a384c | |||
| 16c8d1781b | |||
| 23afebc70e | |||
| 7d5cd09768 | |||
| b1e2a65b85 | |||
| 99ec12c690 | |||
| 979773d476 | |||
| ff2de264c8 | |||
| 3151921c58 | |||
| 118dd45085 | |||
| 5517d4061e | |||
| 952d3775f9 | |||
| 43c97f1a7c | |||
| d8354c536a | |||
| 80899b07df | |||
| d71c675b23 | |||
| bb667d0b45 | |||
| e812b94b87 | |||
| 9a0989a3f7 | |||
| 00e03a9f38 | |||
| 146833338d | |||
| 298c4b31d8 | |||
| e973f68537 | |||
| 3a77f95e49 | |||
| c18006badb | |||
| 2d17f7b9f7 | |||
| 5f915c86ea | |||
| d1a4e99303 | |||
| 191a5e890c | |||
| 07eb52b68f | |||
| 39f665a524 | |||
| 4258e0727d | |||
| 06ee91f968 | |||
| 95f8229b50 | |||
| 3c128ff036 | |||
| 9975feb7ec | |||
| ad6eca731b | |||
| 211c164908 | |||
| ee1f93f011 | |||
| 8818bd0fd2 | |||
| 1611c73001 | |||
| b05e61e4ac | |||
| 57d5d9b2ef | |||
| 9c569bc435 | |||
| ea8a644aa3 | |||
| ad07aa3703 | |||
| 101feb031f | |||
| f5942d0de5 | |||
| 4b01e3ac17 | |||
| ccdf3b423a | |||
| 6a6d28c9c7 | |||
| ebfa4f96af | |||
| 899d01fada | |||
| 3ed878ab46 |
+2
-2
@@ -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]
|
||||
|
||||
+119
-32
@@ -10,6 +10,15 @@ APP_URL=http://localhost
|
||||
ASSET_URL=null
|
||||
APP_KEY=
|
||||
|
||||
# audio
|
||||
AUDIO_DISK_DEFAULT=audios_nyc
|
||||
AUDIO_DISKS=audios_nyc
|
||||
AUDIO_DISK_ROOT=
|
||||
AUDIO_PATH=/audio
|
||||
AUDIO_URL=
|
||||
AUDIO_STREAMING_METHOD=
|
||||
AUDIO_NGINX_REDIRECT=
|
||||
|
||||
# audit
|
||||
AUDITING_ENABLED=true
|
||||
|
||||
@@ -18,10 +27,17 @@ BROADCAST_DRIVER=log
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_HOST=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
ABLY_KEY=
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# cache
|
||||
CACHE_DRIVER=file
|
||||
@@ -57,12 +73,15 @@ 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_HOST=localhost
|
||||
ELASTIC_PORT=9200
|
||||
ELASTIC_SCHEME=http
|
||||
ELASTIC_USER=
|
||||
ELASTIC_PASS=
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://localhost:9200
|
||||
|
||||
# elastic driver
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
@@ -70,16 +89,9 @@ ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
# elastic migrations
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
# enlightn
|
||||
ENLIGHTN_SKIP_ENVIRONMENT_SPECIFIC=false
|
||||
ENLIGHTN_USERNAME=
|
||||
ENLIGHTN_API_TOKEN=
|
||||
ENLIGHTN_GITHUB_REPO=
|
||||
|
||||
# feature flags
|
||||
ALLOW_VIDEO_STREAMS=false
|
||||
ALLOW_DISCORD_NOTIFICATIONS=false
|
||||
ALLOW_VIEW_RECORDING=false
|
||||
# ffmpeg
|
||||
FFMPEG_BINARIES=
|
||||
FFPROBE_BINARIES=
|
||||
|
||||
# filesystems
|
||||
FILESYSTEM_DISK=local
|
||||
@@ -94,14 +106,71 @@ IMAGE_DISABLE_ASSERTS=
|
||||
IMAGE_VISIBILITY=
|
||||
IMAGE_URL=
|
||||
|
||||
VIDEO_ACCESS_KEY_ID=
|
||||
VIDEO_SECRET_ACCESS_KEY=
|
||||
VIDEO_DEFAULT_REGION=
|
||||
VIDEO_ENDPOINT=
|
||||
VIDEO_BUCKET=
|
||||
VIDEO_STREAM_READS=
|
||||
VIDEO_DISABLE_ASSERTS=
|
||||
VIDEO_VISIBILITY=
|
||||
VIDEO_NYC_ACCESS_KEY_ID=
|
||||
VIDEO_NYC_SECRET_ACCESS_KEY=
|
||||
VIDEO_NYC_DEFAULT_REGION=
|
||||
VIDEO_NYC_ENDPOINT=
|
||||
VIDEO_NYC_BUCKET=
|
||||
VIDEO_NYC_STREAM_READS=
|
||||
VIDEO_NYC_DISABLE_ASSERTS=
|
||||
VIDEO_NYC_VISIBILITY=
|
||||
|
||||
VIDEO_FRA_ACCESS_KEY_ID=
|
||||
VIDEO_FRA_SECRET_ACCESS_KEY=
|
||||
VIDEO_FRA_DEFAULT_REGION=
|
||||
VIDEO_FRA_ENDPOINT=
|
||||
VIDEO_FRA_BUCKET=
|
||||
VIDEO_FRA_STREAM_READS=
|
||||
VIDEO_FRA_DISABLE_ASSERTS=
|
||||
VIDEO_FRA_VISIBILITY=
|
||||
|
||||
AUDIO_NYC_ACCESS_KEY_ID=
|
||||
AUDIO_NYC_SECRET_ACCESS_KEY=
|
||||
AUDIO_NYC_DEFAULT_REGION=
|
||||
AUDIO_NYC_ENDPOINT=
|
||||
AUDIO_NYC_BUCKET=
|
||||
AUDIO_NYC_STREAM_READS=
|
||||
AUDIO_NYC_DISABLE_ASSERTS=
|
||||
AUDIO_NYC_VISIBILITY=
|
||||
|
||||
AUDIO_FRA_ACCESS_KEY_ID=
|
||||
AUDIO_FRA_SECRET_ACCESS_KEY=
|
||||
AUDIO_FRA_DEFAULT_REGION=
|
||||
AUDIO_FRA_ENDPOINT=
|
||||
AUDIO_FRA_BUCKET=
|
||||
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=
|
||||
FORTIFY_URL=http://localhost
|
||||
|
||||
# hashing
|
||||
BCRYPT_ROUNDS=10
|
||||
@@ -110,8 +179,13 @@ BCRYPT_ROUNDS=10
|
||||
HORIZON_DOMAIN=
|
||||
HORIZON_PATH=horizon
|
||||
|
||||
# image
|
||||
IMAGE_DISK=images
|
||||
IMAGE_DISK_ROOT=
|
||||
|
||||
# jetstream
|
||||
TEAM_CREATOR_ID=
|
||||
JETSTREAM_PATH=
|
||||
JETSTREAM_URL=http://localhost
|
||||
|
||||
# logging
|
||||
LOG_CHANNEL=daily
|
||||
@@ -127,7 +201,7 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_SENDMAIL_PATH=/usr/sbin/sendmail
|
||||
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -bs -i"
|
||||
MAIL_LOG_CHANNEL=
|
||||
|
||||
# nova
|
||||
@@ -176,11 +250,6 @@ SESSION_STORE=null
|
||||
SESSION_DOMAIN=null
|
||||
SESSION_SECURE_COOKIE=
|
||||
|
||||
# teams
|
||||
HORIZON_TEAM_ID=
|
||||
NOVA_TEAM_ID=
|
||||
TELESCOPE_TEAM_ID=
|
||||
|
||||
# telescope
|
||||
TELESCOPE_DOMAIN=null
|
||||
TELESCOPE_PATH=telescope
|
||||
@@ -188,9 +257,27 @@ TELESCOPE_DRIVER=database
|
||||
TELESCOPE_ENABLED=true
|
||||
|
||||
# video
|
||||
VIDEO_DISK_DEFAULT=videos_nyc
|
||||
VIDEO_DISKS=videos_nyc
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_STREAMING_METHOD=
|
||||
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=
|
||||
|
||||
# wiki
|
||||
WIKI_FEATURED_ENTRY=
|
||||
WIKI_FEATURED_VIDEO=
|
||||
WIKI_DONATE_URL=#
|
||||
WIKI_FAQ_URL=#
|
||||
|
||||
@@ -8,3 +8,4 @@
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
@@ -5,14 +5,14 @@ on: push
|
||||
jobs:
|
||||
static-analysis:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
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@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
run: npm run build
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
|
||||
@@ -5,32 +5,29 @@ on: push
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
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@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
|
||||
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
|
||||
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
|
||||
run: |
|
||||
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
|
||||
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
run: npm run build
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
/node_modules
|
||||
/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
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=wiki" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=wiki" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=wiki"><img src="https://github.styleci.io/repos/111264405/shield?branch=wiki" alt="StyleCI"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=main" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=main" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=main"><img src="https://github.styleci.io/repos/111264405/shield?branch=main" alt="StyleCI"></a>
|
||||
<a href="https://discordapp.com/invite/m9zbVyQ"><img src="https://img.shields.io/discord/354388306580078594.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/wiki/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/main/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://reddit.com/r/AnimeThemes"><img src="https://img.shields.io/reddit/subreddit-subscribers/AnimeThemes?style=social"></a>
|
||||
<a href="https://twitter.com/AnimeThemesMoe"><img src="https://img.shields.io/twitter/follow/AnimeThemesMoe?style=social"></a>
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class ActionResult.
|
||||
*/
|
||||
class ActionResult
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param ActionStatus $status
|
||||
* @param string|null $message
|
||||
*/
|
||||
public function __construct(protected readonly ActionStatus $status, protected readonly ?string $message = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result status.
|
||||
*
|
||||
* @return ActionStatus
|
||||
*/
|
||||
public function getStatus(): ActionStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result message.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has the action failed?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFailed(): bool
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BaseAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class BaseAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
abstract public function handle(): ActionResult;
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return TModel
|
||||
*/
|
||||
abstract protected function getModel(): BaseModel;
|
||||
|
||||
/**
|
||||
* Get the relation to resources.
|
||||
*
|
||||
* @return Relation
|
||||
*/
|
||||
abstract protected function relation(): Relation;
|
||||
|
||||
/**
|
||||
* Get the human-friendly label for the underlying model.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function label(): string
|
||||
{
|
||||
return Str::headline(class_basename($this->getModel()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillImageAction;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeImageAction.
|
||||
*
|
||||
* @extends BackfillImageAction<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeImageAction extends BackfillImageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to Anime.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pivots\AnimeResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeResourceAction.
|
||||
*
|
||||
* @extends BackfillResourceAction<Anime>
|
||||
*/
|
||||
abstract class BackfillAnimeResourceAction extends BackfillResourceAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->resources();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Resource from response.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $slug
|
||||
* @return ExternalResource
|
||||
*/
|
||||
protected function getOrCreateResource(int $id, string $slug = null): ExternalResource
|
||||
{
|
||||
$resource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->where(ExternalResource::ATTRIBUTE_LINK, ResourceSite::formatAnimeResourceLink($this->getSite(), $id, $slug))
|
||||
->first();
|
||||
|
||||
if ($resource === null) {
|
||||
Log::info("Creating {$this->getSite()->description} Resource '$id'");
|
||||
|
||||
$resource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatAnimeResourceLink($this->getSite(), $id, $slug),
|
||||
ExternalResource::ATTRIBUTE_SITE => $this->getSite()->value,
|
||||
]);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Resource to Anime.
|
||||
*
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
protected function attachResource(ExternalResource $resource): void
|
||||
{
|
||||
if (AnimeResource::query()
|
||||
->where($this->getModel()->getKeyName(), $this->getModel()->getKey())
|
||||
->where($resource->getKeyName(), $resource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching Resource '{$resource->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-10
@@ -2,21 +2,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Image;
|
||||
namespace App\Actions\Models\Wiki\Anime\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeImage;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillLargeCoverImage.
|
||||
* Class BackfillLargeCoverImageAction.
|
||||
*/
|
||||
class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
class BackfillLargeCoverImageAction extends BackfillAnimeImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
@@ -37,12 +37,9 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillLargeCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
+7
-10
@@ -2,21 +2,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Image;
|
||||
namespace App\Actions\Models\Wiki\Anime\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeImage;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillSmallCoverImage.
|
||||
* Class BackfillSmallCoverImageAction.
|
||||
*/
|
||||
class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
class BackfillSmallCoverImageAction extends BackfillAnimeImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
@@ -37,12 +37,9 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbImage($anilistResource);
|
||||
if ($anidbResource !== null) {
|
||||
return $anidbResource;
|
||||
}
|
||||
return $this->getAnilistImage($anilistResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -56,7 +53,7 @@ class BackfillSmallCoverImage extends BackfillAnimeImage
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnidbImage(ExternalResource $anilistResource): ?Image
|
||||
protected function getAnilistImage(ExternalResource $anilistResource): ?Image
|
||||
{
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
+7
-7
@@ -2,19 +2,19 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* Class BackfillAnidbResource.
|
||||
* Class BackfillAnidbResourceAction.
|
||||
*/
|
||||
class BackfillAnidbResource extends BackfillAnimeResource
|
||||
class BackfillAnidbResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -37,7 +37,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case AniDB Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($malResource, 'myanimelist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -48,7 +48,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$anidbResource = $this->getAnidbMapping($anilistResource, 'anilist');
|
||||
if ($anidbResource !== null) {
|
||||
@@ -59,7 +59,7 @@ class BackfillAnidbResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMapping($kitsuResource, 'kitsu');
|
||||
}
|
||||
+7
-7
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnilistResource.
|
||||
* Class BackfillAnilistResourceAction.
|
||||
*/
|
||||
class BackfillAnilistResource extends BackfillAnimeResource
|
||||
class BackfillAnilistResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -38,7 +38,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Anilist Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getMalAnilistMapping($malResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -46,7 +46,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$anilistResource = $this->getKitsuAnilistMapping($kitsuResource);
|
||||
if ($anilistResource !== null) {
|
||||
@@ -54,7 +54,7 @@ class BackfillAnilistResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbAnilistMapping($anidbResource);
|
||||
}
|
||||
+5
-5
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnnResource.
|
||||
* Class BackfillAnnResourceAction.
|
||||
*/
|
||||
class BackfillAnnResource extends BackfillAnimeResource
|
||||
class BackfillAnnResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -36,7 +36,7 @@ class BackfillAnnResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getKitsuAnnMapping($kitsuResource);
|
||||
}
|
||||
+8
-8
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillKitsuResource.
|
||||
* Class BackfillKitsuResourceAction.
|
||||
*/
|
||||
class BackfillKitsuResource extends BackfillAnimeResource
|
||||
class BackfillKitsuResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -38,7 +38,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
{
|
||||
// Allow fall-throughs in case Kitsu Resource is not mapped to every external site.
|
||||
|
||||
$malResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($malResource, 'MYANIMELIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -49,7 +49,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anilistResource, 'ANILIST_ANIME');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -60,7 +60,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
$kitsuResource = $this->getKitsuMapping($anidbResource, 'ANIDB');
|
||||
if ($kitsuResource !== null) {
|
||||
@@ -71,7 +71,7 @@ class BackfillKitsuResource extends BackfillAnimeResource
|
||||
sleep(rand(1, 3));
|
||||
}
|
||||
|
||||
$annResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
$annResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANN);
|
||||
if ($annResource instanceof ExternalResource) {
|
||||
return $this->getKitsuMapping($annResource, 'ANIMENEWSNETWORK');
|
||||
}
|
||||
+7
-7
@@ -2,20 +2,20 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Pipes\Wiki\Anime\Resource;
|
||||
namespace App\Actions\Models\Wiki\Anime\Resource;
|
||||
|
||||
use App\Actions\Models\Wiki\Anime\BackfillAnimeResourceAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Pipes\Wiki\Anime\BackfillAnimeResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillMalResource.
|
||||
* Class BackfillMalResourceAction.
|
||||
*/
|
||||
class BackfillMalResource extends BackfillAnimeResource
|
||||
class BackfillMalResourceAction extends BackfillAnimeResourceAction
|
||||
{
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
@@ -36,7 +36,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
*/
|
||||
protected function getResource(): ?ExternalResource
|
||||
{
|
||||
$kitsuResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
$malResource = $this->getKitsuMalMapping($kitsuResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -44,7 +44,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$malResource = $this->getAnilistMalMapping($anilistResource);
|
||||
if ($malResource !== null) {
|
||||
@@ -52,7 +52,7 @@ class BackfillMalResource extends BackfillAnimeResource
|
||||
}
|
||||
}
|
||||
|
||||
$anidbResource = $this->anime->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
$anidbResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANIDB);
|
||||
if ($anidbResource instanceof ExternalResource) {
|
||||
return $this->getAnidbMalMapping($anidbResource);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\Studio;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillStudiosAction;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillAnimeStudiosAction.
|
||||
*
|
||||
* @extends BackfillStudiosAction<Anime>
|
||||
*/
|
||||
class BackfillAnimeStudiosAction extends BackfillStudiosAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Anime $anime
|
||||
*/
|
||||
public function __construct(Anime $anime)
|
||||
{
|
||||
parent::__construct($anime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Anime
|
||||
*/
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to studios.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->studios();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query third-party API for Anime Studios.
|
||||
*
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getStudios(): array
|
||||
{
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
$studios = $this->getMalAnimeStudios($malResource);
|
||||
if (! empty($studios)) {
|
||||
return $studios;
|
||||
}
|
||||
}
|
||||
|
||||
$anilistResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST);
|
||||
if ($anilistResource instanceof ExternalResource) {
|
||||
$studios = $this->getAnilistAnimeStudios($anilistResource);
|
||||
if (! empty($studios)) {
|
||||
return $studios;
|
||||
}
|
||||
}
|
||||
|
||||
$kitsuResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::KITSU);
|
||||
if ($kitsuResource instanceof ExternalResource) {
|
||||
return $this->getKitsuAnimeStudios($kitsuResource);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MAL API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $malResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getMalAnimeStudios(ExternalResource $malResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get('services.mal.client')])
|
||||
->get("https://api.myanimelist.net/v2/anime/$malResource->external_id", [
|
||||
'fields' => 'studios',
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$malStudios = Arr::get($response, 'studios', []);
|
||||
|
||||
foreach ($malStudios as $malStudio) {
|
||||
$name = Arr::get($malStudio, 'name');
|
||||
$id = Arr::get($malStudio, 'id');
|
||||
if (empty($name) || empty($id)) {
|
||||
Log::info("Skipping empty studio of name '$name' and id '$id' for MAL Resource '{$malResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
|
||||
$this->ensureStudioHasResource($studio, ResourceSite::MAL(), $id);
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Anilist API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $anilistResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getAnilistAnimeStudios(ExternalResource $anilistResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
Media (id: $id, type: ANIME) {
|
||||
studios (isMain: true) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'id' => $anilistResource->external_id,
|
||||
];
|
||||
|
||||
$response = Http::post('https://graphql.anilist.co', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$anilistStudios = Arr::get($response, 'data.Media.studios.nodes', []);
|
||||
|
||||
foreach ($anilistStudios as $anilistStudio) {
|
||||
$name = Arr::get($anilistStudio, 'name');
|
||||
$id = Arr::get($anilistStudio, 'id');
|
||||
if (empty($name) || empty($id)) {
|
||||
Log::info("Skipping empty studio of name '$name' and id '$id' for Anilist Resource '{$anilistResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
|
||||
$this->ensureStudioHasResource($studio, ResourceSite::ANILIST(), $id);
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Kitsu API for Anime Studios.
|
||||
*
|
||||
* @param ExternalResource $kitsuResource
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getKitsuAnimeStudios(ExternalResource $kitsuResource): array
|
||||
{
|
||||
$studios = [];
|
||||
|
||||
$query = '
|
||||
query ($id: ID!) {
|
||||
findAnimeById(id: $id) {
|
||||
productions(first:20) {
|
||||
nodes {
|
||||
role
|
||||
company {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'id' => $kitsuResource->external_id,
|
||||
];
|
||||
|
||||
$response = Http::post('https://kitsu.io/api/graphql', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$kitsuStudios = Arr::get($response, 'data.findAnimeById.productions.nodes', []);
|
||||
|
||||
foreach ($kitsuStudios as $kitsuStudio) {
|
||||
$role = Arr::get($kitsuStudio, 'role');
|
||||
$name = Arr::get($kitsuStudio, 'company.name');
|
||||
if ($role !== 'STUDIO' || empty($name)) {
|
||||
Log::info("Skipping production company of name '$name' and role '$role' for Anilist Resource '{$kitsuResource->getName()}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
$studios[] = $studio;
|
||||
}
|
||||
|
||||
return $studios;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
protected function attachStudios(array $studios): void
|
||||
{
|
||||
Log::info("Attaching studios to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach(Arr::pluck($studios, Studio::ATTRIBUTE_ID));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Testing\File;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillImageAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillImageAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Image of Facet '{$this->getFacet()->value}'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$image = $this->getImage();
|
||||
|
||||
if ($image !== null) {
|
||||
$this->attachImage($image);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(Image::ATTRIBUTE_FACET, $this->getFacet()->value)->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getFacet()->description} Image after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Image from response.
|
||||
*
|
||||
* @param string $url
|
||||
* @return Image
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function createImage(string $url): Image
|
||||
{
|
||||
$imageResponse = Http::get($url)->throw();
|
||||
|
||||
$image = $imageResponse->body();
|
||||
|
||||
$file = File::createWithContent(basename($url), $image);
|
||||
|
||||
$fs = Storage::disk(Config::get('image.disk'));
|
||||
|
||||
$fsFile = $fs->putFile($this->path(), $file);
|
||||
|
||||
/** @var Image $image */
|
||||
$image = Image::query()->create([
|
||||
Image::ATTRIBUTE_FACET => $this->getFacet()->value,
|
||||
Image::ATTRIBUTE_PATH => $fsFile,
|
||||
]);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Path to storage image in filesystem.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return Str::of(Str::kebab(class_basename($this->getModel())))
|
||||
->append(DIRECTORY_SEPARATOR)
|
||||
->append(Str::kebab($this->getFacet()->description))
|
||||
->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to model.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachImage(Image $image): void;
|
||||
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
abstract protected function getFacet(): ImageFacet;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getImage(): ?Image;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillResourceAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillResourceAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Resource of Site '{$this->getSite()->value}'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$resource = $this->getResource();
|
||||
|
||||
if ($resource !== null) {
|
||||
$this->attachResource($resource);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $this->getSite()->value)->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no {$this->getSite()->description} Resource after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Resource from response.
|
||||
*
|
||||
* @param int $id
|
||||
* @param string|null $slug
|
||||
* @return ExternalResource
|
||||
*/
|
||||
abstract protected function getOrCreateResource(int $id, string $slug = null): ExternalResource;
|
||||
|
||||
/**
|
||||
* Attach External Resource to model.
|
||||
*
|
||||
* @param ExternalResource $resource
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachResource(ExternalResource $resource): void;
|
||||
|
||||
/**
|
||||
* Get the site to backfill.
|
||||
*
|
||||
* @return ResourceSite
|
||||
*/
|
||||
abstract protected function getSite(): ResourceSite;
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Resource mapping.
|
||||
*
|
||||
* @return ExternalResource|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getResource(): ?ExternalResource;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BaseAction;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Pivots\StudioResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class BackfillStudiosAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
* @extends BaseAction<TModel>
|
||||
*/
|
||||
abstract class BackfillStudiosAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->exists()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Studios.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$studios = $this->getStudios();
|
||||
|
||||
if (! empty($studios)) {
|
||||
$this->attachStudios($studios);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no Studios after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create Studio from name (case-insensitive).
|
||||
*
|
||||
* @param string $name
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getOrCreateStudio(string $name): Studio
|
||||
{
|
||||
$column = Studio::ATTRIBUTE_NAME;
|
||||
$studio = Studio::query()
|
||||
->where(DB::raw("lower($column)"), Str::lower($name))
|
||||
->first();
|
||||
|
||||
if (! $studio instanceof Studio) {
|
||||
Log::info("Creating studio '$name'");
|
||||
|
||||
$studio = Studio::query()->create([
|
||||
Studio::ATTRIBUTE_NAME => $name,
|
||||
Studio::ATTRIBUTE_SLUG => Str::slug($name, '_'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $studio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure Studio has Resource.
|
||||
*
|
||||
* @param Studio $studio
|
||||
* @param ResourceSite $site
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
protected function ensureStudioHasResource(Studio $studio, ResourceSite $site, int $id): void
|
||||
{
|
||||
$studioResource = ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $site->value)
|
||||
->where(ExternalResource::ATTRIBUTE_EXTERNAL_ID, $id)
|
||||
->where(ExternalResource::ATTRIBUTE_LINK, ResourceSite::formatStudioResourceLink($site, $id))
|
||||
->first();
|
||||
|
||||
if (! $studioResource instanceof ExternalResource) {
|
||||
Log::info("Creating studio resource with site '$site->value' and id '$id'");
|
||||
|
||||
$studioResource = ExternalResource::query()->create([
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $id,
|
||||
ExternalResource::ATTRIBUTE_LINK => ResourceSite::formatStudioResourceLink($site, $id),
|
||||
ExternalResource::ATTRIBUTE_SITE => $site->value,
|
||||
]);
|
||||
}
|
||||
|
||||
if (StudioResource::query()
|
||||
->where($studio->getKeyName(), $studio->getKey())
|
||||
->where($studioResource->getKeyName(), $studioResource->getKey())
|
||||
->doesntExist()
|
||||
) {
|
||||
Log::info("Attaching resource '$studioResource->link' to studio '{$studio->getName()}'");
|
||||
$studioResource->studios()->attach($studio);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Studios.
|
||||
*
|
||||
* @param Studio[] $studios
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function attachStudios(array $studios): void;
|
||||
|
||||
/**
|
||||
* Query third-party API for Studios.
|
||||
*
|
||||
* @return Studio[]
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function getStudios(): array;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Studio;
|
||||
|
||||
use App\Actions\Models\Wiki\BackfillImageAction;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class BackfillStudioImageAction.
|
||||
*
|
||||
* @extends BackfillImageAction<Studio>
|
||||
*/
|
||||
abstract class BackfillStudioImageAction extends BackfillImageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Studio $studio
|
||||
*/
|
||||
public function __construct(Studio $studio)
|
||||
{
|
||||
parent::__construct($studio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Studio
|
||||
*/
|
||||
protected function getModel(): Studio
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to images.
|
||||
*
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->images();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Image to Studio.
|
||||
*
|
||||
* @param Image $image
|
||||
* @return void
|
||||
*/
|
||||
protected function attachImage(Image $image): void
|
||||
{
|
||||
Log::info("Attaching Image '{$image->getName()}' to {$this->label()} '{$this->getModel()->getName()}'");
|
||||
$this->relation()->attach($image);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Studio\Image;
|
||||
|
||||
use App\Actions\Models\Wiki\Studio\BackfillStudioImageAction;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
/**
|
||||
* Class BackfillLargeCoverImageAction.
|
||||
*/
|
||||
class BackfillLargeCoverImageAction extends BackfillStudioImageAction
|
||||
{
|
||||
/**
|
||||
* Get the facet to backfill.
|
||||
*
|
||||
* @return ImageFacet
|
||||
*/
|
||||
protected function getFacet(): ImageFacet
|
||||
{
|
||||
return ImageFacet::COVER_LARGE();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query third-party APIs to find Image.
|
||||
*
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getImage(): ?Image
|
||||
{
|
||||
$malResource = $this->getModel()->resources()->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL);
|
||||
if ($malResource instanceof ExternalResource) {
|
||||
return $this->getMalImage($malResource);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query MAL API for large cover image.
|
||||
*
|
||||
* @param ExternalResource $malResource
|
||||
* @return Image|null
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function getMalImage(ExternalResource $malResource): ?Image
|
||||
{
|
||||
return $this->createImage("https://cdn.myanimelist.net/images/company/$malResource->external_id.png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\ReconcileAudioRepositoriesAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo;
|
||||
use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Repositories\Eloquent\Wiki\AudioRepository as AudioDestinationRepository;
|
||||
use App\Repositories\Storage\Wiki\AudioRepository as AudioSourceRepository;
|
||||
use Exception;
|
||||
use FFMpeg\Coordinate\TimeCode;
|
||||
use FFMpeg\Filters\Audio\AddMetadataFilter;
|
||||
use FFMpeg\Filters\Audio\AudioClipFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;
|
||||
|
||||
/**
|
||||
* Class BackfillVideoAudioAction.
|
||||
*
|
||||
* @extends BaseAction<Video>
|
||||
*/
|
||||
class BackfillAudioAction extends BaseAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param DeriveSourceVideo $deriveSourceVideo
|
||||
* @param OverwriteAudio $overwriteAudio
|
||||
*/
|
||||
public function __construct(
|
||||
Video $video,
|
||||
protected readonly DeriveSourceVideo $deriveSourceVideo = new DeriveSourceVideo(DeriveSourceVideo::YES),
|
||||
protected readonly OverwriteAudio $overwriteAudio = new OverwriteAudio(OverwriteAudio::NO)
|
||||
) {
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->relation()->getQuery()->exists() && ! $this->overwriteAudio()) {
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Audio'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED());
|
||||
}
|
||||
|
||||
$audio = $this->getAudio();
|
||||
|
||||
if ($audio !== null) {
|
||||
$this->attachAudio($audio);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no Audio after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return Video
|
||||
*/
|
||||
protected function getModel(): Video
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to audio.
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
protected function relation(): BelongsTo
|
||||
{
|
||||
return $this->getModel()->audio();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the source video should be derived.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function deriveSourceVideo(): bool
|
||||
{
|
||||
return DeriveSourceVideo::YES()->is($this->deriveSourceVideo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if audio should be overwritten.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function overwriteAudio(): bool
|
||||
{
|
||||
return OverwriteAudio::YES()->is($this->overwriteAudio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Audio.
|
||||
*
|
||||
* @return Audio|null
|
||||
*/
|
||||
protected function getAudio(): ?Audio
|
||||
{
|
||||
// Allow bypassing of source video derivation
|
||||
$sourceVideo = $this->deriveSourceVideo()
|
||||
? $this->getSourceVideo()
|
||||
: $this->getModel();
|
||||
|
||||
// It's possible that the video is not attached to any themes, exit early.
|
||||
if ($sourceVideo === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, attempt to set audio from the source video
|
||||
$audio = $sourceVideo->audio;
|
||||
|
||||
// Anticipate audio path for FFmpeg save file
|
||||
$audioPath = $audio === null
|
||||
? Str::replace('webm', 'ogg', $sourceVideo->path)
|
||||
: $audio->path;
|
||||
|
||||
// Second, attempt to set audio from path
|
||||
if ($audio === null) {
|
||||
$audio = Audio::query()->firstWhere(Audio::ATTRIBUTE_PATH, $audioPath);
|
||||
}
|
||||
|
||||
// Finally, extract audio from the source video
|
||||
if ($audio === null || $this->overwriteAudio()) {
|
||||
Log::info("Extracting Audio from Video '{$sourceVideo->getName()}'");
|
||||
|
||||
$this->extractAudio($sourceVideo, $audioPath);
|
||||
$results = $this->reconcileAudio($audioPath);
|
||||
$results->toLog();
|
||||
|
||||
if ($audio === null) {
|
||||
$audio = $results->getCreated()->firstWhere(fn (Audio $audio) => $audio->path === $audioPath);
|
||||
}
|
||||
}
|
||||
|
||||
return $audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the source video for the given video.
|
||||
*
|
||||
* @return Video|null
|
||||
*/
|
||||
protected function getSourceVideo(): ?Video
|
||||
{
|
||||
$source = null;
|
||||
|
||||
$sourceCandidates = $this->getAdjacentVideos();
|
||||
|
||||
foreach ($sourceCandidates as $sourceCandidate) {
|
||||
if (! $source instanceof Video || $sourceCandidate->getSourcePriority() > $source->getSourcePriority()) {
|
||||
$source = $sourceCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the adjacent videos for sourcing.
|
||||
*
|
||||
* @return Collection<int, Video>
|
||||
*/
|
||||
protected function getAdjacentVideos(): Collection
|
||||
{
|
||||
$builder = AnimeTheme::query();
|
||||
|
||||
$sortRelation = $builder->getRelation(AnimeTheme::RELATION_ANIME);
|
||||
|
||||
$orderByNameQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_NAME]);
|
||||
$orderBySeasonQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_SEASON]);
|
||||
$orderByYearQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_YEAR]);
|
||||
|
||||
return $builder->whereHas(AnimeTheme::RELATION_VIDEOS, fn (Builder $relationBuilder) => $relationBuilder->whereKey($this->getModel()))
|
||||
->orderBy($orderByYearQuery->toBase())
|
||||
->orderBy($orderBySeasonQuery->toBase())
|
||||
->orderBy($orderByNameQuery->toBase())
|
||||
->with([
|
||||
AnimeTheme::RELATION_ANIME,
|
||||
AnimeTheme::RELATION_AUDIO,
|
||||
AnimeTheme::RELATION_ENTRIES => fn (Relation $relation) => $relation->getQuery()->orderBy(AnimeThemeEntry::ATTRIBUTE_VERSION),
|
||||
])
|
||||
->get()
|
||||
->flatMap(fn (AnimeTheme $theme) => $theme->animethemeentries)
|
||||
->flatMap(fn (AnimeThemeEntry $entry) => $entry->videos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract audio stream from video and store in filesystem.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param string $audioPath
|
||||
* @return void
|
||||
*/
|
||||
protected function extractAudio(Video $video, string $audioPath): void
|
||||
{
|
||||
try {
|
||||
foreach (Config::get(AudioConstants::DISKS_QUALIFIED, []) as $audioDisk) {
|
||||
FFMpeg::fromDisk(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED))
|
||||
->open($video->path)
|
||||
->addFilter(new AudioClipFilter(new TimeCode(0, 0, 0, 0)))
|
||||
->addFilter(new AddMetadataFilter())
|
||||
->export()
|
||||
->toDisk($audioDisk)
|
||||
->save($audioPath);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
} finally {
|
||||
FFMpeg::cleanupTemporaryFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile audio repositories.
|
||||
*
|
||||
* @param string $audioPath
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
protected function reconcileAudio(string $audioPath): ReconcileResults
|
||||
{
|
||||
$action = new ReconcileAudioRepositoriesAction();
|
||||
|
||||
/** @var RepositoryInterface $sourceRepository */
|
||||
$sourceRepository = App::make(AudioSourceRepository::class);
|
||||
$sourceRepository->handleFilter('path', File::dirname($audioPath));
|
||||
|
||||
/** @var RepositoryInterface $destinationRepository */
|
||||
$destinationRepository = App::make(AudioDestinationRepository::class);
|
||||
$destinationRepository->handleFilter('path', File::dirname($audioPath));
|
||||
|
||||
return $action->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach Audio to model.
|
||||
*
|
||||
* @param Audio $audio
|
||||
* @return void
|
||||
*/
|
||||
protected function attachAudio(Audio $audio): void
|
||||
{
|
||||
if ($this->relation()->isNot($audio)) {
|
||||
Log::info("Associating Audio '{$audio->getName()}' with Video '{$this->getModel()->getName()}'");
|
||||
$this->relation()->associate($audio)->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Balance;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Balance;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileBalanceRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositoriesAction<Balance>
|
||||
*/
|
||||
class ReconcileBalanceRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Balance::ATTRIBUTE_DATE,
|
||||
Balance::ATTRIBUTE_ID,
|
||||
Balance::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Balance $first, Balance $second) => $first->date->format(AllowedDateFormat::YM) <=> $second->date->format(AllowedDateFormat::YM);
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Balance::ATTRIBUTE_BALANCE,
|
||||
Balance::ATTRIBUTE_DATE,
|
||||
Balance::ATTRIBUTE_FREQUENCY,
|
||||
Balance::ATTRIBUTE_ID,
|
||||
Balance::ATTRIBUTE_SERVICE,
|
||||
Balance::ATTRIBUTE_USAGE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Balance $first, Balance $second) => [$first->date->format(AllowedDateFormat::YMD), $first->usage, $first->balance]
|
||||
<=> [$second->date->format(AllowedDateFormat::YMD), $second->usage, $second->balance];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$formattedDestinationDate = $destinationModel->getAttribute(Balance::ATTRIBUTE_DATE)->format(AllowedDateFormat::YM);
|
||||
|
||||
return $sourceModels->first(
|
||||
fn (Balance $balance) => $balance->date->format(AllowedDateFormat::YM) === $formattedDestinationDate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ReconcileBalanceResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Balance;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Billing\Balance;
|
||||
|
||||
/**
|
||||
* Class ReconcileBalanceResults.
|
||||
*
|
||||
* @extends ReconcileResults<Balance>
|
||||
*/
|
||||
class ReconcileBalanceResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Balance>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Balance::class;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Transaction;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Transaction;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileTransactionRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositoriesAction<Transaction>
|
||||
*/
|
||||
class ReconcileTransactionRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Transaction::ATTRIBUTE_AMOUNT,
|
||||
Transaction::ATTRIBUTE_DATE,
|
||||
Transaction::ATTRIBUTE_EXTERNAL_ID,
|
||||
Transaction::ATTRIBUTE_ID,
|
||||
Transaction::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Transaction $first, Transaction $second) => [$first->external_id, $first->date->format(AllowedDateFormat::YMD), $first->amount]
|
||||
<=> [$second->external_id, $second->date->format(AllowedDateFormat::YMD), $second->amount];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ReconcileTransactionResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Billing\Transaction;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Billing\Transaction;
|
||||
|
||||
/**
|
||||
* Class ReconcileTransactionResults.
|
||||
*
|
||||
* @extends ReconcileResults<Transaction>
|
||||
*/
|
||||
class ReconcileTransactionResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Transaction>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Transaction::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories;
|
||||
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileRepositories.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $source
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
public function reconcileRepositories(RepositoryInterface $source, RepositoryInterface $destination): ReconcileResults
|
||||
{
|
||||
$sourceModels = $source->get();
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForCreateDelete());
|
||||
|
||||
$created = $this->createModelsFromSource($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$deleted = $this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForUpdate());
|
||||
|
||||
$updated = $this->updateDestinationModels($destination, $sourceModels, $destinationModels);
|
||||
|
||||
return $this->getResults($created, $deleted, $updated);
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForCreateDelete(): array;
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
abstract protected function diffCallbackForCreateDelete(): Closure;
|
||||
|
||||
/**
|
||||
* Create models that exist in source but not in destination.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function createModelsFromSource(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $createModels->each(fn (Model $createModel) => $destination->save($createModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models that exist in destination but not in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function deleteModelsFromDestination(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $deleteModels->each(fn (Model $deleteModel) => $destination->delete($deleteModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForUpdate(): array;
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
abstract protected function diffCallbackForUpdate(): Closure;
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*
|
||||
* @param Collection $sourceModels
|
||||
* @param Model $destinationModel
|
||||
* @return Model|null
|
||||
*/
|
||||
abstract protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model;
|
||||
|
||||
/**
|
||||
* Update destination models that have changed in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return Collection
|
||||
*/
|
||||
protected function updateDestinationModels(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
|
||||
|
||||
return $updatedModels->each(function (Model $updatedModel) use ($sourceModels, $destination) {
|
||||
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
|
||||
if ($sourceModel !== null) {
|
||||
$destination->update($updatedModel, $sourceModel->toArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
* @return ReconcileResults
|
||||
*/
|
||||
abstract protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class ReconcileResults.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileResults extends ActionResult
|
||||
{
|
||||
/**
|
||||
* Create a new results instance.
|
||||
*
|
||||
* @param Collection $created
|
||||
* @param Collection $deleted
|
||||
* @param Collection $updated
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly Collection $created = new Collection(),
|
||||
protected readonly Collection $deleted = new Collection(),
|
||||
protected readonly Collection $updated = new Collection()
|
||||
) {
|
||||
parent::__construct(ActionStatus::PASSED());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created models.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function getCreated(): Collection
|
||||
{
|
||||
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
|
||||
*/
|
||||
public function hasChanges(): bool
|
||||
{
|
||||
return $this->created->isNotEmpty() || $this->deleted->isNotEmpty() || $this->updated->isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to log.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' created"));
|
||||
$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->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to console output.
|
||||
*
|
||||
* @param Command $command
|
||||
* @return void
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function toConsole(Command $command): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' created"));
|
||||
$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->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result message.
|
||||
*
|
||||
* @return string|null
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
return "No {$this->label($this->created)} created or deleted or updated";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<TModel>
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Get the user-friendly label for the model class name.
|
||||
*
|
||||
* @param int|array|Countable $models
|
||||
* @return string
|
||||
*/
|
||||
protected function label(int|array|Countable $models = 1): string
|
||||
{
|
||||
return Str::plural(Str::headline(class_basename($this->model())), $models);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileAudioRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositoriesAction<Audio>
|
||||
*/
|
||||
class ReconcileAudioRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second) => $first->basename <=> $second->basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_FILENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
Audio::ATTRIBUTE_MIMETYPE,
|
||||
Audio::ATTRIBUTE_PATH,
|
||||
Audio::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second) => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $sourceModels->firstWhere(
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Audio::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ReconcileAudioResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* Class ReconcileAudioResults.
|
||||
*
|
||||
* @extends ReconcileResults<Audio>
|
||||
*/
|
||||
class ReconcileAudioResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Audio>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Audio::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Class ReconcileVideoRepositories.
|
||||
*
|
||||
* @extends ReconcileRepositoriesAction<Video>
|
||||
*/
|
||||
class ReconcileVideoRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Video $first, Video $second) => $first->basename <=> $second->basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_FILENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
Video::ATTRIBUTE_MIMETYPE,
|
||||
Video::ATTRIBUTE_PATH,
|
||||
Video::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Video $first, Video $second) => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $sourceModels->firstWhere(
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Video::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ReconcileVideoResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
/**
|
||||
* Class ReconcileVideoResults.
|
||||
*
|
||||
* @extends ReconcileResults<Video>
|
||||
*/
|
||||
class ReconcileVideoResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Video>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Video::class;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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\Database\Connection;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Database\SQLiteConnection;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use Spatie\DbDumper\Databases\MySql;
|
||||
use Spatie\DbDumper\Databases\PostgreSql;
|
||||
use Spatie\DbDumper\Databases\Sqlite;
|
||||
use Spatie\DbDumper\DbDumper;
|
||||
use Spatie\DbDumper\Exceptions\CannotSetParameter;
|
||||
|
||||
/**
|
||||
* Class DumpAction.
|
||||
*/
|
||||
abstract class DumpAction
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(protected readonly array $options = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
$dumpFile = $this->getDumpFile();
|
||||
|
||||
try {
|
||||
/** @var Connection $connection */
|
||||
$connection = DB::connection();
|
||||
|
||||
$dumper = $this->getDumper($connection);
|
||||
if ($dumper === null) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Unrecognized connection '{$connection->getName()}'"
|
||||
);
|
||||
}
|
||||
|
||||
// First, Dump file to temporary location
|
||||
// Writing to disks is not fully supported
|
||||
$dumper->dumpToFile($dumpFile);
|
||||
|
||||
// Then, store dump file in intended location
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk(Config::get(DumpConstants::DISK_QUALIFIED));
|
||||
$fs->putFileAs('', $dumpFile, File::basename($dumpFile));
|
||||
|
||||
// Finally, delete the temporary file
|
||||
File::delete($dumpFile);
|
||||
} catch (Exception $e) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$reconcileResults = $this->reconcileRepositories();
|
||||
|
||||
$reconcileResults->toLog();
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Database dump '$dumpFile' has been created",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dumper for the database connection.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return DbDumper|null
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function getDumper(Connection $connection): ?DbDumper
|
||||
{
|
||||
return match (get_class($connection)) {
|
||||
SQLiteConnection::class => $this->prepareSqliteDumper($connection),
|
||||
MySqlConnection::class => $this->prepareMySqlDumper($connection),
|
||||
PostgresConnection::class => $this->preparePostgreSqlDumper($connection),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Sqlite database dumper.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return Sqlite
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function prepareSqliteDumper(Connection $connection): Sqlite
|
||||
{
|
||||
if (version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '<')) {
|
||||
throw new RuntimeException('DB connection does not support includeTables option');
|
||||
}
|
||||
|
||||
$dumper = Sqlite::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure MySQL database dumper.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return MySql
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function prepareMySqlDumper(Connection $connection): MySql
|
||||
{
|
||||
$dumper = MySql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$hostConfig = $connection->getConfig('host');
|
||||
if ($hostConfig !== null) {
|
||||
$dumper->setHost(collect($hostConfig)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_numeric($port)) {
|
||||
$dumper->setPort(intval($port));
|
||||
}
|
||||
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
if ($this->option('comments')) {
|
||||
$dumper->dontSkipComments();
|
||||
}
|
||||
|
||||
if ($this->option('skip-comments')) {
|
||||
$dumper->skipComments();
|
||||
}
|
||||
|
||||
if ($this->option('extended-insert')) {
|
||||
$dumper->useExtendedInserts();
|
||||
}
|
||||
|
||||
if ($this->option('skip-extended-insert')) {
|
||||
$dumper->dontUseExtendedInserts();
|
||||
}
|
||||
|
||||
$this->option('single-transaction')
|
||||
? $dumper->useSingleTransaction()
|
||||
: $dumper->dontUseSingleTransaction();
|
||||
|
||||
if ($this->option('lock-tables')) {
|
||||
$dumper->dontSkipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-lock-tables')) {
|
||||
$dumper->skipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-column-statistics')) {
|
||||
$dumper->doNotUseColumnStatistics();
|
||||
}
|
||||
|
||||
if ($this->option('quick')) {
|
||||
$dumper->useQuick();
|
||||
}
|
||||
|
||||
if ($this->option('skip-quick')) {
|
||||
$dumper->dontUseQuick();
|
||||
}
|
||||
|
||||
if ($this->hasOption('default-character-set')) {
|
||||
$dumper->setDefaultCharacterSet($this->option('default-character-set'));
|
||||
}
|
||||
|
||||
if ($this->hasOption('set-gtid-purged')) {
|
||||
$dumper->setGtidPurged($this->option('set-gtid-purged'));
|
||||
}
|
||||
|
||||
if ($this->option('no-create-info')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure PostgreSql database dumper.
|
||||
*
|
||||
* @param Connection $connection
|
||||
* @return PostgreSql
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function preparePostgreSqlDumper(Connection $connection): PostgreSql
|
||||
{
|
||||
$dumper = PostgreSql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$host = $connection->getConfig('host');
|
||||
if ($host !== null) {
|
||||
$dumper->setHost(collect($host)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_int($port)) {
|
||||
$dumper->setPort($port);
|
||||
}
|
||||
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
if ($this->option('inserts')) {
|
||||
$dumper->useInserts();
|
||||
}
|
||||
|
||||
if ($this->option('data-only')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the string option is set.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasOption(string $key): bool
|
||||
{
|
||||
return is_string(Arr::get($this->options, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getDumpFile(): string;
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function allowedTables(): array;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+26
-19
@@ -2,20 +2,22 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Wiki;
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Console\Commands\DatabaseDumpCommand;
|
||||
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;
|
||||
@@ -25,26 +27,20 @@ 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 WikiDatabaseDumpCommand.
|
||||
* Class DumpWikiAction.
|
||||
*/
|
||||
class WikiDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
class DumpWikiAction extends DumpAction
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:dump-wiki {--C|create : Whether the dumper should include create table statements}';
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Produces sanitized database dump, targeting wiki-related tables for seeding purposes';
|
||||
final public const FILENAME_PREFIX = 'animethemes-db-dump-wiki-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
@@ -68,23 +64,34 @@ class WikiDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
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 directory that the file should be dumped to.
|
||||
* 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 getDumpFilePath(): string
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
return 'wiki';
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?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\Repositories\RepositoryInterface;
|
||||
use App\Contracts\Storage\InteractsWithDisks;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Class DeleteAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class DeleteAction implements InteractsWithDisks, StorageAction
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected readonly BaseModel $model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->delete($this->path());
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
return new DeleteResults($this->model, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
$sourceRepository->handleFilter('path', File::dirname($this->path()));
|
||||
$destinationRepository->handleFilter('path', File::dirname($this->path()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes to be completed after handling action.
|
||||
*
|
||||
* @param StorageResults $storageResults
|
||||
* @return void
|
||||
*/
|
||||
public function then(StorageResults $storageResults): void
|
||||
{
|
||||
$reconcileResults = $this->reconcileRepositories();
|
||||
|
||||
$reconcileResults->toLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function path(): string;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?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 App\Models\BaseModel;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class DeleteResults.
|
||||
*/
|
||||
class DeleteResults implements StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @param array<string, bool> $deletions
|
||||
*/
|
||||
public function __construct(protected readonly BaseModel $model, protected readonly array $deletions = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->deletions)) {
|
||||
Log::error('No deletions were attempted.');
|
||||
}
|
||||
foreach ($this->deletions as $fs => $result) {
|
||||
$result
|
||||
? Log::info("Deleted '{$this->model->getName()}' from disk '$fs'")
|
||||
: Log::error("Failed to delete '{$this->model->getName()}' from disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->deletions)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No deletions were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $passed */
|
||||
/** @var Collection $failed */
|
||||
[$passed, $failed] = collect($this->deletions)->partition(fn (bool $result, string $fs) => $result);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to delete '{$this->model->getName()}' from disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Deleted '{$this->model->getName()}' from disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Class MoveAction.
|
||||
*
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class MoveAction implements InteractsWithDisks, StorageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model, protected readonly string $to)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
$from = $this->from();
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->move($from, $this->to);
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function from(): string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
abstract protected function update(): void;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?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 App\Models\BaseModel;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Class MoveResults.
|
||||
*/
|
||||
class MoveResults implements StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @param array<string, bool> $moves
|
||||
*/
|
||||
public function __construct(
|
||||
protected readonly BaseModel $model,
|
||||
protected readonly string $from,
|
||||
protected readonly string $to,
|
||||
protected readonly array $moves = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->moves)) {
|
||||
Log::error('No moves were attempted.');
|
||||
}
|
||||
foreach ($this->moves as $fs => $result) {
|
||||
$result
|
||||
? Log::info("Moved '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'")
|
||||
: Log::error("Failed to move '{$this->model->getName()}' from '$this->from' to '$this->to' in disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->moves)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No moves were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $passed */
|
||||
/** @var Collection $failed */
|
||||
[$passed, $failed] = collect($this->moves)->partition(fn (bool $result, string $fs) => $result);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to move '{$this->model->getName()}' from '$this->from' to '$this->to' in disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Moved '{$this->model->getName()}' from '$this->from' to '$this->to' in disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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'."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Base;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Class UploadAction.
|
||||
*/
|
||||
abstract class UploadAction implements InteractsWithDisks, StorageAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(protected readonly UploadedFile $file, protected readonly string $path)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle action.
|
||||
*
|
||||
* @return StorageResults
|
||||
*/
|
||||
public function handle(): StorageResults
|
||||
{
|
||||
$results = [];
|
||||
|
||||
foreach ($this->disks() as $disk) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($disk);
|
||||
|
||||
$result = $fs->putFileAs($this->path, $this->file, $this->file->getClientOriginalName());
|
||||
|
||||
$results[$disk] = $result;
|
||||
}
|
||||
|
||||
return new UploadResults($results);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?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 UploadResults.
|
||||
*/
|
||||
class UploadResults implements StorageResults
|
||||
{
|
||||
/**
|
||||
* Create a new action result instance.
|
||||
*
|
||||
* @param array<string, string|false> $uploads
|
||||
*/
|
||||
public function __construct(protected readonly array $uploads = [])
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Write results to log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
Log::error('No uploads were attempted.');
|
||||
}
|
||||
foreach ($this->uploads as $fs => $result) {
|
||||
$result === false
|
||||
? Log::error("Failed to upload to disk '$fs'")
|
||||
: Log::info("Uploaded '$result' to disk '$fs'");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return ActionResult
|
||||
*/
|
||||
public function toActionResult(): ActionResult
|
||||
{
|
||||
if (empty($this->uploads)) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
'No uploads were attempted. Please check that disks are configured.'
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection $failed */
|
||||
/** @var Collection $passed */
|
||||
[$failed, $passed] = collect($this->uploads)->partition(fn (string|false $result, string $fs) => $result === false);
|
||||
|
||||
if ($failed->isNotEmpty()) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED(),
|
||||
"Failed to upload to disks {$failed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED(),
|
||||
"Uploaded '{$passed->values()->first()}' to disks {$passed->keys()->join(', ', ' & ')}."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Storage\Base\DeleteAction;
|
||||
use App\Concerns\Repositories\Wiki\ReconcilesAudioRepositories;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class DeleteAudioAction.
|
||||
*
|
||||
* @extends DeleteAction<Audio>
|
||||
*/
|
||||
class DeleteAudioAction extends DeleteAction
|
||||
{
|
||||
use ReconcilesAudioRepositories;
|
||||
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
*/
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of disk names.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Storage\Base\MoveAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Class MoveAudioAction.
|
||||
*
|
||||
* @extends MoveAction<Audio>
|
||||
*/
|
||||
class MoveAudioAction extends MoveAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Audio $audio
|
||||
* @param string $to
|
||||
*/
|
||||
public function __construct(Audio $audio, string $to)
|
||||
{
|
||||
parent::__construct($audio, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of disk names.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_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([
|
||||
Audio::ATTRIBUTE_BASENAME => File::basename($this->to),
|
||||
Audio::ATTRIBUTE_FILENAME => File::name($this->to),
|
||||
Audio::ATTRIBUTE_PATH => $this->to,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Audio;
|
||||
|
||||
use App\Actions\Storage\Base\UploadAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
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.
|
||||
*/
|
||||
class UploadAudioAction extends UploadAction
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(AudioConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Storage\Base\DeleteAction;
|
||||
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* Class DeleteVideoAction.
|
||||
*
|
||||
* @extends DeleteAction<Video>
|
||||
*/
|
||||
class DeleteVideoAction extends DeleteAction
|
||||
{
|
||||
use ReconcilesVideoRepositories;
|
||||
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
*/
|
||||
public function __construct(Video $video)
|
||||
{
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of disk names.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to delete.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Storage\Base\MoveAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
/**
|
||||
* Class MoveVideoAction.
|
||||
*
|
||||
* @extends MoveAction<Video>
|
||||
*/
|
||||
class MoveVideoAction extends MoveAction
|
||||
{
|
||||
/**
|
||||
* Create a new action instance.
|
||||
*
|
||||
* @param Video $video
|
||||
* @param string $to
|
||||
*/
|
||||
public function __construct(Video $video, string $to)
|
||||
{
|
||||
parent::__construct($video, $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of disk names.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_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([
|
||||
Video::ATTRIBUTE_BASENAME => File::basename($this->to),
|
||||
Video::ATTRIBUTE_FILENAME => File::name($this->to),
|
||||
Video::ATTRIBUTE_PATH => $this->to,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Wiki\Video;
|
||||
|
||||
use App\Actions\Storage\Base\UploadAction;
|
||||
use App\Actions\Storage\Wiki\Video\Script\UploadScriptAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
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.
|
||||
*/
|
||||
class UploadVideoAction extends UploadAction
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function disks(): array
|
||||
{
|
||||
return Config::get(VideoConstants::DISKS_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -4,84 +4,59 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Billing;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Balance;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
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
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return string[]
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
protected function getSourceRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return [
|
||||
Balance::ATTRIBUTE_DATE,
|
||||
Balance::ATTRIBUTE_ID,
|
||||
Balance::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
|
||||
|
||||
return match ($service?->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return Closure
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return fn (Balance $first, Balance $second) => $first->date->format(AllowedDateFormat::YM) <=> $second->date->format(AllowedDateFormat::YM);
|
||||
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
|
||||
|
||||
return match ($service?->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return string[]
|
||||
* @return ReconcileRepositoriesAction
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
protected function action(): ReconcileRepositoriesAction
|
||||
{
|
||||
return [
|
||||
Balance::ATTRIBUTE_BALANCE,
|
||||
Balance::ATTRIBUTE_DATE,
|
||||
Balance::ATTRIBUTE_ID,
|
||||
Balance::ATTRIBUTE_SERVICE,
|
||||
Balance::ATTRIBUTE_USAGE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Balance $first, Balance $second) => [$first->date->format(AllowedDateFormat::YMD), $first->usage, $first->balance]
|
||||
<=> [$second->date->format(AllowedDateFormat::YMD), $second->usage, $second->balance];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
$formattedDestinationDate = $destinationModel->getAttribute(Balance::ATTRIBUTE_DATE)->format(AllowedDateFormat::YM);
|
||||
|
||||
return $sourceModels->first(
|
||||
fn (Balance $balance) => $balance->date->format(AllowedDateFormat::YM) === $formattedDestinationDate
|
||||
);
|
||||
return new ReconcileBalanceRepositoriesAction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,42 +4,59 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Billing;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Enums\Http\Api\Filter\AllowedDateFormat;
|
||||
use App\Models\Billing\Transaction;
|
||||
use Closure;
|
||||
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
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return string[]
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
protected function getSourceRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return [
|
||||
Transaction::ATTRIBUTE_AMOUNT,
|
||||
Transaction::ATTRIBUTE_DATE,
|
||||
Transaction::ATTRIBUTE_EXTERNAL_ID,
|
||||
Transaction::ATTRIBUTE_ID,
|
||||
Transaction::ATTRIBUTE_SERVICE,
|
||||
];
|
||||
$service = Service::unstrictCoerce(Arr::get($data, 'service'));
|
||||
|
||||
return match ($service?->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return Closure
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return fn (Transaction $first, Transaction $second) => [$first->external_id, $first->date->format(AllowedDateFormat::YMD), $first->amount]
|
||||
<=> [$second->external_id, $second->date->format(AllowedDateFormat::YMD), $second->amount];
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories;
|
||||
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Models\BaseModel;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
|
||||
/**
|
||||
* Trait ReconcilesRepositories.
|
||||
@@ -17,325 +15,72 @@ use Illuminate\Support\Collection;
|
||||
trait ReconcilesRepositories
|
||||
{
|
||||
/**
|
||||
* The number of models created.
|
||||
* Reconcile repositories.
|
||||
*
|
||||
* @var int
|
||||
* @param array $data
|
||||
* @return ActionResult
|
||||
*/
|
||||
protected int $created = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose creation failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $createdFailed = 0;
|
||||
|
||||
/**
|
||||
* The number of models deleted.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $deleted = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose deletion failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $deletedFailed = 0;
|
||||
|
||||
/**
|
||||
* The number of models updated.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $updated = 0;
|
||||
|
||||
/**
|
||||
* The number of models whose update failed.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $updatedFailed = 0;
|
||||
|
||||
/**
|
||||
* Callback for successful model creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(BaseModel $model): void
|
||||
protected function reconcileRepositories(array $data = []): ActionResult
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for successful model deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for successful model update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for failed model update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(BaseModel $model): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for handling completion of reconciliation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any changes, successful or not, were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasResults(): bool
|
||||
{
|
||||
return $this->hasChanges() || $this->hasFailures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any successful changes were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasChanges(): bool
|
||||
{
|
||||
return $this->created > 0 || $this->deleted > 0 || $this->updated > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any unsuccessful changes were made during reconciliation.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasFailures(): bool
|
||||
{
|
||||
return $this->createdFailed > 0 || $this->deletedFailed > 0 || $this->updatedFailed > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param Repository $source
|
||||
* @param Repository $destination
|
||||
* @return void
|
||||
*/
|
||||
public function reconcileRepositories(Repository $source, Repository $destination): void
|
||||
{
|
||||
try {
|
||||
$sourceModels = $source->get();
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForCreateDelete());
|
||||
|
||||
$this->createModelsFromSource($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForUpdate());
|
||||
|
||||
$this->updateDestinationModels($destination, $sourceModels, $destinationModels);
|
||||
} catch (Exception $exception) {
|
||||
$this->handleException($exception);
|
||||
} finally {
|
||||
$this->postReconciliationTask();
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return string[]
|
||||
* @return ReconcileRepositoriesAction
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
abstract protected function action(): ReconcileRepositoriesAction;
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
* Get source repository.
|
||||
*
|
||||
* @return Closure
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn () => 0;
|
||||
}
|
||||
abstract protected function getSourceRepository(array $data = []): ?RepositoryInterface;
|
||||
|
||||
/**
|
||||
* Create models that exist in source but not in destination.
|
||||
* Get destination repository.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @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 createModelsFromSource(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
protected function handleFilters(
|
||||
RepositoryInterface $sourceRepository,
|
||||
RepositoryInterface $destinationRepository,
|
||||
array $data = []
|
||||
): void {
|
||||
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
foreach ($createModels as $createModel) {
|
||||
$createResult = $destination->save($createModel);
|
||||
if ($createResult) {
|
||||
$this->created++;
|
||||
$this->handleCreated($createModel);
|
||||
} else {
|
||||
$this->createdFailed++;
|
||||
$this->handleFailedCreation($createModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models that exist in destination but not in source.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return void
|
||||
*/
|
||||
protected function deleteModelsFromDestination(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): void {
|
||||
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
foreach ($deleteModels as $deleteModel) {
|
||||
$deleteResult = $destination->delete($deleteModel);
|
||||
if ($deleteResult) {
|
||||
$this->deleted++;
|
||||
$this->handleDeleted($deleteModel);
|
||||
} else {
|
||||
$this->deletedFailed++;
|
||||
$this->handleFailedDeletion($deleteModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update destination models that have changed in source.
|
||||
*
|
||||
* @param Repository $destination
|
||||
* @param Collection $sourceModels
|
||||
* @param Collection $destinationModels
|
||||
* @return void
|
||||
*/
|
||||
protected function updateDestinationModels(
|
||||
Repository $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): void {
|
||||
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
|
||||
|
||||
foreach ($updatedModels as $updatedModel) {
|
||||
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
|
||||
if ($sourceModel !== null) {
|
||||
$updateResult = $destination->update($updatedModel, $sourceModel->toArray());
|
||||
if ($updateResult) {
|
||||
$this->updated++;
|
||||
$this->handleUpdated($updatedModel);
|
||||
} else {
|
||||
$this->updatedFailed++;
|
||||
$this->handleFailedUpdate($updatedModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -4,79 +4,47 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Concerns\Repositories\Wiki;
|
||||
|
||||
use App\Concerns\Repositories\ReconcilesRepositories;
|
||||
use App\Models\Wiki\Video;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
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
|
||||
{
|
||||
use ReconcilesRepositories;
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
* Get source repository for action.
|
||||
*
|
||||
* @return string[]
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
protected function getSourceRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
];
|
||||
return App::make(VideoSourceRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
* Get destination repository for action.
|
||||
*
|
||||
* @return Closure
|
||||
* @param array $data
|
||||
* @return RepositoryInterface|null
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
protected function getDestinationRepository(array $data = []): ?RepositoryInterface
|
||||
{
|
||||
return fn (Video $first, Video $second) => $first->basename <=> $second->basename;
|
||||
return App::make(VideoDestinationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
* Get the reconcile action.
|
||||
*
|
||||
* @return string[]
|
||||
* @return ReconcileRepositoriesAction
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
protected function action(): ReconcileRepositoriesAction
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
Video::ATTRIBUTE_PATH,
|
||||
Video::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*
|
||||
* @return Closure
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Video $first, Video $second) => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $sourceModels->firstWhere(
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Video::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing\Balance;
|
||||
|
||||
use App\Concerns\Repositories\Billing\ReconcilesBalanceRepositories;
|
||||
use App\Console\Commands\Billing\ServiceReconcileCommand;
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\BaseModel;
|
||||
use App\Repositories\Eloquent\Billing\DigitalOceanBalanceRepository as DigitalOceanDestinationRepository;
|
||||
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Print the result to console and log the results to the app log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
{
|
||||
if ($this->hasResults()) {
|
||||
if ($this->hasChanges()) {
|
||||
Log::info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
|
||||
$this->info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
|
||||
}
|
||||
if ($this->hasFailures()) {
|
||||
Log::error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
|
||||
$this->error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
|
||||
}
|
||||
} else {
|
||||
Log::info('No Balances created or deleted or updated');
|
||||
$this->info('No Balances created or deleted or updated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' created");
|
||||
$this->info("Balance '{$model->getName()}' created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance creation.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not created");
|
||||
$this->error("Balance '{$model->getName()}' was not created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' deleted");
|
||||
$this->info("Balance '{$model->getName()}' deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance deletion.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not deleted");
|
||||
$this->error("Balance '{$model->getName()}' was not deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful balance update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(BaseModel $model): void
|
||||
{
|
||||
Log::info("Balance '{$model->getName()}' updated");
|
||||
$this->info("Balance '{$model->getName()}' updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed balance update.
|
||||
*
|
||||
* @param BaseModel $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(BaseModel $model): void
|
||||
{
|
||||
Log::error("Balance '{$model->getName()}' was not updated");
|
||||
$this->error("Balance '{$model->getName()}' was not updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
Log::error($exception->getMessage());
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
protected function getSourceRepository(Service $service): ?Repository
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
protected function getDestinationRepository(Service $service): ?Repository
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing;
|
||||
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* Class ServiceReconcileCommand.
|
||||
*/
|
||||
abstract class ServiceReconcileCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$key = $this->argument('service');
|
||||
$service = Service::coerce(Str::upper($key));
|
||||
|
||||
if ($service === null) {
|
||||
Log::error("Invalid Service '$key'");
|
||||
$this->error("Invalid Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$sourceRepository = $this->getSourceRepository($service);
|
||||
if ($sourceRepository === null) {
|
||||
Log::error("No source repository implemented for Service '$key'");
|
||||
$this->error("No source repository implemented for Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$destinationRepository = $this->getDestinationRepository($service);
|
||||
if ($destinationRepository === null) {
|
||||
Log::error("No destination repository implemented for Service '$key'");
|
||||
$this->error("No destination repository implemented for Service '$key'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->reconcileRepositories($sourceRepository, $destinationRepository);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param Repository $source
|
||||
* @param Repository $destination
|
||||
* @return void
|
||||
*/
|
||||
abstract public function reconcileRepositories(Repository $source, Repository $destination): void;
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
abstract protected function getSourceRepository(Service $service): ?Repository;
|
||||
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
abstract protected function getDestinationRepository(Service $service): ?Repository;
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Billing\Transaction;
|
||||
|
||||
use App\Concerns\Repositories\Billing\ReconcilesTransactionRepositories;
|
||||
use App\Console\Commands\Billing\ServiceReconcileCommand;
|
||||
use App\Contracts\Repositories\Repository;
|
||||
use App\Enums\Models\Billing\Service;
|
||||
use App\Models\Billing\Transaction;
|
||||
use App\Repositories\Eloquent\Billing\DigitalOceanTransactionRepository as DigitalOceanDestinationRepository;
|
||||
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Print the result to console and log the results to the app log.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function postReconciliationTask(): void
|
||||
{
|
||||
if ($this->hasResults()) {
|
||||
if ($this->hasChanges()) {
|
||||
Log::info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
|
||||
$this->info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
|
||||
}
|
||||
if ($this->hasFailures()) {
|
||||
Log::error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
|
||||
$this->error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
|
||||
}
|
||||
} else {
|
||||
Log::info('No Transactions created or deleted or updated');
|
||||
$this->info('No Transactions created or deleted or updated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction creation.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCreated(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' created");
|
||||
$this->info("Transaction '{$model->getName()}' created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction creation.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedCreation(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not created");
|
||||
$this->error("Transaction '{$model->getName()}' was not created");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction deletion.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleDeleted(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' deleted");
|
||||
$this->info("Transaction '{$model->getName()}' deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction deletion.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedDeletion(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not deleted");
|
||||
$this->error("Transaction '{$model->getName()}' was not deleted");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for successful transaction update.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUpdated(Transaction $model): void
|
||||
{
|
||||
Log::info("Transaction '{$model->getName()}' updated");
|
||||
$this->info("Transaction '{$model->getName()}' updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for failed transaction update.
|
||||
*
|
||||
* @param Transaction $model
|
||||
* @return void
|
||||
*/
|
||||
protected function handleFailedUpdate(Transaction $model): void
|
||||
{
|
||||
Log::error("Transaction '{$model->getName()}' was not updated");
|
||||
$this->error("Transaction '{$model->getName()}' was not updated");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for exception.
|
||||
*
|
||||
* @param Exception $exception
|
||||
* @return void
|
||||
*/
|
||||
protected function handleException(Exception $exception): void
|
||||
{
|
||||
Log::error($exception->getMessage());
|
||||
$this->error($exception->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
protected function getSourceRepository(Service $service): ?Repository
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get destination repository for service.
|
||||
*
|
||||
* @param Service $service
|
||||
* @return Repository|null
|
||||
*/
|
||||
protected function getDestinationRepository(Service $service): ?Repository
|
||||
{
|
||||
return match ($service->value) {
|
||||
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Database\SQLiteConnection;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use PDO;
|
||||
use Spatie\DbDumper\Databases\MySql;
|
||||
use Spatie\DbDumper\Databases\PostgreSql;
|
||||
use Spatie\DbDumper\Databases\Sqlite;
|
||||
use Spatie\DbDumper\DbDumper;
|
||||
|
||||
/**
|
||||
* Class DatabaseDumpCommand.
|
||||
*/
|
||||
abstract class DatabaseDumpCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$create = $this->option('create');
|
||||
|
||||
try {
|
||||
$connection = DB::connection();
|
||||
|
||||
$dumper = $this->getDumper($connection, $create);
|
||||
if ($dumper === null) {
|
||||
Log::error("Unrecognized connection '{$connection->getName()}'");
|
||||
$this->error("Unrecognized connection '{$connection->getName()}'");
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! static::canIncludeTables($connection)) {
|
||||
Log::error('DB connection does not support includeTables option');
|
||||
$this->error('DB connection does not support includeTables option');
|
||||
|
||||
return 1;
|
||||
}
|
||||
$dumper->includeTables($this->allowedTables());
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName())
|
||||
->setUserName(strval($connection->getConfig('username')))
|
||||
->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$host = $connection->getConfig('host');
|
||||
if ($host !== null) {
|
||||
$dumper->setHost(collect($host)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_int($port)) {
|
||||
$dumper->setPort($port);
|
||||
}
|
||||
|
||||
$dumpFile = $this->getDumpFile($create);
|
||||
|
||||
$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());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dumper for the database connection.
|
||||
*
|
||||
* @param ConnectionInterface $connection
|
||||
* @param bool $create
|
||||
* @return DbDumper|null
|
||||
*/
|
||||
protected function getDumper(ConnectionInterface $connection, bool $create): ?DbDumper
|
||||
{
|
||||
return match (get_class($connection)) {
|
||||
SQLiteConnection::class => Sqlite::create(),
|
||||
MySqlConnection::class => $create ? MySql::create() : MySql::create()->doNotCreateTables(),
|
||||
PostgresConnection::class => $create ? PostgreSql::create() : PostgreSql::create()->doNotCreateTables(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the database connection supports table inclusion.
|
||||
*
|
||||
* @param ConnectionInterface $connection
|
||||
* @return bool
|
||||
*/
|
||||
public static function canIncludeTables(ConnectionInterface $connection): bool
|
||||
{
|
||||
// Sqlite version 3.32.0 is required when using the includeTables option
|
||||
if ($connection instanceof SQLiteConnection) {
|
||||
return version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '>=');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The target path for the database dump.
|
||||
* Pattern: "/path/to/project/storage/db-dumps/{path}/animethemes-db-dump-{create?}-{year}-{month}-{day}.sql".
|
||||
*
|
||||
* @param bool $create
|
||||
* @return string
|
||||
*/
|
||||
public function getDumpFile(bool $create = false): string
|
||||
{
|
||||
$filesystem = Storage::disk('db-dumps');
|
||||
|
||||
$filesystem->makeDirectory($this->getDumpFilePath());
|
||||
|
||||
return Str::of($filesystem->path($this->getDumpFilePath()))
|
||||
->append(DIRECTORY_SEPARATOR)
|
||||
->append('animethemes-db-dump-')
|
||||
->append($create ? 'create-' : '')
|
||||
->append(Date::now()->toDateString())
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory that the file should be dumped to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getDumpFilePath(): string;
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function allowedTables(): array;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Document;
|
||||
|
||||
use App\Console\Commands\DatabaseDumpCommand;
|
||||
use App\Models\Document\Page;
|
||||
|
||||
/**
|
||||
* Class DocumentDatabaseDumpCommand.
|
||||
*/
|
||||
class DocumentDatabaseDumpCommand extends DatabaseDumpCommand
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:dump-document {--C|create : Whether the dumper should include create table statements}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Produces sanitized database dump, targeting document-related tables for seeding purposes';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
Page::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The directory that the file should be dumped to.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getDumpFilePath(): string
|
||||
{
|
||||
return 'document';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands\Repositories\Storage;
|
||||
|
||||
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 implements InteractsWithDisk
|
||||
{
|
||||
/**
|
||||
* 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 {
|
||||
parent::handleFilters($sourceRepository, $destinationRepository, $data);
|
||||
|
||||
$path = Arr::get($data, 'path');
|
||||
if ($path !== null) {
|
||||
$sourceRepository->handleFilter('path', $path);
|
||||
$destinationRepository->handleFilter('path', $path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validator for options.
|
||||
*
|
||||
* @return Validator
|
||||
*/
|
||||
protected function validator(): Validator
|
||||
{
|
||||
$fs = Storage::disk($this->disk());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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\DumpDocumentAction;
|
||||
|
||||
/**
|
||||
* Class DocumentDumpCommand.
|
||||
*/
|
||||
class DocumentDumpCommand extends DumpCommand
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'db:dump-document
|
||||
{--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 document-related tables for seeding purposes';
|
||||
|
||||
/**
|
||||
* Get the underlying action.
|
||||
*
|
||||
* @return DumpAction
|
||||
*/
|
||||
protected function action(): DumpAction
|
||||
{
|
||||
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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user