Laravel 5.1 Beauty - Upload Manager

Creating an upload manager and interfacing with Amazon S3

Posted on April 26, 2015 in L5 Beauty
Note: this is the ninth step in the tutorial.

In this chapter we’ll create an Upload Manager for the blog administration. First, the local file system will be used to store any uploaded files. Then, we’ll change the configuration to allow files to be stored on Amazon’s S3 cloud storage.

Chapter Contents

Configuring the File System

Let’s start with the configuration. Specifically, where will any files be stored? Create an upload directory within the project’s public directory. This way any files uploaded will be publicly accessible to the web.

Creating Upload Public Directory

vagrant@homestead:~/Code/l5beauty$ mkdir public/uploads

Easy. Now edit config/blog.php, to match what’s below.

Updated blog configuration

<?php
return [
  'title' => 'My Blog',
  'posts_per_page' => 5,
  'uploads' => [
    'storage' => 'local',
    'webpath' => '/uploads',
  ],
];

With the above changes now config('blog.uploads.storage') will define the file system and the config('blog.uploads.webpath') will be the root of our storage on the web.

Finally, edit the config/filesystems.php, changing the section as specified below.

Changes to config/filesystems.php

// Change the following lines
  'disks' => [
    'local' => [
      'driver' => 'local',
      'root'   => storage_path('app'),
    ],

    ...

    's3' => [
      'driver' => 's3',
      'key'    => 'your-key',
      'secret' => 'your-secret',
      'region' => 'your-region',
      'bucket' => 'your-bucket',
    ],

// to the following
  'disks' => [
    'local' => [
      'driver' => 'local',
      'root'   => public_path('uploads'),
    ],

    ...

    's3' => [
      'driver' => 's3',
      'key'    => env('AWS_KEY'),
      'secret' => env('AWS_SECRET'),
      'region' => env('AWS_REGION'),
      'bucket' => env('AWS_BUCKET'),
    ],

The first thing changed was the root of the local storage. This changed to the public/uploads directory just created a moment ago. Then each of the configuration settings for the Amazon S3 storage driver changed to pull the values from the environment. This is so we can change them later in our .env file.

Adding a Helpers file

With projects in Laravel 5.1 it’s often handy to have a place to stash those little functions which don’t warrant a class on their own. A common practice is to place these functions in a helpers file.

Create a new file in the app directory named helpers.php. Populate this file with the code below.

Content of helpers.php

<?php

/**
 * Return sizes readable by humans
 */
function human_filesize($bytes, $decimals = 2)
{
  $size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
  $factor = floor((strlen($bytes) - 1) / 3);

  return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) .
      @$size[$factor];
}

/**
 * Is the mime type an image
 */
function is_image($mimeType)
{
    return starts_with($mimeType, 'image/');
}

The human_filesize() function returns a file size which is easier to read than just a count of bytes. The is_image() function returns true if the mime type is an image.

To make you application aware of the helpers.php file, update composer.json as instructed below.

Updates to composer.json for helpers.php

{
  ...
  "autoload": {
    "classmap": [
      "database"
    ],
    "psr-4": {
      "App\\": "app/"
    },
    "files": [
      "app/helpers.php"
    ]
  },
  ...
}

The files array within the autoload section specifies any files to always load. Do a composer dumpauto to set up the autoloader correctly.

Composer dumpauto

vagrant@homestead:~/Code/l5beauty$ composer dumpauto
Generating autoload files

Now any functions in helpers.php will always be loaded and available to the l5beauty application.

Creating an Upload Manager Service

Now that the basic configuration is finished, let’s create a service class to manage our uploaded files.

Detecting Mime Types

Depending on the files being uploaded, we may want to have different actions occur. So it’d be nice to easily detect the Mime type of files.

PHP has the function mime_content_type(), but it’s deprecated. The PHP Fileinfo functions can detect the mime type, but in Windows a DLL must be copied to make fileinfo functions work. Let’s use a different solution.

Searching Packagist for “mime” shows a package by dflydev which looks good.

Add the package to composer with the instructions below.

Adding dflydev mime package to composer

vagrant@homestead:~/Code/l5beauty$ composer require "dflydev/apache-mime-types"
Using version ^1.0 for dflydev/apache-mime-types
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing dflydev/apache-mime-types (v1.0.1)
    Downloading: 100%

Writing lock file
Generating autoload files

We’ll use this package in the UploadsManager class to detect a file’s mime type.

Creating the UploadsManager class

Create the file UploadsManager in the app/Services directory with the following content.

Initial version of UploadsManager

<?php
namespace App\Services;

use Carbon\Carbon;
use Dflydev\ApacheMimeTypes\PhpRepository;
use Illuminate\Support\Facades\Storage;

class UploadsManager
{
  protected $disk;
  protected $mimeDetect;

  public function __construct(PhpRepository $mimeDetect)
  {
    $this->disk = Storage::disk(config('blog.uploads.storage'));
    $this->mimeDetect = $mimeDetect;
  }

  /**
   * Return files and directories within a folder
   *
   * @param string $folder
   * @return array of [
   *    'folder' => 'path to current folder',
   *    'folderName' => 'name of just current folder',
   *    'breadCrumbs' => breadcrumb array of [ $path => $foldername ]
   *    'folders' => array of [ $path => $foldername] of each subfolder
   *    'files' => array of file details on each file in folder
   * ]
   */
  public function folderInfo($folder)
  {
    $folder = $this->cleanFolder($folder);

    $breadcrumbs = $this->breadcrumbs($folder);
    $slice = array_slice($breadcrumbs, -1);
    $folderName = current($slice);
    $breadcrumbs = array_slice($breadcrumbs, 0, -1);

    $subfolders = [];
    foreach (array_unique($this->disk->directories($folder)) as $subfolder) {
      $subfolders["/$subfolder"] = basename($subfolder);
    }

    $files = [];
    foreach ($this->disk->files($folder) as $path) {
        $files[] = $this->fileDetails($path);
    }

    return compact(
      'folder',
      'folderName',
      'breadcrumbs',
      'subfolders',
      'files'
    );
  }

  /**
   * Sanitize the folder name
   */
  protected function cleanFolder($folder)
  {
    return '/' . trim(str_replace('..', '', $folder), '/');
  }

  /**
   * Return breadcrumbs to current folder
   */
  protected function breadcrumbs($folder)
  {
    $folder = trim($folder, '/');
    $crumbs = ['/' => 'root'];

    if (empty($folder)) {
      return $crumbs;
    }

    $folders = explode('/', $folder);
    $build = '';
    foreach ($folders as $folder) {
      $build .= '/'.$folder;
      $crumbs[$build] = $folder;
    }

    return $crumbs;
  }

  /**
   * Return an array of file details for a file
   */
  protected function fileDetails($path)
  {
    $path = '/' . ltrim($path, '/');

    return [
      'name' => basename($path),
      'fullPath' => $path,
      'webPath' => $this->fileWebpath($path),
      'mimeType' => $this->fileMimeType($path),
      'size' => $this->fileSize($path),
      'modified' => $this->fileModified($path),
    ];
  }

  /**
   * Return the full web path to a file
   */
  public function fileWebpath($path)
  {
    $path = rtrim(config('blog.uploads.webpath'), '/') . '/' .
        ltrim($path, '/');
    return url($path);
  }

  /**
   * Return the mime type
   */
  public function fileMimeType($path)
  {
      return $this->mimeDetect->findType(
        pathinfo($path, PATHINFO_EXTENSION)
      );
  }

  /**
   * Return the file size
   */
  public function fileSize($path)
  {
    return $this->disk->size($path);
  }

  /**
   * Return the last modified time
   */
  public function fileModified($path)
  {
    return Carbon::createFromTimestamp(
      $this->disk->lastModified($path)
    );
  }
}

__construct()
Inject dependencies. Use the disk defined in the blog configuration and the class from the dflydev/apache-mime-types package required earlier.
folderInfo()
Here’s the main method that returns everything needed about the contents of a folder.

Comments within the code explain the rest of the methods well enough you should be able to follow what’s going on.

Implementing UploadController index

Now that the UploadsManager service class has been created to do the bulk of our work, implementing the index method is almost trivial.

Creating the index method

Update the UploadController.php file located in the app/Http/Controllers/Admin directory to match what’s below.

Updating UploadController for index

<?php
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Services\UploadsManager;
use Illuminate\Http\Request;

class UploadController extends Controller
{
  protected $manager;

  public function __construct(UploadsManager $manager)
  {
    $this->manager = $manager;
  }

  /**
   * Show page of files / subfolders
   */
  public function index(Request $request)
  {
    $folder = $request->get('folder');
    $data = $this->manager->folderInfo($folder);

    return view('admin.upload.index', $data);
  }
}

The constructor injects the UploadsManager dependency and the index() method simply returns the view with data returned from the folderInfo() method.

You probably noticed the $folder is taken from the request. Yes, we’ll just use a query argument to handle changing folders.

Creating the index view

Create the resources/views/admin/upload directory and within it create the index.blade.php file with the following content.

Content of index.blade.php

@extends('admin.layout')

@section('content')
  <div class="container-fluid">

    {{-- Top Bar --}}
    <div class="row page-title-row">
      <div class="col-md-6">
        <h3 class="pull-left">Uploads  </h3>
        <div class="pull-left">
          <ul class="breadcrumb">
            @foreach ($breadcrumbs as $path => $disp)
              <li><a href="/admin/upload?folder={{ $path }}">{{ $disp }}</a></li>
            @endforeach
            <li class="active">{{ $folderName }}</li>
          </ul>
        </div>
      </div>
      <div class="col-md-6 text-right">
        <button type="button" class="btn btn-success btn-md"
                data-toggle="modal" data-target="#modal-folder-create">
          <i class="fa fa-plus-circle"></i> New Folder
        </button>
        <button type="button" class="btn btn-primary btn-md"
                data-toggle="modal" data-target="#modal-file-upload">
          <i class="fa fa-upload"></i> Upload
        </button>
      </div>
    </div>

    <div class="row">
      <div class="col-sm-12">

        @include('admin.partials.errors')
        @include('admin.partials.success')

        <table id="uploads-table" class="table table-striped table-bordered">
          <thead>
            <tr>
              <th>Name</th>
              <th>Type</th>
              <th>Date</th>
              <th>Size</th>
              <th data-sortable="false">Actions</th>
            </tr>
          </thead>
          <tbody>

{{-- The Subfolders --}}
@foreach ($subfolders as $path => $name)
  <tr>
    <td>
      <a href="/admin/upload?folder={{ $path }}">
        <i class="fa fa-folder fa-lg fa-fw"></i>
        {{ $name }}
      </a>
    </td>
    <td>Folder</td>
    <td>-</td>
    <td>-</td>
    <td>
      <button type="button" class="btn btn-xs btn-danger"
              onclick="delete_folder('{{ $name }}')">
        <i class="fa fa-times-circle fa-lg"></i>
        Delete
      </button>
    </td>
  </tr>
@endforeach

{{-- The Files --}}
@foreach ($files as $file)
  <tr>
    <td>
      <a href="{{ $file['webPath'] }}">
        @if (is_image($file['mimeType']))
          <i class="fa fa-file-image-o fa-lg fa-fw"></i>
        @else
          <i class="fa fa-file-o fa-lg fa-fw"></i>
        @endif
        {{ $file['name'] }}
      </a>
    </td>
    <td>{{ $file['mimeType'] or 'Unknown' }}</td>
    <td>{{ $file['modified']->format('j-M-y g:ia') }}</td>
    <td>{{ human_filesize($file['size']) }}</td>
    <td>
      <button type="button" class="btn btn-xs btn-danger"
              onclick="delete_file('{{ $file['name'] }}')">
        <i class="fa fa-times-circle fa-lg"></i>
        Delete
      </button>
      @if (is_image($file['mimeType']))
        <button type="button" class="btn btn-xs btn-success"
                onclick="preview_image('{{ $file['webPath'] }}')">
          <i class="fa fa-eye fa-lg"></i>
          Preview
        </button>
      @endif
    </td>
  </tr>
@endforeach

          </tbody>
        </table>

      </div>
    </div>
  </div>

  @include('admin.upload._modals')

@stop

@section('scripts')
  <script>

    // Confirm file delete
    function delete_file(name) {
      $("#delete-file-name1").html(name);
      $("#delete-file-name2").val(name);
      $("#modal-file-delete").modal("show");
    }

    // Confirm folder delete
    function delete_folder(name) {
      $("#delete-folder-name1").html(name);
      $("#delete-folder-name2").val(name);
      $("#modal-folder-delete").modal("show");
    }

    // Preview image
    function preview_image(path) {
      $("#preview-image").attr("src", path);
      $("#modal-image-view").modal("show");
    }

    // Startup code
    $(function() {
      $("#uploads-table").DataTable();
    });
  </script>
@stop

Although this template is long, it is easy to follow. All of the management of file uploading and downloading will be handled here.

Did you notice the inclusion of admin.upload._modals toward the end? I broke out the modal dialogs into a separate template.

For now, create an empty _modals.blade.php in the resources/views/admin/upload directory.

The Upload Manager Screen

Load up your browser, logging into the administration area and click on the Uploads link in the navigation bar. The screen should look like the one below.

Figure 10.1 - First Upload Manager Screen

Upload Manager Screen

Nice and clean. Now let’s implement all the modal dialogs and the functionality behind them.

Finishing the Upload Manager

There’s actually not a lot left to the upload manager. Let’s take it one area at a time and complete all the functionality.

Updating the routes

We need to finish setting up all needed routes for the Upload Manager. Edit app/Http/routes.php making the changes below.

Changes to routes.php

// After the line that reads
  get('admin/upload', 'UploadController@index');

// Add the following routes
  post('admin/upload/file', 'UploadController@uploadFile');
  delete('admin/upload/file', 'UploadController@deleteFile');
  post('admin/upload/folder', 'UploadController@createFolder');
  delete('admin/upload/folder', 'UploadController@deleteFolder');

Adding all the Modal Dialogs

Edit that empty _modals.blade.php file we created a few minutes ago. (It’s in the resources/views/admin/upload directory.) Make the content match what’s below.

Content of admin.upload._modals view

{{-- Create Folder Modal --}}
<div class="modal fade" id="modal-folder-create">
  <div class="modal-dialog">
    <div class="modal-content">
      <form method="POST" action="/admin/upload/folder"
            class="form-horizontal">
        <input type="hidden" name="_token" value="{{ csrf_token() }}">
        <input type="hidden" name="folder" value="{{ $folder }}">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal">
            ×
          </button>
          <h4 class="modal-title">Create New Folder</h4>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <label for="new_folder_name" class="col-sm-3 control-label">
              Folder Name
            </label>
            <div class="col-sm-8">
              <input type="text" id="new_folder_name" name="new_folder"
                     class="form-control">
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">
            Cancel
          </button>
          <button type="submit" class="btn btn-primary">
            Create Folder
          </button>
        </div>
      </form>
    </div>
  </div>
</div>

{{-- Delete File Modal --}}
<div class="modal fade" id="modal-file-delete">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          ×
        </button>
        <h4 class="modal-title">Please Confirm</h4>
      </div>
      <div class="modal-body">
        <p class="lead">
          <i class="fa fa-question-circle fa-lg"></i>  
          Are you sure you want to delete the
          <kbd><span id="delete-file-name1">file</span></kbd>
          file?
        </p>
      </div>
      <div class="modal-footer">
        <form method="POST" action="/admin/upload/file">
          <input type="hidden" name="_token" value="{{ csrf_token() }}">
          <input type="hidden" name="_method" value="DELETE">
          <input type="hidden" name="folder" value="{{ $folder }}">
          <input type="hidden" name="del_file" id="delete-file-name2">
          <button type="button" class="btn btn-default" data-dismiss="modal">
            Cancel
          </button>
          <button type="submit" class="btn btn-danger">
            Delete File
          </button>
        </form>
      </div>
    </div>
  </div>
</div>

{{-- Delete Folder Modal --}}
<div class="modal fade" id="modal-folder-delete">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          ×
        </button>
        <h4 class="modal-title">Please Confirm</h4>
      </div>
      <div class="modal-body">
        <p class="lead">
          <i class="fa fa-question-circle fa-lg"></i>  
          Are you sure you want to delete the
          <kbd><span id="delete-folder-name1">folder</span></kbd>
          folder?
        </p>
      </div>
      <div class="modal-footer">
        <form method="POST" action="/admin/upload/folder">
          <input type="hidden" name="_token" value="{{ csrf_token() }}">
          <input type="hidden" name="_method" value="DELETE">
          <input type="hidden" name="folder" value="{{ $folder }}">
          <input type="hidden" name="del_folder" id="delete-folder-name2">
          <button type="button" class="btn btn-default" data-dismiss="modal">
            Cancel
          </button>
          <button type="submit" class="btn btn-danger">
            Delete Folder
          </button>
        </form>
      </div>
    </div>
  </div>
</div>

{{-- Upload File Modal --}}
<div class="modal fade" id="modal-file-upload">
  <div class="modal-dialog">
    <div class="modal-content">
      <form method="POST" action="/admin/upload/file"
            class="form-horizontal" enctype="multipart/form-data">
        <input type="hidden" name="_token" value="{{ csrf_token() }}">
        <input type="hidden" name="folder" value="{{ $folder }}">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal">
            ×
          </button>
          <h4 class="modal-title">Upload New File</h4>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <label for="file" class="col-sm-3 control-label">
              File
            </label>
            <div class="col-sm-8">
              <input type="file" id="file" name="file">
            </div>
          </div>
          <div class="form-group">
            <label for="file_name" class="col-sm-3 control-label">
              Optional Filename
            </label>
            <div class="col-sm-4">
              <input type="text" id="file_name" name="file_name"
                     class="form-control">
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">
            Cancel
          </button>
          <button type="submit" class="btn btn-primary">
            Upload File
          </button>
        </div>
      </form>
    </div>
  </div>
</div>

{{-- View Image Modal --}}
<div class="modal fade" id="modal-image-view">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          ×
        </button>
        <h4 class="modal-title">Image Preview</h4>
      </div>
      <div class="modal-body">
        <img id="preview-image" src="x" class="img-responsive">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">
          Cancel
        </button>
      </div>
    </div>
  </div>
</div>

There’s five different modal popup boxes in that file. Each one will do the appropriate POST or DELETE to the routes just set up.

Adding the Request classes

Create the UploadFileRequest and UploadNewFolderRequest with the content below.

You can use the artisan make:request ClassName command to create a blank file in the app/Http/Requests directory, or manually create them.

Content of UploadFileRequest.php

<?php
namespace App\Http\Requests;

class UploadFileRequest extends Request
{
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'file' => 'required',
      'folder' => 'required',
    ];
  }
}

