9
votes

As you may know Laravel uses Flysystem PHP package to provide filesystem abstraction. Ive started to use this feature in my project, just for fun uploaded some images to my Amazon s3 bucket, I have also instance of Cloudfront linked to this bucket. My problem is when im trying to display those images in my html page, i need a url.

I could not find any "clean" way to do it, as flysystem is generic library i throught that i will be able to do something like this:

Storage::disk('my_awesome_disk')->publicUrl("{$path}/{$image->name}")

for 'public' files and its easy to determine if file is "public" or not because its included in their api, so if im using s3 bucket as my driver for the disk i should get: "https://s3.amazonaws.com/my_awesome_bucket/path/image.png"

or alternatively:

Storage::disk('my_awesome_disk')->signedUrl("{$path}/{$image->name}", $timeout)

for 'private' files - i should get a temporary url that will expire after some time.

Is this something i can achieve only with specific implementation? for example if im using Amazon S3 i can easily run:

$signed_url = $s3Client->getObjectUrl($bucket_name,
                        $resource_key,
                        "+{$expires} minutes");

But i dont want to do ugly "switch cases" to determine what driver im using. And how can i get a url from a cdn (like cloudfront) through the FileSystem interface? Any suggestion?

1
did you manage to do this yet?Christopher Francisco
Im working on it, i think i will have to write a library that will handle it, extend the current api using decorator design pattern. Still have dificulties to think about the system design.Lihai

1 Answers

3
votes

I think of Flysystem as an interface to a disk (or other storage mechanism) and nothing more. Just as I would not ask my local filesystem to calculate a public URI, I would not ask Flysystem to do it either.

I create objects that correspond to the files that I save via Flysystem. Depending on my needs, I might save the public URI directly in the database record, or I might create a custom getter that builds the public URI based on runtime circumstances.

With Flysystem, I know the path to the file when I write the file. In order to keep track of these files I'll typically create an object that represents a saved file:

class SavedFile extends Model
{
    protected $fillable = [
        'disk',
        'path',
        'publicURI',
    ];
}

When I save the file, I create a record of it in the database:

$disk     = 'local_example';
$path     = '/path/to/file.txt';
$contents = 'lorem ipsum';
$host     = 'https://example.com/path/to/storage/root/';

if (Store::disk($disk)->put($path, $contents)) {
    SavedFile::create([
        'disk'      => $disk,
        'path'      => $path,
        'publicURI' => $host . $path,
    ]);
}

Whenever I need the public URI, I can just grab it off the SavedFile model. This is handy for trivial applications, but it breaks down if I ever need to switch storage providers.

Leverage Inheritance

Another neat trick is to define a method that will resolve the public URI based on a variable defined in the child of an abstract SavedFile model. That way I'm not hard-coding the URI in the database, and I can create new classes for other storage services with just a couple of variable definitions:

abstract class SavedFile extends Model
{
    protected $table = 'saved_files';

    protected $disk;

    protected $host;

    protected $fillable = [
        'path',
    ];

    public function getPublicURIAttribute()
    {
        return $this->host . $this->attributes['path'];
    }
}

class LocalSavedFile extends SavedFile
{
    $disk = 'local_example';
    $host = 'https://example.com/path/to/storage/root';
}

class AwsSavedFile extends SavedFile
{
    $disk = 'aws_example';
    $host = 'https://s3.amazonaws.com/my_awesome_bucket';
}

Now if I have a bunch of files stored on my local filesystem and one day I move them to Amazon S3, I can simply copy the files and swap out the dependencies in my IoC binding definitions and I'm done. No need to do a time consuming and potentially hazardous find-and-replace on a massive database table, since the calculation of the URI is done by the model:

$localFile = LocalSavedFile::create([
    'path' => '/path/to/file.txt'
]);
echo $localFile->publicURI; // https://example.com/path/to/storage/root/path/to/file.txt

$awsFile = AwsSavedFile::find($localFile->id);
echo $awsFile->publicURI; // https://s3.amazonaws.com/my_awesome_bucket/path/to/file.txt

Edit:

Support for Public and Private Files

Just add a flag to the object:

abstract class SavedFile extends Model
{
    protected $table = 'saved_files';

    protected $disk;

    protected $host;

    protected $fillable = [
        'path',
        'public',
    ];

    public function getPublicURIAttribute()
    {
        if ($this->attributes['public'] === false) {
            // you could throw an exception or return a default image if you prefer
            return false;
        }

        return $this->host . $this->attributes['path'];
    }
}

class LocalSavedFile extends SavedFile
{
    $disk = 'local_example';
    $host = 'https://example.com/path/to/storage/root';
}

$localFile = LocalSavedFile::create([
    'path'   => '/path/to/file.txt',
    'public' => false,
]);
var_dump($localFile->publicURI); // bool(false)