feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)

Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.

Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
  - Provider key: s3compatible
  - Reads user-configured endpoint URL from settings
  - Uses path-style URL access (required by most S3-compatible services)
  - Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
    AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
  - Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

Binary file not shown.

View File

@@ -0,0 +1,31 @@
probe {
type: EXTERNAL
name: "spanner"
interval_msec: 1800000
timeout_msec: 30000
targets { dummy_targets {} } # No targets for external probe
external_probe {
mode: ONCE
command: "php grpc_gpc_prober/prober.php --api=spanner"
}
}
probe {
type: EXTERNAL
name: "firestore"
interval_msec: 1800000
timeout_msec: 30000
targets { dummy_targets {} } # No targets for external probe
external_probe {
mode: ONCE
command: "php grpc_gpc_prober/prober.php --api=firestore"
}
}
surfacer {
type: STACKDRIVER
name: "stackdriver"
stackdriver_surfacer {
monitoring_url: "custom.googleapis.com/cloudprober/"
}
}

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
rm -rf google
for p in $(find ../third_party/googleapis/google -type f -name *.proto); do
protoc \
--proto_path=../third_party/googleapis \
--php_out=./ \
--grpc_out=./ \
--plugin=protoc-gen-grpc="$(which grpc_php_plugin)" \
"$p"
done

View File

@@ -0,0 +1,25 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp;
require '../vendor/autoload.php';
$_PARENT_RESOURCE = 'projects/grpc-prober-testing/databases/(default)/documents';
/*
Probes to test ListDocuments grpc call from Firestore stub.
Args:
stub: An object of FirestoreStub.
metrics: A dict of metrics.
*/
function document($client, &$metrics)
{
global $_PARENT_RESOURCE;
$list_document_request = new Google\Cloud\Firestore\V1beta1\ListDocumentsRequest();
$list_document_request->setParent($_PARENT_RESOURCE);
$time_start = microtime_float();
$client->ListDocuments($list_document_request);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['list_documents_latency_ms'] = $lantency;
}
$probFunctions = ['documents' => 'document'];
return $probFunctions;

View File

@@ -0,0 +1,85 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp;
\chdir(\dirname(__FILE__));
require '../vendor/autoload.php';
// require_once '../Google/Cloud/Firestore/V1beta1/FirestoreClient.php';
// require_once '../Google/Cloud/Spanner/V1/SpannerClient.php';
$firestore_probes = (require './firestore_probes.php');
$spanner_probes = (require './spanner_probes.php');
require './stackdriver_util.php';
$_OAUTH_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
$_FIRESTORE_TARGET = 'firestore.googleapis.com:443';
$_SPANNER_TARGET = 'spanner.googleapis.com:443';
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Auth\ApplicationDefaultCredentials;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Firestore\V1beta1\FirestoreGrpcClient;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\Spanner\V1\SpannerGrpcClient;
function getArgs()
{
$options = \getopt('', ['api:', 'extension:']);
return $options;
}
/*
function secureAuthorizedChannel($credentials, $request, $target, $kwargs){
$metadata_plugin = $transport_grpc->AuthMetadataPlugin($credentials, $request);
$ssl_credentials = Grpc\ChannelCredentials::createSsl();
$composit_credentials = $grpc->composite_channel_credentials($ssl_credentials, $google_auth_credentials);
return $grpc_gcp->secure_channel($target, $composit_credentials, $kwargs);
}
function getStubChannel($target){
$res = $auth->default([$_OAUTH_SCOPE]);
$cred = $res[0];
return secureAuthorizedChannel($cred, Request(), $target);
}*/
function executeProbes($api)
{
global $_OAUTH_SCOPE;
global $_SPANNER_TARGET;
global $_FIRESTORE_TARGET;
global $spanner_probes;
global $firestore_probes;
$util = new StackdriverUtil($api);
$auth = Google\Auth\ApplicationDefaultCredentials::getCredentials($_OAUTH_SCOPE);
$opts = ['credentials' => \Grpc\ChannelCredentials::createSsl(), 'update_metadata' => $auth->getUpdateMetadataFunc()];
if ($api == 'spanner') {
$client = new SpannerGrpcClient($_SPANNER_TARGET, $opts);
$probe_functions = $spanner_probes;
} else {
if ($api == 'firestore') {
$client = new FirestoreGrpcClient($_FIRESTORE_TARGET, $opts);
$probe_functions = $firestore_probes;
} else {
echo 'grpc not implemented for ' . $api;
exit(1);
}
}
$total = \sizeof($probe_functions);
$success = 0;
$metrics = [];
# Execute all probes for given api
foreach ($probe_functions as $probe_name => $probe_function) {
try {
$probe_function($client, $metrics);
$success++;
} catch (\Exception $e) {
$util->reportError($e);
}
}
if ($success == $total) {
$util->setSuccess(\DeliciousBrains\WP_Offload_Media\Gcp\True);
}
$util->addMetrics($metrics);
$util->outputMetrics();
if ($success != $total) {
# TODO: exit system
exit(1);
}
}
function main()
{
$args = getArgs();
executeProbes($args['api']);
}
main();