Content of UploadNewFolderRequest.php

<?php
namespace App\Http\Requests;

class UploadNewFolderRequest extends Request
{
  /**
   * Determine if the user is authorized to make this request.
   *
   * @return bool
   */
  public function authorize()
  {
    return true;
  }

  /**
   * Get the validation rules that apply to the request.
   *
   * @return array
   */
  public function rules()
  {
    return [
      'folder' => 'required',
      'new_folder' => 'required',
    ];
  }
}

Again, these are very simple classes that validate the input on construction.

Finishing UploadController

Update the UploadController.php file as instructed below.

Updates to UploadController

<?php
// Add the following 3 lines at the top, with the use statements
use App\Http\Requests\UploadFileRequest;
use App\Http\Requests\UploadNewFolderRequest;
use Illuminate\Support\Facades\File;

// Add the following 4 methods to the UploadControllerClass
  /**
   * Create a new folder
   */
  public function createFolder(UploadNewFolderRequest $request)
  {
    $new_folder = $request->get('new_folder');
    $folder = $request->get('folder').'/'.$new_folder;

    $result = $this->manager->createDirectory($folder);

    if ($result === true) {
      return redirect()
          ->back()
          ->withSuccess("Folder '$new_folder' created.");
    }

    $error = $result ? : "An error occurred creating directory.";
    return redirect()
        ->back()
        ->withErrors([$error]);
  }

  /**
   * Delete a file
   */
  public function deleteFile(Request $request)
  {
    $del_file = $request->get('del_file');
    $path = $request->get('folder').'/'.$del_file;

    $result = $this->manager->deleteFile($path);

    if ($result === true) {
      return redirect()
          ->back()
          ->withSuccess("File '$del_file' deleted.");
    }

    $error = $result ? : "An error occurred deleting file.";
    return redirect()
        ->back()
        ->withErrors([$error]);
  }

  /**
   * Delete a folder
   */
  public function deleteFolder(Request $request)
  {
    $del_folder = $request->get('del_folder');
    $folder = $request->get('folder').'/'.$del_folder;

    $result = $this->manager->deleteDirectory($folder);

    if ($result === true) {
      return redirect()
          ->back()
          ->withSuccess("Folder '$del_folder' deleted.");
    }

    $error = $result ? : "An error occurred deleting directory.";
    return redirect()
        ->back()
        ->withErrors([$error]);
  }

  /**
   * Upload new file
   */
  public function uploadFile(UploadFileRequest $request)
  {
    $file = $_FILES['file'];
    $fileName = $request->get('file_name');
    $fileName = $fileName ?: $file['name'];
    $path = str_finish($request->get('folder'), '/') . $fileName;
    $content = File::get($file['tmp_name']);

    $result = $this->manager->saveFile($path, $content);

    if ($result === true) {
      return redirect()
          ->back()
          ->withSuccess("File '$fileName' uploaded.");
    }

    $error = $result ? : "An error occurred uploading file.";
    return redirect()
        ->back()
        ->withErrors([$error]);
  }

