Skip to content

Commit 29f882e

Browse files
authored
adds fallback storage driver (i.e. layer s3 over rackspace files) (#5034)
* adds fallback storage driver (i.e. layer s3 over rackspace files) * fix linting * fix linting * fix test * fix v3 media encode * media: add fallback lookup for urlencoded names * fix media url access fallback * fix test * fix media url access fallback * fix media url access fallback
1 parent d2939b9 commit 29f882e

File tree

10 files changed

+378
-6
lines changed

10 files changed

+378
-6
lines changed

config/filesystems.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
'tenantid' => env('RS_TENANTID', '1'),
8282
],
8383

84+
'fallback' => [
85+
'driver' => 'fallback',
86+
'primary' => env('FALLBACK_PRIMARY', 's3'),
87+
'secondary' => env('FALLBACK_SECONDARY', 'rackspace'),
88+
],
89+
8490
],
8591

8692
];
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
namespace Ushahidi\Addons\FallbackStorage;
4+
5+
use League\Flysystem\Adapter\AbstractAdapter;
6+
use League\Flysystem\Adapter\Polyfill\NotSupportingVisibilityTrait;
7+
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
8+
9+
final class FallbackStorageAdapter extends AbstractAdapter
10+
{
11+
use StreamedCopyTrait;
12+
use NotSupportingVisibilityTrait;
13+
14+
private AbstractAdapter $primaryAdapter;
15+
private AbstractAdapter $secondaryAdapter;
16+
17+
public function __construct(
18+
AbstractAdapter $primaryAdapter,
19+
AbstractAdapter $secondaryAdapter
20+
) {
21+
$this->primaryAdapter = $primaryAdapter;
22+
$this->secondaryAdapter = $secondaryAdapter;
23+
}
24+
25+
public function write($path, $contents, $config = [])
26+
{
27+
// Only write to primaryAdapter, no fallback needed
28+
return $this->primaryAdapter->write($path, $contents, $config);
29+
}
30+
31+
public function writeStream($path, $resource, $config = [])
32+
{
33+
// Only write to primaryAdapter, no fallback needed
34+
return $this->primaryAdapter->writeStream($path, $resource, $config);
35+
}
36+
37+
public function update($path, $contents, $config = [])
38+
{
39+
// Try primaryAdapter, no fallback allowed
40+
return $this->primaryAdapter->update($path, $contents, $config);
41+
}
42+
43+
public function updateStream($path, $resource, $config = [])
44+
{
45+
// Try primaryAdapter, no fallback allowed
46+
return $this->primaryAdapter->updateStream($path, $resource, $config);
47+
}
48+
49+
public function rename($path, $newpath): bool
50+
{
51+
// Try primaryAdapter, no fallback allowed
52+
if ($this->primaryAdapter->has($path)) {
53+
if ($this->primaryAdapter->rename($path, $newpath)) {
54+
return true;
55+
}
56+
}
57+
return false;
58+
}
59+
60+
public function copy($path, $newpath)
61+
{
62+
// Try primaryAdapter if the path is there,
63+
// Otherwise check secondaryAdapter and stream copy from there to primaryAdapter
64+
if ($this->primaryAdapter->has($path)) {
65+
return $this->primaryAdapter->copy($path, $newpath);
66+
}
67+
if ($this->secondaryAdapter->has($path)) {
68+
$stream = $this->secondaryAdapter->readStream($path);
69+
if ($stream === false || !isset($stream['stream'])) {
70+
return false;
71+
}
72+
/* TODO: secondaryAdapter to primaryAdapter stream copy */
73+
// $result = $this->primaryAdapter->writeStream($newpath, $stream['stream']);
74+
// if (is_resource($stream['stream'])) {
75+
// fclose($stream['stream']);
76+
// }
77+
// return $result;
78+
return false;
79+
}
80+
// Not found in either, return false
81+
return false;
82+
}
83+
84+
public function delete($path)
85+
{
86+
// Only delete from primaryAdapter, no deletion on secondaryAdapter
87+
return $this->primaryAdapter->delete($path);
88+
}
89+
90+
public function deleteDir($dirname)
91+
{
92+
// Only delete from primaryAdapter, no deletion on secondaryAdapter
93+
return $this->primaryAdapter->deleteDir($dirname);
94+
}
95+
96+
public function createDir($dirname, $config = [])
97+
{
98+
// Only create in primaryAdapter, no creation in secondaryAdapter
99+
return $this->primaryAdapter->createDir($dirname, $config);
100+
}
101+
102+
public function has($path)
103+
{
104+
// If primaryAdapter knows about it, return true; otherwise check secondaryAdapter
105+
if ($this->primaryAdapter->has($path)) {
106+
return true;
107+
}
108+
return $this->secondaryAdapter->has($path);
109+
}
110+
111+
public function read($path)
112+
{
113+
$result = $this->primaryAdapter->read($path);
114+
if ($result === false) {
115+
$result = $this->secondaryAdapter->read($path);
116+
}
117+
return $result;
118+
}
119+
120+
public function readStream($path)
121+
{
122+
$result = $this->primaryAdapter->readStream($path);
123+
if ($result === false) {
124+
$result = $this->secondaryAdapter->readStream($path);
125+
}
126+
return $result;
127+
}
128+
129+
public function listContents($directory = '', $recursive = false)
130+
{
131+
$primaryAdapterList = $this->primaryAdapter->listContents($directory, $recursive);
132+
$secondaryAdapterList = $this->secondaryAdapter->listContents($directory, $recursive);
133+
134+
$combined = [];
135+
$seen = [];
136+
137+
foreach ((array) $primaryAdapterList as $entry) {
138+
if (isset($entry['path'])) {
139+
$seen[$entry['path']] = true;
140+
}
141+
$combined[] = $entry;
142+
}
143+
144+
foreach ((array) $secondaryAdapterList as $entry) {
145+
if (isset($entry['path']) && isset($seen[$entry['path']])) {
146+
continue;
147+
}
148+
if (isset($entry['path'])) {
149+
$seen[$entry['path']] = true;
150+
}
151+
$combined[] = $entry;
152+
}
153+
154+
return $combined;
155+
}
156+
157+
public function getMetadata($path)
158+
{
159+
$result = $this->primaryAdapter->getMetadata($path);
160+
if ($result === false) {
161+
$result = $this->secondaryAdapter->getMetadata($path);
162+
}
163+
return $result;
164+
}
165+
166+
public function getSize($path)
167+
{
168+
$result = $this->primaryAdapter->getSize($path);
169+
if ($result === false) {
170+
$result = $this->secondaryAdapter->getSize($path);
171+
}
172+
return $result;
173+
}
174+
175+
public function getMimetype($path)
176+
{
177+
$result = $this->primaryAdapter->getMimetype($path);
178+
if ($result === false) {
179+
$result = $this->secondaryAdapter->getMimetype($path);
180+
}
181+
return $result;
182+
}
183+
184+
public function getTimestamp($path)
185+
{
186+
$result = $this->primaryAdapter->getTimestamp($path);
187+
if ($result === false) {
188+
$result = $this->secondaryAdapter->getTimestamp($path);
189+
}
190+
return $result;
191+
}
192+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Ushahidi\Addons\FallbackStorage;
4+
5+
use CaptainHook\App\Runner\Files;
6+
use League\Flysystem\FilesystemInterface;
7+
use League\Flysystem\Filesystem;
8+
9+
use Illuminate\Filesystem\FilesystemAdapter;
10+
use Illuminate\Support\Facades\Log;
11+
12+
class FallbackStorageDriver extends Filesystem
13+
{
14+
15+
private Filesystem $primary;
16+
private Filesystem $secondary;
17+
18+
public function __construct(Filesystem $primary, Filesystem $secondary, array $config = [])
19+
{
20+
$this->primary = $primary;
21+
$this->secondary = $secondary;
22+
$adapter = new FallbackStorageAdapter($primary->getAdapter(), $secondary->getAdapter());
23+
24+
parent::__construct($adapter, $config);
25+
}
26+
27+
public function getUrl($path)
28+
{
29+
// Try primary first
30+
$url = $this->getUrlWrapper($this->primary, $path);
31+
Log::debug('FallbackStorageDriver: getUrl('.$path.') primary url='.($url ?: 'null'));
32+
if ($url) {
33+
return $url;
34+
}
35+
36+
// Fallback to secondary
37+
$url = $this->getUrlWrapper($this->secondary, $path);
38+
Log::debug('FallbackStorageDriver: getUrl('.$path.') secondary url='.($url ?: 'null'));
39+
return $url;
40+
}
41+
42+
private function getUrlWrapper($driver, $path)
43+
{
44+
if (!$driver->has($path)) {
45+
return null;
46+
}
47+
if (method_exists($driver, 'getUrl')) {
48+
return $driver->getUrl($path);
49+
}
50+
$fsAdapter = new FilesystemAdapter($driver);
51+
return $fsAdapter->url($path);
52+
}
53+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Ushahidi\Addons\FallbackStorage;
4+
5+
use Illuminate\Support\Facades\Storage;
6+
use Illuminate\Support\ServiceProvider;
7+
use League\Flysystem\Filesystem;
8+
9+
class FallbackStorageServiceProvider extends ServiceProvider
10+
{
11+
/**
12+
* Register any application services.
13+
*/
14+
public function register(): void
15+
{
16+
// ...
17+
}
18+
19+
/**
20+
* Bootstrap any application services.
21+
*/
22+
public function boot(): void
23+
{
24+
Storage::extend('fallback', function ($app, $config) {
25+
$primaryDisk = Storage::disk($config['primary']);
26+
$secondaryDisk = Storage::disk($config['secondary']);
27+
28+
$driver1 = $primaryDisk->getDriver();
29+
$driver2 = $secondaryDisk->getDriver();
30+
31+
# Ensure League\Flysystem\Filesystem instances
32+
if (!($driver1 instanceof Filesystem)) {
33+
throw new \InvalidArgumentException('Primary disk must be an instance of League\Flysystem\Filesystem (found '.get_class($driver1).')');
34+
}
35+
if (!($driver2 instanceof Filesystem)) {
36+
throw new \InvalidArgumentException('Secondary disk must be an instance of League\Flysystem\Filesystem (found '.get_class($driver2).')');
37+
}
38+
return new FallbackStorageDriver($driver1, $driver2, $config);
39+
});
40+
}
41+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "ushahidi/flysystem-fallback-storage",
3+
"description": "Fallback storage adapter for Flysystem package",
4+
"type": "library",
5+
"license": "AGPL-3.0",
6+
"require": {
7+
"php-opencloud/openstack": "^3.2"
8+
},
9+
"autoload": {
10+
"psr-4": {
11+
"Ushahidi\\Addons\\FallbackStorage\\": ""
12+
},
13+
"files": []
14+
},
15+
"extra": {
16+
"laravel": {
17+
"providers": [
18+
"Ushahidi\\Addons\\FallbackStorage\\FallbackStorageServiceProvider"
19+
]
20+
}
21+
},
22+
"suggest": {},
23+
"config": {
24+
"sort-packages": true
25+
},
26+
"minimum-stability": "dev"
27+
}

src/Ushahidi/Addons/Rackspace/RackspaceAdapter.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ public function setCdnContainer(CdnContainer $container)
351351
*/
352352
public function getUrl($path)
353353
{
354+
// Check if the container has the requested path
355+
if (!$this->has($path)) {
356+
return null;
357+
}
358+
354359
if ($this->cdnContainer === null) {
355360
return (string) $this->container->getObject($path)->getPublicUri();
356361
}

src/Ushahidi/Modules/V3/Formatter/Media.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected function formatOFilename($value)
5858
{
5959
// Removes path from image file name, encodes the filename, and joins the path and filename together
6060
$url_path = explode("/", $value);
61-
$filename = rawurlencode(array_pop($url_path));
61+
$filename = array_pop($url_path);
6262
array_push($url_path, $filename);
6363
$path = implode("/", $url_path);
6464

0 commit comments

Comments
 (0)