mirror of
https://github.com/bitinflow/bunny-cli.git
synced 2026-03-13 13:45:54 +00:00
first commit
This commit is contained in:
40
app/Bunny/Client.php
Normal file
40
app/Bunny/Client.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny;
|
||||
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class Client
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new \GuzzleHttp\Client([
|
||||
'base_uri' => 'https://api.bunny.net/',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getPullZone(int $pullZoneId): Result
|
||||
{
|
||||
return $this->request('GET', "pullzone/{$pullZoneId}", [
|
||||
RequestOptions::HEADERS => [
|
||||
'AccessKey' => config('bunny.api.access_key'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function purgeCache(int $pullZoneId): Result
|
||||
{
|
||||
return $this->request('POST', "pullzone/{$pullZoneId}/purgeCache", [
|
||||
RequestOptions::HEADERS => [
|
||||
'AccessKey' => config('bunny.api.access_key'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function request(string $method, string $uri, array $options): Result
|
||||
{
|
||||
$response = $this->client->request($method, $uri, $options);
|
||||
|
||||
return new Result($response);
|
||||
}
|
||||
}
|
||||
31
app/Bunny/Filesystem/EdgeFile.php
Normal file
31
app/Bunny/Filesystem/EdgeFile.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use stdClass;
|
||||
|
||||
class EdgeFile implements File
|
||||
{
|
||||
private stdClass $file;
|
||||
|
||||
public function __construct(stdClass $file)
|
||||
{
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
public function getFilename($search = '', $replace = ''): string
|
||||
{
|
||||
return Str::replaceFirst($search, $replace, $this->file->Path . $this->file->ObjectName);
|
||||
}
|
||||
|
||||
public function isDirectory(): bool
|
||||
{
|
||||
return $this->file->IsDirectory;
|
||||
}
|
||||
|
||||
public function getChecksum(): string
|
||||
{
|
||||
return $this->file->Checksum;
|
||||
}
|
||||
}
|
||||
94
app/Bunny/Filesystem/EdgeStorage.php
Normal file
94
app/Bunny/Filesystem/EdgeStorage.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class EdgeStorage
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => sprintf('https://%s', config('bunny.storage.hostname')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function allFiles(string $path, &$results = array())
|
||||
{
|
||||
$promise = $this->client->getAsync(self::normalizePath($path, true), [
|
||||
RequestOptions::HEADERS => [
|
||||
'AccessKey' => config('bunny.storage.password'),
|
||||
],
|
||||
]);
|
||||
|
||||
$promise->then(
|
||||
function (ResponseInterface $res) use (&$results) {
|
||||
$files = array_map(
|
||||
fn($file) => new EdgeFile($file),
|
||||
json_decode($res->getBody()->getContents(), false)
|
||||
);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$results[] = $file;
|
||||
if ($file->isDirectory()) {
|
||||
$this->allFiles($file->getFilename(), $results);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
function (RequestException $e) {
|
||||
echo $e->getMessage() . "\n";
|
||||
echo $e->getRequest()->getMethod();
|
||||
}
|
||||
);
|
||||
|
||||
$promise->wait();
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function put(LocalFile $file, string $local, string $edge): PromiseInterface
|
||||
{
|
||||
return $this->client->putAsync(self::normalizePath($file->getFilename($local, $edge), $file->isDirectory()), [
|
||||
RequestOptions::HEADERS => [
|
||||
'AccessKey' => config('bunny.storage.password'),
|
||||
'Checksum' => $file->getChecksum(),
|
||||
],
|
||||
RequestOptions::BODY => fopen($file->getFilename(), 'r'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(EdgeFile $file): PromiseInterface
|
||||
{
|
||||
return $this->client->deleteAsync(self::normalizePath($file->getFilename(), $file->isDirectory()), [
|
||||
RequestOptions::HEADERS => [
|
||||
'AccessKey' => config('bunny.storage.password'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getClient(): Client
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
private static function normalizePath(string $filename, bool $isDirectory): string
|
||||
{
|
||||
if (!Str::startsWith($filename, ['/'])) {
|
||||
$filename = '/' . $filename;
|
||||
}
|
||||
|
||||
if ($isDirectory && !Str::endsWith($filename, ['/'])) {
|
||||
$filename = $filename . '/';
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
12
app/Bunny/Filesystem/File.php
Normal file
12
app/Bunny/Filesystem/File.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
interface File
|
||||
{
|
||||
public function getFilename($search = '', $replace = ''): string;
|
||||
|
||||
public function getChecksum(): string;
|
||||
|
||||
public function isDirectory(): bool;
|
||||
}
|
||||
141
app/Bunny/Filesystem/FileCompare.php
Normal file
141
app/Bunny/Filesystem/FileCompare.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
use App\Bunny\Client;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class FileCompare
|
||||
{
|
||||
private const RESERVED_FILENAMES = [];
|
||||
|
||||
private LocalStorage $localStorage;
|
||||
private EdgeStorage $edgeStorage;
|
||||
private Command $output;
|
||||
|
||||
public function __construct(LocalStorage $localStorage, EdgeStorage $edgeStorage, Command $output)
|
||||
{
|
||||
$this->localStorage = $localStorage;
|
||||
$this->edgeStorage = $edgeStorage;
|
||||
$this->apiClient = new Client();
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function compare(string $local, string $edge): void
|
||||
{
|
||||
$this->output->info('- Hashing files...');
|
||||
$localFiles = $this->localStorage->allFiles($local);
|
||||
$this->output->info(sprintf('✔ Finished hashing %s files', count($localFiles)));
|
||||
$this->output->info('- CDN diffing files...');
|
||||
$edgeFiles = $this->edgeStorage->allFiles($edge);
|
||||
|
||||
$requests = [];
|
||||
|
||||
/** @var LocalFile $localFile */
|
||||
foreach ($localFiles as $localFile) {
|
||||
if ($localFile->isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = $localFile->getFilename($local);
|
||||
if ($match = $this->contains($edgeFiles, $filename, $edge)) {
|
||||
if ($match->getChecksum() != $localFile->getChecksum()) {
|
||||
$requests[] = fn() => $this->edgeStorage->put($localFile, $local, $edge);
|
||||
}
|
||||
} else {
|
||||
$requests[] = fn() => $this->edgeStorage->put($localFile, $local, $edge);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var EdgeFile $edgeFile */
|
||||
foreach ($edgeFiles as $edgeFile) {
|
||||
$filename = $edgeFile->getFilename($edge);
|
||||
if (!$this->contains($localFiles, $filename, $local) && !$this->isReserved($filename)) {
|
||||
$requests[] = fn() => $this->edgeStorage->delete($edgeFile);
|
||||
}
|
||||
}
|
||||
|
||||
$this->output->info(sprintf('✔ CDN requesting %s files', $count = count($requests)));
|
||||
|
||||
if ($count > 0) {
|
||||
$this->output->info(sprintf('- Synchronizing %s files', $count));
|
||||
|
||||
$bar = $this->output->getOutput()->createProgressBar($count);
|
||||
|
||||
$pool = new Pool($this->edgeStorage->getClient(), $requests, [
|
||||
'concurrency' => 5,
|
||||
'fulfilled' => function (Response $response, $index) use ($bar) {
|
||||
$bar->advance();
|
||||
},
|
||||
'rejected' => function (RequestException $reason, $index) use ($bar) {
|
||||
$bar->advance();
|
||||
|
||||
if ($this->rejectedDue404Deletion($reason)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->output->warn(sprintf(
|
||||
'Request rejected by bunny.net. Status: %s, Message: %s',
|
||||
$reason->getResponse()->getStatusCode(),
|
||||
$reason->getMessage()
|
||||
));
|
||||
},
|
||||
]);
|
||||
|
||||
// Initiate the transfers and create a promise
|
||||
$promise = $pool->promise();
|
||||
|
||||
$bar->start();
|
||||
$promise->wait(); // Force the pool of requests to complete.
|
||||
$bar->finish();
|
||||
|
||||
$this->output->newLine();
|
||||
|
||||
$this->output->info(sprintf('✔ Finished synchronizing %s files', $count));
|
||||
}
|
||||
|
||||
$this->output->info('- Waiting for deploy to go live...');
|
||||
|
||||
$result = $this->apiClient->purgeCache($pullZoneId = config('bunny.pull_zone.id'));
|
||||
|
||||
if (!$result->success()) {
|
||||
$this->output->info('✔ Deploy is live (without flush)!');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->apiClient->getPullZone($pullZoneId);
|
||||
|
||||
$this->output->info('✔ Deploy is live!');
|
||||
$this->output->newLine();
|
||||
|
||||
foreach ($result->getData()->Hostnames as $hostname) {
|
||||
$schema = ($hostname->ForceSSL || $hostname->HasCertificate) ? 'https' : 'http';
|
||||
$this->output->info(sprintf('Website URL: %s://%s', $schema, $hostname->Value));
|
||||
}
|
||||
}
|
||||
|
||||
private function contains(array $files, string $filename, string $search): ?File
|
||||
{
|
||||
foreach ($files as $edgeFile) {
|
||||
if ($edgeFile->getFilename($search) === $filename) {
|
||||
return $edgeFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isReserved($filename): bool
|
||||
{
|
||||
return in_array($filename, self::RESERVED_FILENAMES);
|
||||
}
|
||||
|
||||
private function rejectedDue404Deletion(RequestException $reason): bool
|
||||
{
|
||||
return $reason->getRequest()->getMethod() === 'DELETE'
|
||||
&& in_array($reason->getResponse()->getStatusCode(), [404, 400, 500], true);
|
||||
}
|
||||
}
|
||||
32
app/Bunny/Filesystem/LocalFile.php
Normal file
32
app/Bunny/Filesystem/LocalFile.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class LocalFile implements File
|
||||
{
|
||||
private string $filename;
|
||||
private ?string $checksum;
|
||||
|
||||
public function __construct(string $filename, ?string $checksum)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->checksum = $checksum;
|
||||
}
|
||||
|
||||
public function getFilename($search = '', $replace = ''): string
|
||||
{
|
||||
return Str::replaceFirst($search, $replace, $this->filename);
|
||||
}
|
||||
|
||||
public function getChecksum(): string
|
||||
{
|
||||
return $this->checksum;
|
||||
}
|
||||
|
||||
public function isDirectory(): bool
|
||||
{
|
||||
return $this->checksum == null;
|
||||
}
|
||||
}
|
||||
23
app/Bunny/Filesystem/LocalStorage.php
Normal file
23
app/Bunny/Filesystem/LocalStorage.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bunny\Filesystem;
|
||||
|
||||
class LocalStorage
|
||||
{
|
||||
function allFiles($dir, &$results = array())
|
||||
{
|
||||
$files = scandir($dir);
|
||||
|
||||
foreach ($files as $value) {
|
||||
$path = realpath($dir . DIRECTORY_SEPARATOR . $value);
|
||||
if (!is_dir($path)) {
|
||||
$results[] = new LocalFile($path, strtoupper(hash_file('sha256', $path)));
|
||||
} else if ($value != "." && $value != "..") {
|
||||
$results[] = new LocalFile($path, null);
|
||||
$this->allFiles($path, $results);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
30
app/Bunny/Result.php
Normal file
30
app/Bunny/Result.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace App\Bunny;
|
||||
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class Result
|
||||
{
|
||||
private ResponseInterface $response;
|
||||
|
||||
private $data;
|
||||
|
||||
public function __construct(ResponseInterface $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->data = json_decode($this->response->getBody()->getContents(), false);
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function success(): bool
|
||||
{
|
||||
return in_array(floor($this->response->getStatusCode() / 100), [2]);
|
||||
}
|
||||
}
|
||||
0
app/Commands/.gitkeep
Normal file
0
app/Commands/.gitkeep
Normal file
64
app/Commands/DeployCommand.php
Normal file
64
app/Commands/DeployCommand.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Bunny\Filesystem\EdgeStorage;
|
||||
use App\Bunny\Filesystem\FileCompare;
|
||||
use App\Bunny\Filesystem\LocalStorage;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
|
||||
class DeployCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The signature of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'deploy
|
||||
{--dir=dist : Root directory to upload}';
|
||||
|
||||
/**
|
||||
* The description of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Deploy dist folder to edge storage';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$edgeStorage = new EdgeStorage();
|
||||
$localStorage = new LocalStorage();
|
||||
$fileCompare = new FileCompare($localStorage, $edgeStorage, $this);
|
||||
|
||||
$localPath = realpath($path = $this->option('dir') ?? 'dist');
|
||||
|
||||
if (!file_exists($localPath) || !is_dir($localPath)) {
|
||||
$this->warn(sprintf('The directory %s does not exists.', $path));
|
||||
return 1;
|
||||
}
|
||||
|
||||
$edgePath = sprintf('/%s', config('bunny.storage.username'));
|
||||
|
||||
$fileCompare->compare($localPath, $edgePath);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Define the command's schedule.
|
||||
*
|
||||
* @param Schedule $schedule
|
||||
* @return void
|
||||
*/
|
||||
public function schedule(Schedule $schedule): void
|
||||
{
|
||||
// $schedule->command(static::class)->everyMinute();
|
||||
}
|
||||
}
|
||||
33
app/Providers/AppServiceProvider.php
Normal file
33
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\FtpAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use League\Flysystem\Filesystem;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
Storage::extend('ftp', function ($app, $config) {
|
||||
return new Filesystem(new FtpAdapter($config));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user