I’m not going to comment much on those methods because this chapter’s already getting long. Basically, we’re calling methods on the UploadsManager class to do the actual file work.

Finishing the UploadsManager Service class

Update the UploadsManager class in the app/Services directory, making the changes below.

Updates to UploadsManager

<?php
// Add the 4 methods below to the class
  /**
   * Create a new directory
   */
  public function createDirectory($folder)
  {
    $folder = $this->cleanFolder($folder);

    if ($this->disk->exists($folder)) {
      return "Folder '$folder' aleady exists.";
    }

    return $this->disk->makeDirectory($folder);
  }

  /**
   * Delete a directory
   */
  public function deleteDirectory($folder)
  {
    $folder = $this->cleanFolder($folder);

    $filesFolders = array_merge(
      $this->disk->directories($folder),
      $this->disk->files($folder)
    );
    if (! empty($filesFolders)) {
      return "Directory must be empty to delete it.";
    }

    return $this->disk->deleteDirectory($folder);
  }

  /**
   * Delete a file
   */
  public function deleteFile($path)
  {
    $path = $this->cleanFolder($path);

    if (! $this->disk->exists($path)) {
      return "File does not exist.";
    }

    return $this->disk->delete($path);
  }

  /**
   * Save a file
   */
  public function saveFile($path, $content)
  {
    $path = $this->cleanFolder($path);

    if ($this->disk->exists($path)) {
      return "File already exists.";
    }

    return $this->disk->put($path, $content);
  }


