2023-12-21 15:51:27 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace Shimmie2;
|
|
|
|
|
2024-01-11 00:55:05 +00:00
|
|
|
use Symfony\Component\Console\Command\Command;
|
2024-01-16 01:25:47 +00:00
|
|
|
use Symfony\Component\Console\Input\{InputInterface,InputArgument,InputOption};
|
2024-01-11 00:55:05 +00:00
|
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
|
|
|
2024-01-06 22:26:22 +00:00
|
|
|
use function MicroHTML\INPUT;
|
|
|
|
|
2023-12-21 15:51:27 +00:00
|
|
|
require_once "config.php";
|
|
|
|
|
|
|
|
class S3 extends Extension
|
|
|
|
{
|
2024-01-10 18:41:06 +00:00
|
|
|
public int $synced = 0;
|
2023-12-21 15:51:27 +00:00
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onSetupBuilding(SetupBuildingEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
global $config;
|
|
|
|
|
|
|
|
$sb = $event->panel->create_new_block("S3 CDN");
|
|
|
|
$sb->add_text_option(S3Config::ACCESS_KEY_ID, "Access Key ID: ");
|
|
|
|
$sb->add_text_option(S3Config::ACCESS_KEY_SECRET, "<br>Access Key Secret: ");
|
|
|
|
$sb->add_text_option(S3Config::ENDPOINT, "<br>Endpoint: ");
|
|
|
|
$sb->add_text_option(S3Config::IMAGE_BUCKET, "<br>Image Bucket: ");
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onDatabaseUpgrade(DatabaseUpgradeEvent $event): void
|
2024-01-09 17:23:34 +00:00
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
|
|
|
|
if ($this->get_version("ext_s3_version") < 1) {
|
|
|
|
$database->create_table("s3_sync_queue", "
|
|
|
|
hash CHAR(32) NOT NULL PRIMARY KEY,
|
|
|
|
time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
|
|
action CHAR(1) NOT NULL DEFAULT 'S'
|
|
|
|
");
|
|
|
|
$this->set_version("ext_s3_version", 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-16 00:35:17 +00:00
|
|
|
public function onAdminBuilding(AdminBuildingEvent $event): void
|
|
|
|
{
|
|
|
|
global $database, $page;
|
|
|
|
$count = $database->get_one("SELECT COUNT(*) FROM s3_sync_queue");
|
|
|
|
$html = SHM_SIMPLE_FORM(
|
|
|
|
"admin/s3_process",
|
2024-01-16 00:44:17 +00:00
|
|
|
INPUT(["type" => 'number', "name" => 'count', 'value' => '10']),
|
|
|
|
SHM_SUBMIT("Sync N/$count posts"),
|
2024-01-16 00:35:17 +00:00
|
|
|
);
|
|
|
|
$page->add_block(new Block("Process S3 Queue", $html));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function onAdminAction(AdminActionEvent $event): void
|
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
if($event->action == "s3_process") {
|
2024-01-16 00:44:17 +00:00
|
|
|
foreach($database->get_all(
|
|
|
|
"SELECT * FROM s3_sync_queue ORDER BY time ASC LIMIT :count",
|
|
|
|
["count" => isset($_POST['count']) ? int_escape($_POST["count"]) : 10]
|
|
|
|
) as $row) {
|
2024-01-16 00:35:17 +00:00
|
|
|
if($row['action'] == "S") {
|
|
|
|
$image = Image::by_hash($row['hash']);
|
|
|
|
$this->sync_post($image);
|
|
|
|
} elseif($row['action'] == "D") {
|
|
|
|
$this->remove_file($row['hash']);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$event->redirect = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onCliGen(CliGenEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
2024-01-11 00:55:05 +00:00
|
|
|
$event->app->register('s3:process')
|
2024-01-16 01:25:47 +00:00
|
|
|
->addOption('count', 'c', InputOption::VALUE_REQUIRED, 'Number of items to process')
|
2024-01-11 00:55:05 +00:00
|
|
|
->setDescription('Process the S3 queue')
|
|
|
|
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
|
|
|
global $database;
|
2024-01-10 17:25:02 +00:00
|
|
|
$count = $database->get_one("SELECT COUNT(*) FROM s3_sync_queue");
|
2024-01-11 00:55:05 +00:00
|
|
|
$output->writeln("{$count} items in queue");
|
2024-01-16 01:25:47 +00:00
|
|
|
foreach($database->get_all(
|
|
|
|
"SELECT * FROM s3_sync_queue ORDER BY time ASC LIMIT :count",
|
|
|
|
["count" => $input->getOption('count') ?? $count]
|
|
|
|
) as $row) {
|
2024-01-09 17:23:34 +00:00
|
|
|
if($row['action'] == "S") {
|
|
|
|
$image = Image::by_hash($row['hash']);
|
2024-01-11 00:55:05 +00:00
|
|
|
$output->writeln("SYN {$row['hash']} ($image->id)");
|
2024-01-09 17:23:34 +00:00
|
|
|
$this->sync_post($image);
|
|
|
|
} elseif($row['action'] == "D") {
|
2024-01-11 00:55:05 +00:00
|
|
|
$output->writeln("DEL {$row['hash']}");
|
2024-01-09 17:23:34 +00:00
|
|
|
$this->remove_file($row['hash']);
|
|
|
|
} else {
|
2024-01-11 00:55:05 +00:00
|
|
|
$output->writeln("??? {$row['hash']} ({$row['action']})");
|
2024-01-09 17:23:34 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-11 00:55:05 +00:00
|
|
|
return Command::SUCCESS;
|
|
|
|
});
|
|
|
|
$event->app->register('s3:sync')
|
|
|
|
->addArgument('start', InputArgument::REQUIRED)
|
|
|
|
->addArgument('end', InputArgument::REQUIRED)
|
|
|
|
->setDescription('Sync a range of images to S3')
|
|
|
|
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
|
|
|
$start = (int)$input->getArgument('start');
|
|
|
|
$end = (int)$input->getArgument('end');
|
|
|
|
$output->writeln("Syncing range: $start - $end");
|
2024-01-09 17:23:34 +00:00
|
|
|
foreach(Search::find_images_iterable(tags: ["order=id", "id>=$start", "id<=$end"]) as $image) {
|
|
|
|
if($this->sync_post($image)) {
|
|
|
|
print("{$image->id}: {$image->hash}\n");
|
|
|
|
} else {
|
|
|
|
print("{$image->id}: {$image->hash} (skipped)\n");
|
|
|
|
}
|
2024-01-06 22:57:00 +00:00
|
|
|
}
|
2024-01-11 00:55:05 +00:00
|
|
|
return Command::SUCCESS;
|
|
|
|
});
|
|
|
|
$event->app->register('s3:rm')
|
|
|
|
->addArgument('hash', InputArgument::REQUIRED)
|
|
|
|
->setDescription('Delete a leftover file from S3')
|
|
|
|
->setCode(function (InputInterface $input, OutputInterface $output): int {
|
|
|
|
$hash = $input->getArgument('hash');
|
|
|
|
$output->writeln("Deleting file: '$hash'");
|
2023-12-21 15:51:27 +00:00
|
|
|
$this->remove_file($hash);
|
2024-01-11 00:55:05 +00:00
|
|
|
return Command::SUCCESS;
|
|
|
|
});
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onPageRequest(PageRequestEvent $event): void
|
2024-01-06 22:26:22 +00:00
|
|
|
{
|
|
|
|
global $config, $page, $user;
|
|
|
|
if ($event->page_matches("s3/sync")) {
|
|
|
|
if ($user->check_auth_token()) {
|
|
|
|
if ($user->can(Permissions::DELETE_IMAGE) && isset($_POST['image_id'])) {
|
|
|
|
$id = int_escape($_POST['image_id']);
|
|
|
|
if ($id > 0) {
|
|
|
|
$this->sync_post(Image::by_id($id));
|
|
|
|
log_info("s3", "Manual resync for >>$id", "File re-sync'ed");
|
|
|
|
$page->set_mode(PageMode::REDIRECT);
|
|
|
|
$page->set_redirect(make_link("post/view/$id"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onImageAdminBlockBuilding(ImageAdminBlockBuildingEvent $event): void
|
2024-01-06 22:26:22 +00:00
|
|
|
{
|
|
|
|
global $user;
|
|
|
|
if ($user->can(Permissions::DELETE_IMAGE)) {
|
|
|
|
$event->add_part(SHM_SIMPLE_FORM(
|
|
|
|
"s3/sync",
|
|
|
|
INPUT(["type" => 'hidden', "name" => 'image_id', "value" => $event->image->id]),
|
|
|
|
INPUT(["type" => 'submit', "value" => 'CDN Re-Sync']),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onImageAddition(ImageAdditionEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
2024-01-05 13:53:21 +00:00
|
|
|
// Tags aren't set at this point, let's wait for the TagSetEvent
|
|
|
|
// $this->sync_post($event->image);
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onTagSet(TagSetEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
2024-01-09 15:27:02 +00:00
|
|
|
$this->sync_post($event->image, $event->new_tags);
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onImageDeletion(ImageDeletionEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
$this->remove_file($event->image->hash);
|
|
|
|
}
|
|
|
|
|
2024-01-15 11:52:35 +00:00
|
|
|
public function onImageReplace(ImageReplaceEvent $event): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
2024-01-09 15:27:02 +00:00
|
|
|
$this->remove_file($event->old_hash);
|
2024-01-09 02:33:14 +00:00
|
|
|
$this->sync_post($event->image);
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// utils
|
2024-01-20 14:10:59 +00:00
|
|
|
private function get_client(): ?\Aws\S3\S3Client
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
global $config;
|
|
|
|
$access_key_id = $config->get_string(S3Config::ACCESS_KEY_ID);
|
|
|
|
$access_key_secret = $config->get_string(S3Config::ACCESS_KEY_SECRET);
|
|
|
|
if(is_null($access_key_id) || is_null($access_key_secret)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$endpoint = $config->get_string(S3Config::ENDPOINT);
|
|
|
|
$credentials = new \Aws\Credentials\Credentials($access_key_id, $access_key_secret);
|
|
|
|
return new \Aws\S3\S3Client([
|
|
|
|
'region' => 'auto',
|
|
|
|
'endpoint' => $endpoint,
|
|
|
|
'version' => 'latest',
|
|
|
|
'credentials' => $credentials,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
private function hash_to_path(string $hash): string
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
$ha = substr($hash, 0, 2);
|
|
|
|
$sh = substr($hash, 2, 2);
|
|
|
|
return "$ha/$sh/$hash";
|
|
|
|
}
|
|
|
|
|
2024-01-09 17:23:34 +00:00
|
|
|
private function is_busy(): bool
|
|
|
|
{
|
|
|
|
global $config;
|
|
|
|
$this->synced++;
|
|
|
|
if(PHP_SAPI == "cli") {
|
|
|
|
return false; // CLI can go on for as long as it wants
|
|
|
|
}
|
|
|
|
return $this->synced > $config->get_int(UploadConfig::COUNT);
|
|
|
|
}
|
|
|
|
|
2023-12-21 15:51:27 +00:00
|
|
|
// underlying s3 interaction functions
|
2024-01-20 14:10:59 +00:00
|
|
|
/**
|
|
|
|
* @param string[]|null $new_tags
|
|
|
|
*/
|
2024-01-06 22:57:00 +00:00
|
|
|
private function sync_post(Image $image, ?array $new_tags = null, bool $overwrite = true): bool
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
global $config;
|
|
|
|
|
|
|
|
$client = $this->get_client();
|
|
|
|
if(is_null($client)) {
|
2024-01-06 22:57:00 +00:00
|
|
|
return false;
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
$image_bucket = $config->get_string(S3Config::IMAGE_BUCKET);
|
2024-01-05 13:53:21 +00:00
|
|
|
|
2024-01-06 22:57:00 +00:00
|
|
|
$key = $this->hash_to_path($image->hash);
|
|
|
|
if(!$overwrite && $client->doesObjectExist($image_bucket, $key)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-01-09 17:23:34 +00:00
|
|
|
if($this->is_busy()) {
|
|
|
|
$this->enqueue($image->hash, "S");
|
|
|
|
} else {
|
|
|
|
if(is_null($new_tags)) {
|
|
|
|
$friendly = $image->parse_link_template('$id - $tags.$ext');
|
|
|
|
} else {
|
|
|
|
$_orig_tags = $image->get_tag_array();
|
|
|
|
$image->tag_array = $new_tags;
|
|
|
|
$friendly = $image->parse_link_template('$id - $tags.$ext');
|
|
|
|
$image->tag_array = $_orig_tags;
|
|
|
|
}
|
|
|
|
$client->putObject([
|
|
|
|
'Bucket' => $image_bucket,
|
|
|
|
'Key' => $key,
|
|
|
|
'Body' => file_get_contents($image->get_image_filename()),
|
|
|
|
'ACL' => 'public-read',
|
|
|
|
'ContentType' => $image->get_mime(),
|
|
|
|
'ContentDisposition' => "inline; filename=\"$friendly\"",
|
|
|
|
]);
|
|
|
|
$this->dequeue($image->hash);
|
|
|
|
}
|
2024-01-06 22:57:00 +00:00
|
|
|
return true;
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
private function remove_file(string $hash): void
|
2023-12-21 15:51:27 +00:00
|
|
|
{
|
|
|
|
global $config;
|
|
|
|
$client = $this->get_client();
|
|
|
|
if(is_null($client)) {
|
|
|
|
return;
|
|
|
|
}
|
2024-01-09 17:23:34 +00:00
|
|
|
if($this->is_busy()) {
|
|
|
|
$this->enqueue($hash, "D");
|
|
|
|
} else {
|
|
|
|
$client->deleteObject([
|
|
|
|
'Bucket' => $config->get_string(S3Config::IMAGE_BUCKET),
|
|
|
|
'Key' => $this->hash_to_path($hash),
|
|
|
|
]);
|
|
|
|
$this->dequeue($hash);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
private function enqueue(string $hash, string $action): void
|
2024-01-09 17:23:34 +00:00
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
$database->execute("DELETE FROM s3_sync_queue WHERE hash = :hash", ["hash" => $hash]);
|
|
|
|
$database->execute("
|
|
|
|
INSERT INTO s3_sync_queue (hash, action)
|
|
|
|
VALUES (:hash, :action)
|
|
|
|
", ["hash" => $hash, "action" => $action]);
|
|
|
|
}
|
|
|
|
|
2024-01-20 14:10:59 +00:00
|
|
|
private function dequeue(string $hash): void
|
2024-01-09 17:23:34 +00:00
|
|
|
{
|
|
|
|
global $database;
|
|
|
|
$database->execute("DELETE FROM s3_sync_queue WHERE hash = :hash", ["hash" => $hash]);
|
2023-12-21 15:51:27 +00:00
|
|
|
}
|
|
|
|
}
|