View File

@@ -0,0 +1,245 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp;
require '../vendor/autoload.php';
$_DATABASE = 'projects/grpc-prober-testing/instances/test-instance/databases/test-db';
$_TEST_USERNAME = 'test_username';
function hardAssert($value, $error_message)
{
if (!$value) {
echo $error_message . "\n";
exit(1);
}
}
function hardAssertIfStatusOk($status)
{
if ($status->code !== \Grpc\STATUS_OK) {
echo "Call did not complete successfully. Status object:\n";
\var_dump($status);
exit(1);
}
}
function microtime_float()
{
list($usec, $sec) = \explode(" ", \microtime());
return (float) $usec + (float) $sec;
}
/*
Probes to test session related grpc call from Spanner stub.
Includes tests against CreateSession, GetSession, ListSessions, and
DeleteSession of Spanner stub.
Args:
stub: An object of SpannerStub.
metrics: A list of metrics.
*/
function sessionManagement($client, &$metrics)
{
global $_DATABASE;
$createSessionRequest = new Google\Cloud\Spanner\V1\CreateSessionRequest();
$createSessionRequest->setDatabase($_DATABASE);
#Create Session test
#Create
$time_start = microtime_float();
list($session, $status) = $client->CreateSession($createSessionRequest)->wait();
hardAssertIfStatusOk($status);
hardAssert($session !== null, 'Call completed with a null response');
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['create_session_latency_ms'] = $lantency;
#Get Session
$getSessionRequest = new Google\Cloud\Spanner\V1\GetSessionRequest();
$getSessionRequest->setName($session->getName());
$time_start = microtime_float();
$response = $client->GetSession($getSessionRequest);
$response->wait();
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['get_session_latency_ms'] = $lantency;
#List session
$listSessionsRequest = new Google\Cloud\Spanner\V1\ListSessionsRequest();
$listSessionsRequest->setDatabase($_DATABASE);
$time_start = microtime_float();
$response = $client->ListSessions($listSessionsRequest);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['list_sessions_latency_ms'] = $lantency;
#Delete session
$deleteSessionRequest = new Google\Cloud\Spanner\V1\DeleteSessionRequest();
$deleteSessionRequest->setName($session->getName());
$time_start = microtime_float();
$client->deleteSession($deleteSessionRequest);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['delete_session_latency_ms'] = $lantency;
}
/*
Probes to test ExecuteSql and ExecuteStreamingSql call from Spanner stub.
Args:
stub: An object of SpannerStub.
metrics: A list of metrics.
*/
function executeSql($client, &$metrics)
{
global $_DATABASE;
$createSessionRequest = new Google\Cloud\Spanner\V1\CreateSessionRequest();
$createSessionRequest->setDatabase($_DATABASE);
list($session, $status) = $client->CreateSession($createSessionRequest)->wait();
hardAssertIfStatusOk($status);
hardAssert($session !== null, 'Call completed with a null response');
# Probing ExecuteSql call
$time_start = microtime_float();
$executeSqlRequest = new Google\Cloud\Spanner\V1\ExecuteSqlRequest();
$executeSqlRequest->setSession($session->getName());
$executeSqlRequest->setSql('select * FROM users');
$result_set = $client->ExecuteSql($executeSqlRequest);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['execute_sql_latency_ms'] = $lantency;
// TODO: Error check result_set
# Probing ExecuteStreamingSql call
$partial_result_set = $client->ExecuteStreamingSql($executeSqlRequest);
$time_start = microtime_float();
$first_result = \array_values($partial_result_set->getMetadata())[0];
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['execute_streaming_sql_latency_ms'] = $lantency;
// TODO: Error Check for sreaming sql first result
$deleteSessionRequest = new Google\Cloud\Spanner\V1\DeleteSessionRequest();
$deleteSessionRequest->setName($session->getName());
$client->deleteSession($deleteSessionRequest);
}
/*
Probe to test Read and StreamingRead grpc call from Spanner stub.
Args:
stub: An object of SpannerStub.
metrics: A list of metrics.
*/
function read($client, &$metrics)
{
global $_DATABASE;
$createSessionRequest = new Google\Cloud\Spanner\V1\CreateSessionRequest();
$createSessionRequest->setDatabase($_DATABASE);
list($session, $status) = $client->CreateSession($createSessionRequest)->wait();
hardAssertIfStatusOk($status);
hardAssert($session !== null, 'Call completed with a null response');
# Probing Read call
$time_start = microtime_float();
$readRequest = new Google\Cloud\Spanner\V1\ReadRequest();
$readRequest->setSession($session->getName());
$readRequest->setTable('users');
$readRequest->setColumns(['username', 'firstname', 'lastname']);
$keyset = new Google\Cloud\Spanner\V1\KeySet();
$keyset->setAll(\DeliciousBrains\WP_Offload_Media\Gcp\True);
$readRequest->setKeySet($keyset);
$result_set = $client->Read($readRequest);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['read_latency_ms'] = $lantency;
// TODO: Error Check for result_set
# Probing StreamingRead call
$partial_result_set = $client->StreamingRead($readRequest);
$time_start = microtime_float();
$first_result = \array_values($partial_result_set->getMetadata())[0];
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['streaming_read_latency_ms'] = $lantency;
//TODO: Error Check for streaming read first result
$deleteSessionRequest = new Google\Cloud\Spanner\V1\DeleteSessionRequest();
$deleteSessionRequest->setName($session->getName());
$client->deleteSession($deleteSessionRequest);
}
/*
Probe to test BeginTransaction, Commit and Rollback grpc from Spanner stub.
Args:
stub: An object of SpannerStub.
metrics: A list of metrics.
*/
function transaction($client, &$metrics)
{
global $_DATABASE;
$createSessionRequest = new Google\Cloud\Spanner\V1\CreateSessionRequest();
$createSessionRequest->setDatabase($_DATABASE);
list($session, $status) = $client->CreateSession($createSessionRequest)->wait();
hardAssertIfStatusOk($status);
hardAssert($session !== null, 'Call completed with a null response');
$txn_options = new Google\Cloud\Spanner\V1\TransactionOptions();
$rw = new Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite();
$txn_options->setReadWrite($rw);
$txn_request = new Google\Cloud\Spanner\V1\BeginTransactionRequest();
$txn_request->setSession($session->getName());
$txn_request->setOptions($txn_options);
# Probing BeginTransaction call
$time_start = microtime_float();
list($txn, $status) = $client->BeginTransaction($txn_request)->wait();
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['begin_transaction_latency_ms'] = $lantency;
hardAssertIfStatusOk($status);
hardAssert($txn !== null, 'Call completed with a null response');
# Probing Commit Call
$commit_request = new Google\Cloud\Spanner\V1\CommitRequest();
$commit_request->setSession($session->getName());
$commit_request->setTransactionId($txn->getId());
$time_start = microtime_float();
$client->Commit($commit_request);
$latency = (microtime_float() - $time_start) * 1000;
$metrics['commit_latency_ms'] = $lantency;
# Probing Rollback call
list($txn, $status) = $client->BeginTransaction($txn_request)->wait();
$rollback_request = new Google\Cloud\Spanner\V1\RollbackRequest();
$rollback_request->setSession($session->getName());
$rollback_request->setTransactionId($txn->getId());
hardAssertIfStatusOk($status);
hardAssert($txn !== null, 'Call completed with a null response');
$time_start = microtime_float();
$client->Rollback($rollback_request);
$latency = (microtime_float() - $time_start) * 1000;
$metrics['rollback_latency_ms'] = $latency;
$deleteSessionRequest = new Google\Cloud\Spanner\V1\DeleteSessionRequest();
$deleteSessionRequest->setName($session->getName());
$client->deleteSession($deleteSessionRequest);
}
/*
Probe to test PartitionQuery and PartitionRead grpc call from Spanner stub.
Args:
stub: An object of SpannerStub.
metrics: A list of metrics.
*/
function partition($client, &$metrics)
{
global $_DATABASE;
global $_TEST_USERNAME;
$createSessionRequest = new Google\Cloud\Spanner\V1\CreateSessionRequest();
$createSessionRequest->setDatabase($_DATABASE);
list($session, $status) = $client->CreateSession($createSessionRequest)->wait();
hardAssertIfStatusOk($status);
hardAssert($session !== null, 'Call completed with a null response');
$txn_options = new Google\Cloud\Spanner\V1\TransactionOptions();
$ro = new Google\Cloud\Spanner\V1\TransactionOptions\PBReadOnly();
$txn_options->setReadOnly($ro);
$txn_selector = new Google\Cloud\Spanner\V1\TransactionSelector();
$txn_selector->setBegin($txn_options);
#Probing PartitionQuery call
$ptn_query_request = new Google\Cloud\Spanner\V1\PartitionQueryRequest();
$ptn_query_request->setSession($session->getName());
$ptn_query_request->setSql('select * FROM users');
$ptn_query_request->setTransaction($txn_selector);
$time_start = microtime_float();
$client->PartitionQuery($ptn_query_request);
$lantency = (microtime_float() - $time_start) * 1000;
$metrics['partition_query_latency_ms'] = $lantency;
#Probing PartitionRead call
$ptn_read_request = new Google\Cloud\Spanner\V1\PartitionReadRequest();
$ptn_read_request->setSession($session->getName());
$ptn_read_request->setTable('users');
$ptn_read_request->setTransaction($txn_selector);
$keyset = new Google\Cloud\Spanner\V1\KeySet();
$keyset->setAll(\DeliciousBrains\WP_Offload_Media\Gcp\True);
$ptn_read_request->setKeySet($keyset);
$ptn_read_request->setColumns(['username', 'firstname', 'lastname']);
$time_start = microtime_float();
$client->PartitionRead($ptn_read_request);
$latency = (microtime_float() - $time_start) * 1000;
$metrics['partition_read_latency_ms'] = $latency;
# Delete Session
$deleteSessionRequest = new Google\Cloud\Spanner\V1\DeleteSessionRequest();
$deleteSessionRequest->setName($session->getName());
$client->deleteSession($deleteSessionRequest);
}
$PROBE_FUNCTIONS = ['session_management' => 'sessionManagement', 'execute_sql' => 'executeSql', 'read' => 'read', 'transaction' => 'transaction', 'partition' => 'partition'];
return $PROBE_FUNCTIONS;

