Skip to content

Commit

Permalink
Add option to expire links (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
aerni authored Dec 9, 2022
1 parent c0186f8 commit 7532ffd
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 27 deletions.
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ return [
| Save To Disk
|--------------------------------------------------------------------------
|
| Set this to 'true' to save the zips to disk.
| Set this to 'true' to save the created zips to disk.
| The saved file will be used the next time a user requests a zip with the same payload.
|
*/
Expand All @@ -44,6 +44,17 @@ return [

'disk' => 'public',

/*
|--------------------------------------------------------------------------
| Link Expiry
|--------------------------------------------------------------------------
|
| Set the time in minutes after which a link should expire.
|
*/

'expiry' => null,

];
```

Expand All @@ -61,24 +72,34 @@ images:
Somehwere in your views:
```html
```antlers
{{ zip:images }}
```

You may optionally pass a filename using the `filename` parameter. The example below binds the name of the zip to the title of the current page. The filename defaults to the current timestamp.
### Filename

You may optionally pass a filename using the `filename` parameter. If you don't provide one, the filename will default to the timestamp at the time of download. The example below binds the name of the zip to the title of the page.

```html
```antlers
{{ zip:images :filename='title' }}
```

### Link Expiry

If you want to expire your links after a certain time, you can either set the expiry globally in the config, or use the `expiry` parameter on the tag. The expiry is to be set in minutes. Note, that the expiry on the tag will overide the expiry in the config.

```antlers
{{ zip:images expiry="60" }}
```

## Advanced Usage

This addon also exposes two methods that let you get the route or create a zip programmatically.

The `route` method returns the route that handles creating the zip. This is the same as using the `zip` tag in your views:

```php
\Aerni\Zipper\Zipper::route($files, $filename);
\Aerni\Zipper\Zipper::route($files, $filename, $expiry);
```

The `create` method creates and returns the zip directly:
Expand Down
11 changes: 11 additions & 0 deletions config/zipper.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,15 @@

'disk' => 'public',

/*
|--------------------------------------------------------------------------
| Link Expiry
|--------------------------------------------------------------------------
|
| Set the time in minutes after which a link should expire.
|
*/

'expiry' => null,

];
2 changes: 1 addition & 1 deletion routes/actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
use Aerni\Zipper\ZipperController;
use Illuminate\Support\Facades\Route;

Route::get('/create/{files}', [ZipperController::class, 'create'])->name('zipper.create');
Route::get('/create/{cipher}', [ZipperController::class, 'create'])->name('zipper.create');
39 changes: 29 additions & 10 deletions src/Zipper.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use League\Flysystem\AwsS3V3\AwsS3V3Adapter;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Statamic\Contracts\Assets\Asset;
Expand All @@ -16,12 +17,22 @@

class Zipper
{
public static function route(Collection $files, ?string $filename = null): string
public static function route(Collection $files, ?string $filename = null, ?int $expiry = null): string
{
return route('statamic.zipper.create', [
'files' => self::encrypt($files),
'filename' => $filename,
]);
$expiry = $expiry ?? config('zipper.expiry');

if (empty($expiry)) {
return URL::signedRoute(
'statamic.zipper.create',
self::encrypt($files, $filename)
);
}

return URL::temporarySignedRoute(
'statamic.zipper.create',
now()->addMinutes($expiry),
self::encrypt($files, $filename)
);
}

public static function create(Collection $files, ?string $filename = null): mixed
Expand Down Expand Up @@ -85,22 +96,30 @@ protected static function addFile(Asset|string $file, ZipStream $zip): ZipStream
throw new Exception('Zipper doesn\'t support ['.$adapter::class.'].');
}

protected static function encrypt(Collection $files): string
protected static function encrypt(Collection $files, ?string $filename): string
{
$files = $files->map(fn ($file) => match (true) {
($file instanceof Asset) => $file->id(),
(is_string($file)) => $file,
default => throw new Exception('Unsupported file type. The file has to be a Statamic Asset, a URL or an absolute path.')
});

return Crypt::encryptString($files);
return Crypt::encrypt([
'files' => $files,
'filename' => $filename,
]);
}

public static function decrypt(string $files): Collection
public static function decrypt(string $cipher): array
{
$files = json_decode(Crypt::decryptString($files));
$plaintext = Crypt::decrypt($cipher);

$files = collect($plaintext['files'])->map(fn ($file) => AssetFacade::find($file) ?? $file);

return collect($files)->map(fn ($file) => AssetFacade::find($file) ?? $file);
return [
'files' => $files,
'filename' => $plaintext['filename'],
];
}

protected static function filename(?string $filename): string
Expand Down
14 changes: 8 additions & 6 deletions src/ZipperController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@

class ZipperController extends Controller
{
public function create(string $files, Request $request)
public function create(string $cipher, Request $request)
{
$request->validate([
'filename' => 'sometimes|required|string',
]);
if (! $request->hasValidSignature()) {
abort(401);
}

$plaintext = Zipper::decrypt($cipher);

return Zipper::create(
files: Zipper::decrypt($files),
filename: $request->get('filename')
files: $plaintext['files'],
filename: $plaintext['filename'],
);
}
}
3 changes: 2 additions & 1 deletion src/ZipperTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public function wildcard(): string

return Zipper::route(
files: collect($files),
filename: $this->params->get('filename')
filename: $this->params->get('filename'),
expiry: $this->params->get('expiry'),
);
}
}
10 changes: 6 additions & 4 deletions tests/ZipperTagsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ public function can_handle_a_single_asset()

$url = $this->tag->wildcard();

$files = Str::afterLast($url, '/');
$file = Zipper::decrypt($files)[0];
$uri = Str::afterLast($url, '/');
$cipher = Str::before($uri, '?signature');
$file = Zipper::decrypt($cipher)['files'][0];

$this->assertSame($value->value()->resolvedPath(), $file->resolvedPath());
}
Expand All @@ -73,8 +74,9 @@ public function can_handle_multiple_assets()

$url = $this->tag->wildcard();

$files = Str::afterLast($url, '/');
$files = Zipper::decrypt($files)->map(fn ($file) => $file->resolvedPath());
$uri = Str::afterLast($url, '/');
$cipher = Str::before($uri, '?signature');
$files = Zipper::decrypt($cipher)['files']->map(fn ($file) => $file->resolvedPath());

$value->value()->get()->each(function ($file) use ($files) {
$this->assertContains($file->resolvedPath(), $files);
Expand Down

0 comments on commit 7532ffd

Please sign in to comment.