Skip to content

Commit

Permalink
Improved Form Post (#65)
Browse files Browse the repository at this point in the history
* Improved FormPost function, documentation and added more tests

* Fix typo

* Fixed  not being present for old Laravel Versions

* Remove references to Carbon

Co-authored-by: sausin <sausin@users.noreply.github.com>
  • Loading branch information
iksaku and sausin authored Jun 18, 2020
1 parent 12b60aa commit 7cf671e
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 34 deletions.
67 changes: 58 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Install via composer:
composer require sausin/laravel-ovh
```

Please see below for the details on various branches. You can choose the version of the package which is suitable for your development. Also take note of the upgrade
Please see below for the details on various branches. You can choose the version of the package which is suitable for your development.
Also, take note of the upgrade.

| Package version | PHP compatibility | Laravel versions | Special features of OVH | Status |
| --------------- | :---------------: | :--------------: | :---------------------------------------: | :--------- |
Expand Down Expand Up @@ -177,7 +178,8 @@ to every newly uploaded object by default:
OS_DELETE_AFTER=259200
```

For more information about these variables, please refer to [OVH's Automatic Object Deletion Documentation](https://docs.ovh.com/gb/en/storage/configure_automatic_object_deletion/)
For more information about these variables, please refer to
[OVH's Automatic Object Deletion Documentation](https://docs.ovh.com/gb/en/storage/configure_automatic_object_deletion/)

## Large Object Support

Expand All @@ -202,24 +204,71 @@ OS_SEGMENT_CONTAINER="large-object-container-name"
```

Using a separate container for storing the segments of your Large Objects can be beneficial in
some cases, to learn more about this, please refer to [OpenStack's Last Note on Using Swift for Large Objects](https://docs.openstack.org/swift/stein/overview_large_objects.html#using-swift)
some cases, to learn more about this, please refer to
[OpenStack's Last Note on Using Swift for Large Objects](https://docs.openstack.org/swift/stein/overview_large_objects.html#using-swift)

To learn more about segmented uploads for large objects, please refer to:
- [OVH's Optimizing Large Object Uploads Documentation](https://docs.ovh.com/gb/en/storage/optimised_method_for_uploading_files_to_object_storage/)
- [OpenStack's Large Object Support Documentation](https://docs.openstack.org/swift/latest/overview_large_objects.html)

## Form Post Middleware

While this feature in not documented by the OVH team, it's explained in the openstack [documentation](https://docs.openstack.org/swift/latest/api/form_post_middleware.html). This setup allows for uploading of files _directly_ to the OVH servers rather than going through the application servers (which is quite inefficient).
While this feature in not documented by the OVH team, it's explained in the
[OpenStack's Documentation](https://docs.openstack.org/swift/latest/api/form_post_middleware.html).

This feature allows for uploading of files _directly_ to the OVH servers rather than going through the application servers
(which is quite inefficient).

You must generate a valid FormPost signature, for which you can use the following function:
```php
Storage::disk('ovh')->getAdapter()->getFormPostSignature($path, $expiresAt, $redirect, $maxFileCount, $maxFileSize);
```
Where:
- `$path` is the directory path in which you would like to store the files.
- `$expiresAt` is a `DateTimeInterface` object that specifies a date in which the FormPost signature will expire.
- `$redirect` is the URL to which the user will be redirected once all files finish uploading. Defaults to `null` to prevent redirects.
- `$maxFileCount` is the max quantity of files that the user will be able to upload using the signature. Defaults to `1` file.
- `$maxFileSize` is the size limit that each uploaded file can have. Defaults to `25 MB` (`25*1024*1024`).

After obtaining the signature, you need to pass the signature data to your HTML form:
```blade
<form action="{{ $url }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="redirect" value="{{ $redirect }}">
<input type="hidden" name="max_file_count" value="{{ $maxFileCount }}">
<input type="hidden" name="max_file_size" value="{{ $maxFileSize }}">
<input type="hidden" name="expires" value="{{ $expiresAt->getTimestamp() }}">
<input type="hidden" name="signature" value="{{ $signature }}">
<input type="file">
</form>
```

> **NOTE**: The upload method in the form _must_ be type of `POST`.
The signature can be obtained by using the following command:
The `$url` variable refers to the path URL to your container, you can get it by passing the path to the adapter `getUrl`:
```php
Storage::disk('ovh')->getAdapter()->getFormPostSignature('path', time() + 600, 1, 50*1024*1024)
$url = Storage::disk('ovh')->getAdapter()->getUrl($path);
```
The above example will generate a signature which is valid of 600 seconds, allows for 1 file to be uploaded with a maximum size of 50 MB. Parameters can be tweaked as desired.

The upload form can then use this signature as described in the openstack documentation.
> **NOTE**: If you've setup a custom domain for your Object Storage container, you can use that domain (along with the corresponding path)
> to upload your files without exposing your OVH's URL scheme.
Important note:- The upload method in the form _must_ be of the type POST.
### Examples

```php
// Generate a signature that allows an upload to the 'images' directory for the next 10 minutes.
Storage::disk('ovh')->getAdapter()->getFormPostSignature('images', now()->addMinutes(10));

// Generate a signature that redirects to a url after successful file upload to the root of the container.
Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addMinutes(5), route('file-uploaded'));

// Generate a signature that allows upload of 3 files until next day.
Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addDay(), null, 3);

// Generate a signature that allows to upload 1 file of 1GB until the next hour.
Storage::disk('ovh')->getAdapter()->getFormPostSignature('', now()->addHour(), null, 1, 1 * 1024 * 1024 * 1024);
```


# Credits
Expand Down
4 changes: 3 additions & 1 deletion src/OVHConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Sausin\LaravelOvh;

use BadMethodCallException;

class OVHConfiguration
{
/**
Expand Down Expand Up @@ -161,7 +163,7 @@ public static function make(array $config): self
$missingKeys = array_diff($neededKeys, array_keys($config));

if (count($missingKeys) > 0) {
throw new \BadMethodCallException('The following keys must be provided: '.implode(', ', $missingKeys));
throw new BadMethodCallException('The following keys must be provided: '.implode(', ', $missingKeys));
}

return (new self())
Expand Down
35 changes: 24 additions & 11 deletions src/OVHSwiftAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Sausin\LaravelOvh;

use DateTime;
use DateTimeInterface;
use InvalidArgumentException;
use League\Flysystem\Config;
use Nimbusoft\Flysystem\OpenStack\SwiftAdapter;
use OpenStack\Common\Error\BadResponseError;
Expand Down Expand Up @@ -87,16 +89,21 @@ public function getUrlConfirm($path): string
/**
* Generate a temporary URL for private containers.
*
* For more information, refer to OpenStack's documentation on Temporary URL middleware:
* https://docs.openstack.org/swift/stein/api/temporary_url_middleware.html
*
* @param string $path
* @param \DateTimeInterface $expiresAt
* @param DateTimeInterface $expiresAt
* @param array $options
* @return string
*/
public function getTemporaryUrl(string $path, DateTimeInterface $expiresAt, array $options = []): string
{
// Ensure Temp URL Key is provided for the Disk
if (empty($this->config->getTempUrlKey())) {
throw new \InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'");
throw new InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'");
}

// Get the method
Expand Down Expand Up @@ -126,25 +133,28 @@ public function getTemporaryUrl(string $path, DateTimeInterface $expiresAt, arra
}

/**
* Get form post signature. See Form POST middleware documentation.
* Generate a FormPost signature to upload files directly to your OVH container.
*
* For more information, refer to OpenStack's documentation on FormPost middleware:
* https://docs.openstack.org/swift/stein/api/form_post_middleware.html
*
* @param string $path
* @param DateTimeInterface $expiresAt
* @param string $redirect
* @param string|null $redirect
* @param int $maxFileCount
* @param int $maxFileSize
* @param int $maxFileSize Defaults to 25MB (25 * 1024 * 1024)
* @return string
*/
public function getFormPostSignature(string $path, DateTimeInterface $expiresAt, string $redirect = '', int $maxFileCount = 1, int $maxFileSize = 26214400): string
public function getFormPostSignature(string $path, DateTimeInterface $expiresAt, ?string $redirect = null, int $maxFileCount = 1, int $maxFileSize = 26214400): string
{
// Ensure Temp URL Key is provided for the Disk
if (empty($this->config->getTempUrlKey())) {
throw new \InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'");
throw new InvalidArgumentException("No Temp URL Key provided for container '".$this->container->name."'");
}

// check if the 'expires' values is in the past
if (($expiresAt->getTimestamp() ?? 0) < time()) {
throw new \InvalidArgumentException("Value of 'expires' cannot be in the past");
// Ensure that 'expires' timestamp is in the future
if ((new DateTime()) >= $expiresAt) {
throw new InvalidArgumentException('Expiration time of FormPost signature must be in the future.');
}

// Ensure $path doesn't begin with a slash
Expand All @@ -159,7 +169,7 @@ public function getFormPostSignature(string $path, DateTimeInterface $expiresAt,
);

// Body for the HMAC hash
$body = sprintf("%s\n%s\n%s\n%s\n%s", $codePath, $redirect, $maxFileSize, $maxFileCount, $expiresAt->getTimestamp());
$body = sprintf("%s\n%s\n%s\n%s\n%s", $codePath, $redirect ?? '', $maxFileSize, $maxFileCount, $expiresAt->getTimestamp());

// The actual hash signature
return hash_hmac('sha1', $body, $this->config->getTempUrlKey());
Expand Down Expand Up @@ -188,10 +198,13 @@ protected function getWriteData($path, $config): array
$data = parent::getWriteData($path, $config);

if ($config->has('deleteAfter')) {
// Apply object expiration timestamp if given
$data['deleteAfter'] = $config->get('deleteAfter');
} elseif ($config->has('deleteAt')) {
// Apply object expiration time if given
$data['deleteAt'] = $config->get('deleteAt');
} elseif (!empty($this->config->getDeleteAfter())) {
// Apply default object expiration time from package config
$data['deleteAfter'] = $this->config->getDeleteAfter();
}

Expand Down
44 changes: 31 additions & 13 deletions tests/Functional/UrlGenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Sausin\LaravelOvh\Tests\Functional;

use DateInterval;
use DateTime;
use Sausin\LaravelOvh\Tests\TestCase;

class UrlGenerationTest extends TestCase
Expand Down Expand Up @@ -72,39 +74,55 @@ public function testCanGenerateTemporaryUrl()

$this->object->shouldNotReceive('retrieve', 'getObject');

$url = $this->adapter->getTemporaryUrl('hello.jpg', new \DateTime('2004-09-22'));
$url = $this->adapter->getTemporaryUrl('hello.jpg', new DateTime('2004-09-22'));

$this->assertNotNull($url);
}

public function testCanGenerateFormPostSignature()
public function testCanGenerateTemporaryUrlOnCustomEndpoint()
{
$this->config->setTempUrlKey('my-key');
$this->config
->setEndpoint('http://custom.endpoint')
->setTempUrlKey('my-key');

$this->object->shouldNotReceive('retrieve', 'getObject');

$signature = $this->adapter->getFormPostSignature('/prefix', now()->addSeconds(60));
$url = $this->adapter->getTemporaryUrl('hello.jpg', new DateTime('2015-10-21'));

$this->assertNotNull($signature);
$this->assertNotNull($url);
}

public function testCanGenerateTemporaryUrlOnCustomEndpoint()
public function testTemporaryUrlWillFailIfNoKeyProvided()
{
$this->config
->setEndpoint('http://custom.endpoint')
->setTempUrlKey('my-key');
$this->expectException('InvalidArgumentException');

$this->adapter->getTemporaryUrl('hello.jpg', new DateTime('1979-06-13'));
}

public function testCanGenerateFormPostSignature()
{
$this->config->setTempUrlKey('my-key');

$this->object->shouldNotReceive('retrieve', 'getObject');

$url = $this->adapter->getTemporaryUrl('hello.jpg', new \DateTime('2015-10-21'));
$signature = $this->adapter->getFormPostSignature('images', (new DateTime())->add(new DateInterval('PT5M')));

$this->assertNotNull($url);
$this->assertNotNull($signature);
}

public function testTemporaryUrlWillFailIfNoKeyProvided()
public function testFormPostSignatureWillFailIfNoKeyProvided()
{
$this->expectException('InvalidArgumentException');

$this->adapter->getTemporaryUrl('hello.jpg', new \DateTime('1979-06-13'));
$this->adapter->getFormPostSignature('images', new DateTime());
}

public function testFormPostWillFailIfExpirationIsNotInTheFuture()
{
$this->config->setTempUrlKey('my-key');

$this->expectException('InvalidArgumentException');

$this->adapter->getFormPostSignature('images', new DateTime('2010-07-28'));
}
}

0 comments on commit 7cf671e

Please sign in to comment.