View File

@@ -0,0 +1,58 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp;
require '../vendor/autoload.php';
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\ErrorReporting\V1beta1\ReportErrorsServiceClient;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\ErrorReporting\V1beta1\ErrorContext;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\ErrorReporting\V1beta1\ReportedErrorEvent;
use DeliciousBrains\WP_Offload_Media\Gcp\Google\Cloud\ErrorReporting\V1beta1\SourceLocation;
class StackdriverUtil
{
protected $api;
protected $metrics;
protected $success;
protected $err_client;
function __construct($api)
{
$this->api = $api;
$this->metrics = [];
$this->success = \FALSE;
$this->err_client = new ReportErrorsServiceClient();
}
function addMetric($key, $value)
{
$this->matrics[$key] = $value;
}
function addMetrics($metrics)
{
$this->metrics = \array_merge($metrics, $this->metrics);
}
function setSuccess($result)
{
$this->success = $result;
}
function outputMetrics()
{
if ($this->success) {
echo $this->api . '_success 1' . "\n";
} else {
echo $this->api . '_success 0' . "\n";
}
foreach ($this->metrics as $key => $value) {
echo $key . ' ' . $value . "\n";
}
}
function reportError($err)
{
\error_log($err);
$projectId = '434076015357';
$project_name = $this->err_client->projectName($projectId);
$location = (new SourceLocation())->setFunctionName($this->api);
$context = (new ErrorContext())->setReportLocation($location);
$error_event = new ReportedErrorEvent();
$error_event->setMessage('PHPProbeFailure: fails on ' . $this->api . ' API. Details: ' . (string) $err . "\n");
$error_event->setContext($context);
$this->err_client->reportErrorEvent($project_name, $error_event);
}
}