Four new methods here, which implement the new functionality of the Upload Manager.

Congratulations

You now have completed the Upload Manager for your blog’s administration area. Everything should work. Give it a try. Upload a few files, create a directory. Try uploading an image and you’ll see an option to preview it.

Setting Up Your S3 Account

Using Amazon Simple Storage Service, also known as Amazon S3, is a cheap, fast and easy solution for storing files in The Cloud. With Laravel 5.1 it’s easy to configure the Upload Manager created in this chapter to save and retrieve files using Amazon S3.

Converting to Amazon S3 is optional. If you don’t want to do this, skip to the next chapter.

Here’s a quick startup list to set up S3 services.

1. Log in (or sign up) with Amazon

Go to amazon.com and log in if you have an existing account. If not, sign up for a new account..

2. Get a Web Services Account

Go to aws.amazon.com and sign up for a Amazon Web Services Account. You can sign up for a free account but you’ll still will need to enter a credit card.

3. Create a S3 Bucket

After you’ve created or logged into your AWS account, go to the Console and click on the S3 link. If you’ve never created an S3 bucket before you’ll be prompted to do so.

Figure 10.2 - AWS Create Bucket

AWS Create Bucket

Here I’m using l5beauty as the bucket name and taking the default region of Oregon it provides for me. (If you use a bucket name already in use Amazon will prompt you to pick a different name.)

