Skip to content

Commit eed20aa

Browse files
authored
Merge pull request #242 from open-runtimes/fix-s3-bucket-prefix
Fix S3 bucket prefix omission for custom endpoints in StorageFactory
2 parents f04a458 + bf5f94e commit eed20aa

3 files changed

Lines changed: 97 additions & 3 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,15 @@ OPR_EXECUTOR_RETRY_ATTEMPTS=5
9292
OPR_EXECUTOR_RETRY_DELAY_MS=500
9393
```
9494

95-
> `OPR_EXECUTOR_CONNECTION_STORAGE` takes a DSN string that represents a connection to your storage device. If you would like to use your local filesystem, you can use `file://localhost`. If using S3 or any other provider for storage, use a DSN of the following format `s3://access_key:access_secret@host:port/bucket_name?region=us-east-1`
95+
> `OPR_EXECUTOR_CONNECTION_STORAGE` takes a DSN string that represents a connection to your storage device. A host is always required in the DSN, even when it is not used to reach the storage backend. For example:
96+
>
97+
> | Storage | DSN |
98+
> |---------|-----|
99+
> | Local filesystem | `file://localhost` |
100+
> | AWS S3 | `s3://access_key:access_secret@bucket_name.s3.us-east-1.amazonaws.com?region=us-east-1` |
101+
> | S3-compatible (MinIO, Garage, etc.) | `s3://access_key:access_secret@localhost/bucket_name?region=us-east-1&url=http%3A%2F%2Fminio%3A9000` |
102+
>
103+
> When a host is provided, the executor connects to it directly using the generic S3 device. For AWS S3, use the bucket's virtual-hosted-style endpoint as the host. For S3-compatible providers, pass the URL-encoded endpoint via the `url` parameter and use any placeholder host.
96104

97105
> For backwards compatibility, executor also supports `OPR_EXECUTOR_STORAGE_*` variables as replacement for `OPR_EXECUTOR_CONNECTION_STORAGE`, as seen in [Appwrite repository](https://github.com/appwrite/appwrite/blob/1.3.8/.env#L26-L46).
98106

‎src/Executor/StorageFactory.php‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,17 @@ public static function getDevice(string $root, ?string $connection = ''): Device
5454

5555
switch ($deviceType) {
5656
case Storage::DEVICE_S3:
57+
$bucketRoot = ($bucket === '' || $bucket === '0')
58+
? $root
59+
: \rtrim($bucket . '/' . \ltrim($root, '/'), '/');
60+
5761
if ($url !== '' && $url !== '0') {
58-
return new S3($root, $accessKey, $accessSecret, $url, $dsnRegion, $acl);
62+
return new S3($bucketRoot, $accessKey, $accessSecret, $url, $dsnRegion, $acl);
5963
}
6064

6165
if ($host !== '' && $host !== '0') {
6266
$host = $insecure ? 'http://' . $host : $host;
63-
return new S3(root: $root, accessKey: $accessKey, secretKey: $accessSecret, host: $host, region: $dsnRegion, acl: $acl);
67+
return new S3(root: $bucketRoot, accessKey: $accessKey, secretKey: $accessSecret, host: $host, region: $dsnRegion, acl: $acl);
6468
}
6569

6670
return new AWS(root: $root, accessKey: $accessKey, secretKey: $accessSecret, bucket: $bucket, region: $dsnRegion, acl: $acl);
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit\Executor;
6+
7+
use OpenRuntimes\Executor\StorageFactory;
8+
use PHPUnit\Framework\TestCase;
9+
use Utopia\Storage\Device\Local;
10+
use Utopia\Storage\Device\S3;
11+
12+
final class StorageFactoryTest extends TestCase
13+
{
14+
public function testEmptyConnectionReturnsLocalDevice(): void
15+
{
16+
$device = StorageFactory::getDevice('/storage/builds/app-test', '');
17+
18+
$this->assertInstanceOf(Local::class, $device);
19+
$this->assertSame('/storage/builds/app-test', $device->getRoot());
20+
}
21+
22+
public function testNullConnectionReturnsLocalDevice(): void
23+
{
24+
$device = StorageFactory::getDevice('/storage/builds/app-test', null);
25+
26+
$this->assertInstanceOf(Local::class, $device);
27+
}
28+
29+
public function testInvalidDsnFallsBackToLocalDevice(): void
30+
{
31+
// Missing host is invalid for a DSN
32+
$device = StorageFactory::getDevice('/storage/builds/app-test', 's3://accessKey:secret@/mybucket?region=garage');
33+
34+
$this->assertInstanceOf(Local::class, $device);
35+
}
36+
37+
public function testS3WithUrlPrependsBucketToRoot(): void
38+
{
39+
$dsn = 's3://accessKey:secret@localhost/mybucket?region=garage&url=' . \urlencode('http://127.0.0.1:3900');
40+
$device = StorageFactory::getDevice('/storage/builds/app-test', $dsn);
41+
42+
$this->assertSame(S3::class, $device::class);
43+
$this->assertSame('mybucket/storage/builds/app-test', $device->getRoot());
44+
$this->assertSame('mybucket/storage/builds/app-test/artifact.tar.gz', $device->getPath('artifact.tar.gz'));
45+
}
46+
47+
public function testS3WithUrlAndRootSlashUsesBucketAsRoot(): void
48+
{
49+
$dsn = 's3://accessKey:secret@localhost/mybucket?region=garage&url=' . \urlencode('http://127.0.0.1:3900');
50+
$device = StorageFactory::getDevice('/', $dsn);
51+
52+
$this->assertSame(S3::class, $device::class);
53+
$this->assertSame('mybucket', $device->getRoot());
54+
}
55+
56+
public function testS3WithUrlWithoutBucketKeepsRoot(): void
57+
{
58+
$dsn = 's3://accessKey:secret@localhost?region=garage&url=' . \urlencode('http://127.0.0.1:3900');
59+
$device = StorageFactory::getDevice('/storage/builds/app-test', $dsn);
60+
61+
$this->assertSame(S3::class, $device::class);
62+
$this->assertSame('/storage/builds/app-test', $device->getRoot());
63+
}
64+
65+
public function testS3WithHostPrependsBucketToRoot(): void
66+
{
67+
$dsn = 's3://accessKey:secret@minio/mybucket?region=us-east-1&insecure=true';
68+
$device = StorageFactory::getDevice('/storage/builds/app-test', $dsn);
69+
70+
$this->assertSame(S3::class, $device::class);
71+
$this->assertSame('mybucket/storage/builds/app-test', $device->getRoot());
72+
}
73+
74+
public function testS3WithHostWithoutBucketKeepsRoot(): void
75+
{
76+
$dsn = 's3://accessKey:secret@mybucket.s3.us-east-1.amazonaws.com?region=us-east-1';
77+
$device = StorageFactory::getDevice('/storage/builds/app-test', $dsn);
78+
79+
$this->assertSame(S3::class, $device::class);
80+
$this->assertSame('/storage/builds/app-test', $device->getRoot());
81+
}
82+
}

0 commit comments

Comments
 (0)