[s3] extension for adding/deleting images on an S3 compatible host

This commit is contained in:
Shish 2023-12-21 15:51:27 +00:00
parent a491d70b0e
commit 8015d69acf
5 changed files with 1084 additions and 689 deletions

View file

@ -56,7 +56,8 @@
"psr/simple-cache" : "^1.0", "psr/simple-cache" : "^1.0",
"sabre/cache" : "^2.0.1", "sabre/cache" : "^2.0.1",
"naroga/redis-cache": "dev-master", "naroga/redis-cache": "dev-master",
"tbela99/css": "dev-master" "tbela99/css": "dev-master",
"aws/aws-sdk-php": "^3.294"
}, },
"require-dev" : { "require-dev" : {

1592
composer.lock generated

File diff suppressed because it is too large Load diff

13
ext/s3/config.php Normal file
View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
abstract class S3Config
{
public const ACCESS_KEY_ID = 's3_access_key_id';
public const ACCESS_KEY_SECRET = 's3_access_key_secret';
public const ENDPOINT = 's3_endpoint';
public const IMAGE_BUCKET = 's3_image_bucket';
}

17
ext/s3/info.php Normal file
View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
class S3Info extends ExtensionInfo
{
public const KEY = "s3";
public string $key = self::KEY;
public string $name = "S3 CDN Backend";
public string $url = self::SHIMMIE_URL;
public array $authors = [self::SHISH_NAME => self::SHISH_EMAIL];
public string $license = self::LICENSE_GPLV2;
public string $description = "Push post updates to S3";
}

148
ext/s3/main.php Normal file
View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Shimmie2;
require_once "config.php";
class S3 extends Extension
{
public static array $synced = [];
public function onSetupBuilding(SetupBuildingEvent $event)
{
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: ");
}
public function onCommand(CommandEvent $event)
{
if ($event->cmd == "help") {
print "\ts3-sync <post id>\n";
print "\t\tsync a post to s3\n\n";
print "\ts3-rm <hash>\n";
print "\t\tdelete a leftover file from s3\n\n";
}
if ($event->cmd == "s3-sync") {
if (preg_match('/^(\d+)-(\d+)$/', $event->args[0], $matches)) {
$start = (int)$matches[1];
$end = (int)$matches[2];
} else {
$start = (int)$event->args[0];
$end = $start;
}
foreach(Search::find_images_iterable(tags: ["order=id", "id>=$start", "id<=$end"]) as $image) {
print("{$image->id}: {$image->hash}\n");
ob_flush();
$this->sync_post($image);
}
}
if ($event->cmd == "s3-rm") {
foreach($event->args as $hash) {
print("{$hash}\n");
ob_flush();
$this->remove_file($hash);
}
}
}
public function onImageAddition(ImageAdditionEvent $event)
{
$this->sync_post($event->image);
}
public function onTagSet(TagSetEvent $event)
{
// pretend that tags were set already so that sync works
$orig_tags = $event->image->tag_array;
$event->image->tag_array = $event->tags;
$this->sync_post($event->image);
$event->image->tag_array = $orig_tags;
}
public function onImageDeletion(ImageDeletionEvent $event)
{
$this->remove_file($event->image->hash);
}
public function onImageReplace(ImageReplaceEvent $event)
{
$existing = Image::by_id($event->id);
$this->remove_file($existing->hash);
$this->sync_post($event->image);
}
// utils
private function get_client()
{
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,
]);
}
private function hash_to_path(string $hash)
{
$ha = substr($hash, 0, 2);
$sh = substr($hash, 2, 2);
return "$ha/$sh/$hash";
}
// underlying s3 interaction functions
private function sync_post(Image $image)
{
global $config;
// multiple events can trigger a sync,
// let's only do one per request
if(in_array($image->id, self::$synced)) {
return;
}
self::$synced[] = $image->id;
$client = $this->get_client();
if(is_null($client)) {
return;
}
$image_bucket = $config->get_string(S3Config::IMAGE_BUCKET);
$friendly = $image->parse_link_template('$id - $tags.$ext');
$client->putObject([
'Bucket' => $image_bucket,
'Key' => $this->hash_to_path($image->hash),
'Body' => file_get_contents($image->get_image_filename()),
'ACL' => 'public-read',
'ContentType' => $image->get_mime(),
'ContentDisposition' => "inline; filename=\"$friendly\"",
]);
}
private function remove_file(string $hash)
{
global $config;
$client = $this->get_client();
if(is_null($client)) {
return;
}
$image_bucket = $config->get_string(S3Config::IMAGE_BUCKET);
$client->deleteObject([
'Bucket' => $image_bucket,
'Key' => $this->hash_to_path($hash),
]);
}
}