If you have created a bucket before, it probably is still a good idea to create a new one.

Remember this Bucket and Region

You’ll need it later when we configure the application to use S3.

4. Create an Access Key

Click on your name at the top of the screen and then choose Security Credentials. This will present you with the Security Credentials screen. Click on the Access Keys area and you’ll see a screen similar to the one below.

Figure 10.2 - AWS Security Credentials

AWS Security Credentials

Click on the [Create New Access Key] button and you’ll see a screen that follows.

Important

Write down the values from the screen below before closing the window. You may want to download the key file too. You won’t have access to the secret key again.

Figure 10.3 - Create New Access Key

Create New Access Key

S3 Is Now Set Up

For now. Later we’ll make access public

Configuring L5Beauty to Use S3

Now that S3 services are set up, let’s configure the blog to use them.

Use the table below to get the code for the region you used when creating your bucket.

Code Name
ap-northeast-1 Asia Pacific (Tokyo)
ap-southeast-1 Asia Pacific (Singapore)
ap-southeast-2 Asia Pacific (Sydney)
eu-central-1 EU (Frankfurt)
eu-west-1 EU (Ireland)
sa-east-1 South America (Sao Paulo)
us-east-1 US East (N. Virginia)
us-west-1 US West (N. California)
us-west-2 US West (Oregon)

Edit the .env file, filling in the AWS parameters like below.

AWS paramters in .env

AWS_KEY=AKIAJTP7B64BVNBJKZ3Q
AWS_SECRET=j1bKrPz7Gm7gdRkgrHULHAcX4/gXh/nQF1VPc+ZM
AWS_REGION=us-west-2
AWS_BUCKET=l5beauty

USE YOUR OWN VALUES. (I’ll delete these values from my account shortly.)

Edit the config/blog.php file as follows

Update of blog config

<?php
return [
  'title' => 'My Blog',
  'posts_per_page' => 5,
  'uploads' => [
    'storage' => 's3',
    'webpath' => 'https://s3-us-west-2.amazonaws.com/l5beauty',
  ],
];

The webpath seems to follow the pattern: https://s3-REGION.amazonaws.com/BUCKET/. If this is incorrect, don’t worry about it right now.

Installing an Additional Package

Laravel 5.1 requires an additional package when interfacing with S3. From the OS Console, install it as instructed below.

Requiring flysystem-aws

~/Code/l5beauty% composer require league/flysystem-aws-s3-v3
Using version ^1.0 for league/flysystem-aws-s3-v3
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
  - Installing mtdowling/jmespath.php (2.2.0)
    Downloading: 100%

  - Installing guzzlehttp/promises (1.0.1)
    Loading from cache

  - Installing psr/http-message (1.0)
    Loading from cache

  - Installing guzzlehttp/psr7 (1.1.0)
    Downloading: 100%

  - Installing guzzlehttp/guzzle (6.0.1)
    Loading from cache

  - Installing aws/aws-sdk-php (3.0.6)
    Downloading: 100%

  - Installing league/flysystem-aws-s3-v3 (1.0.3)
    Downloading: 100%

Writing lock file
Generating autoload files
Generating optimized class loader

Test The Upload Manager

Point your browser to your l5beauty project’s administration area and go into uploads. Try creating directories, uploading files. Be sure to upload at least one image before moving on.

Also, verify from within your AWS Console that changes you make are reflected there.

Important

After uploading a file from the Upload Manager, view your bucket from the AWS console, click on the file and check the Properties tab. There will be a Link: value. Examine this URL and make any necessary changes to the uploads.webpath property of config/blog.php.

Fixing Bucket Permissions

Everything should work great, from the administration area, but if a user visits your web page and you’re displaying an image you’ve stored on S3, they won’t have permissions.

To fix this we’ll make everything on this bucket you created publicly viewable.

Go into the AWS Console and navigate to the bucket you just created. Then click on the Properties tab and click on the Permissions accordion to expand it. You should see a screen like the one below:

Figure 10.4 - Bucket Permissions

Bucket Permissions

Click on the Add bucket policy link and when the Bucket Policy Editor appears click on the new policy link.

The following screen will appear.

Figure 10.5 - AWS Policy Generator

AWS Policy Generator

The Actions selected should be GetObject.

Click the [Add Statement] button and then the [Generate Policy] button and you’ll see a policy like the one below.

Policy JSON Document

{
  "Id": "Policy1430071323597",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1430071313897",
      "Action": [
        "s3:GetObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::l5beauty/*",
      "Principal": "*"
    }
  ]
}

Copy this text and paste it into the previous screen’s Bucket Policy Editor box, click Save and you’re done.

Congratulations

Your Upload Manager should now work, allowing you to manage any files uploaded for your block in the cloud.

Recap

In this chapter we configured the file system and built an upload manager that allowed us to upload files to the public area of the L5 Beauty application. We added a helpers.php file to the application as a place to store one-off functions.

A couple new packages were pulled in. One to detect mime-types and one required to access the Amazon S3 service.

Instructions were provided to set up and configure the Amazon S3 service. And finally, we configured our Upload Manager to use this.

Quite a bit accomplished in this chapter.

comments powered by Disqus