Skip to content
This repository was archived by the owner on Feb 18, 2023. It is now read-only.

Commit 248c6f2

Browse files
committed
#22 Add Asset API endpoint to upload a file from a URL or request body.
1 parent 0d19999 commit 248c6f2

File tree

15 files changed

+603
-3
lines changed

15 files changed

+603
-3
lines changed

app/Entities/Assets/Asset.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace App\Entities\Assets;
4+
5+
use App\Support\UuidScopeTrait;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
/**
9+
* Class Asset
10+
*
11+
* @package App\Entities\Assets
12+
*/
13+
class Asset extends Model
14+
{
15+
use UuidScopeTrait;
16+
17+
/**
18+
* @var array
19+
*/
20+
protected $guarded = ['id'];
21+
}

app/Events/AssetWasCreated.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Events;
4+
5+
use App\Entities\Assets\Asset;
6+
7+
/**
8+
* Class AssetWasCreated
9+
*
10+
* @package App\Events
11+
*/
12+
class AssetWasCreated
13+
{
14+
/**
15+
* @var \App\Entities\Assets\Asset
16+
*/
17+
public $asset;
18+
19+
/**
20+
* AssetWasCreated constructor.
21+
*
22+
* @param \App\Entities\Assets\Asset $asset
23+
*/
24+
public function __construct(Asset $asset)
25+
{
26+
$this->asset = $asset;
27+
}
28+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
use Exception;
6+
7+
/**
8+
* Class BodyTooLargeException
9+
*
10+
* @package App\Exceptions
11+
*/
12+
class BodyTooLargeException extends Exception
13+
{
14+
15+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\Assets;
4+
5+
use GuzzleHttp\Client;
6+
use Illuminate\Http\Request;
7+
use App\Entities\Assets\Asset;
8+
use Dingo\Api\Routing\Helpers;
9+
use App\Events\AssetWasCreated;
10+
use App\Http\Controllers\Controller;
11+
use Illuminate\Support\Facades\Storage;
12+
use App\Exceptions\BodyTooLargeException;
13+
use GuzzleHttp\Exception\TransferException;
14+
use App\Transformers\Assets\AssetTransformer;
15+
use Dingo\Api\Exception\StoreResourceFailedException;
16+
17+
/**
18+
* Class UploadFileController
19+
*
20+
* @package App\Http\Controllers\Api\Assets
21+
*/
22+
class UploadFileController extends Controller
23+
{
24+
use Helpers;
25+
26+
/**
27+
* @var array
28+
*/
29+
protected $validMimes = [
30+
'image/jpeg' => [
31+
'type' => 'image',
32+
'extension' => 'jpeg',
33+
],
34+
'image/jpg' => [
35+
'type' => 'image',
36+
'extension' => 'jpg',
37+
],
38+
'image/png' => [
39+
'type' => 'image',
40+
'extension' => 'png',
41+
],
42+
];
43+
44+
/**
45+
* @var \GuzzleHttp\Client
46+
*/
47+
protected $client;
48+
49+
/**
50+
* @var \App\Entities\Assets\Asset
51+
*/
52+
protected $model;
53+
54+
/**
55+
* UploadFileController constructor.
56+
*
57+
* @param \GuzzleHttp\Client $client
58+
* @param \App\Entities\Assets\Asset $model
59+
*/
60+
public function __construct(Client $client, Asset $model)
61+
{
62+
$this->client = $client;
63+
$this->model = $model;
64+
}
65+
66+
/**
67+
* @param \Illuminate\Http\Request $request
68+
* @return $this
69+
*/
70+
public function store(Request $request)
71+
{
72+
if ($request->isJson()) {
73+
$asset = $this->uploadFromUrl([
74+
'url' => $request->get('url'),
75+
'user' => $request->user(),
76+
]);
77+
} else {
78+
$body = ! (base64_decode($request->getContent())) ? $request->getContent() : base64_decode($request->getContent());
79+
$asset = $this->uploadFromDirectFile([
80+
'mime' => $request->header('Content-Type'),
81+
'content' => $body,
82+
'Content-Length' => $request->header('Content-Length'),
83+
'user' => $request->user(),
84+
]);
85+
}
86+
87+
event(new AssetWasCreated($asset));
88+
89+
return $this->response->item($asset, new AssetTransformer())->setStatusCode(201);
90+
}
91+
92+
/**
93+
* @param array $attributes
94+
* @return mixed
95+
*/
96+
protected function uploadFromDirectFile($attributes = [])
97+
{
98+
$this->validateMime($attributes['mime']);
99+
$this->validateBodySize($attributes['Content-Length'], $attributes['content']);
100+
$path = $this->storeInFileSystem($attributes);
101+
102+
return $this->storeInDatabase($attributes, $path);
103+
}
104+
105+
/**
106+
* @param array $attributes
107+
* @return mixed
108+
*/
109+
protected function uploadFromUrl($attributes = [])
110+
{
111+
$response = $this->callFileUrl($attributes['url']);
112+
$attributes['mime'] = $response->getHeader('content-type')[0];
113+
$this->validateMime($attributes['mime']);
114+
$attributes['content'] = $response->getBody();
115+
$path = $this->storeInFileSystem($attributes);
116+
117+
return $this->storeInDatabase($attributes, $path);
118+
}
119+
120+
/**
121+
* @param array $attributes
122+
* @param $path
123+
* @return mixed
124+
*/
125+
protected function storeInDatabase(array $attributes, $path)
126+
{
127+
$file = $this->model->create([
128+
'type' => $this->validMimes[$attributes['mime']]['type'],
129+
'path' => $path,
130+
'mime' => $attributes['mime'],
131+
'user_id' => ! empty($attributes['user']) ? $attributes['user']->id : null,
132+
]);
133+
134+
return $file;
135+
}
136+
137+
/**
138+
* @param array $attributes
139+
* @return string
140+
*/
141+
protected function storeInFileSystem(array $attributes)
142+
{
143+
$path = md5(str_random(16).date('U')).'.'.$this->validMimes[$attributes['mime']]['extension'];
144+
Storage::put($path, $attributes['content']);
145+
146+
return $path;
147+
}
148+
149+
/**
150+
* @param $url
151+
* @return \Psr\Http\Message\ResponseInterface
152+
*/
153+
protected function callFileUrl($url)
154+
{
155+
try {
156+
$response = $this->client->get($url);
157+
if ($response->getStatusCode() != 200) {
158+
throw new StoreResourceFailedException("Validation Error", [
159+
'url' => "The url seems unreachable",
160+
]);
161+
}
162+
163+
return $response;
164+
} catch (TransferException $e) {
165+
throw new StoreResourceFailedException("Validation Error", [
166+
'url' => 'The url seems to be unreachable: '.$e->getCode(),
167+
]);
168+
}
169+
}
170+
171+
/**
172+
* @param $mime
173+
*/
174+
protected function validateMime($mime)
175+
{
176+
if (! array_key_exists($mime, $this->validMimes)) {
177+
throw new StoreResourceFailedException("Validation Error", [
178+
'Content-Type' => 'The Content Type sent is not valid',
179+
]);
180+
}
181+
}
182+
183+
/**
184+
* @param $contentLength
185+
* @param $content
186+
* @throws \App\Exceptions\BodyTooLargeException
187+
*/
188+
protected function validateBodySize($contentLength, $content)
189+
{
190+
if ($contentLength > config('files.maxsize', 1000000)) {
191+
throw new BodyTooLargeException();
192+
}
193+
if (mb_strlen($content) > config('files.maxsize', 1000000)) {
194+
throw new BodyTooLargeException();
195+
}
196+
}
197+
}

app/Providers/ErrorHandlerServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Dingo\Api\Routing\Helpers;
66
use Illuminate\Support\ServiceProvider;
7+
use App\Exceptions\BodyTooLargeException;
78
use Illuminate\Auth\AuthenticationException;
89
use Illuminate\Database\Eloquent\ModelNotFoundException;
910

@@ -35,5 +36,8 @@ public function register()
3536
app('Dingo\Api\Exception\Handler')->register(function (ModelNotFoundException $exception) {
3637
return $this->response->errorNotFound('404 Not Found');
3738
});
39+
app('Dingo\Api\Exception\Handler')->register(function (BodyTooLargeException $exception) {
40+
return $this->response->error("The body is too large", 413);
41+
});
3842
}
3943
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace App\Transformers\Assets;
4+
5+
use App\Entities\Assets\Asset;
6+
use League\Fractal\TransformerAbstract;
7+
8+
/**
9+
* Class AssetTransformer
10+
*
11+
* @package App\Transformers\Assets
12+
*/
13+
class AssetTransformer extends TransformerAbstract
14+
{
15+
/**
16+
* @param \App\Entities\Assets\Asset $model
17+
* @return array
18+
*/
19+
public function transform(Asset $model)
20+
{
21+
return [
22+
'id' => $model->uuid,
23+
'type' => $model->type,
24+
'path' => $model->path,
25+
'mime' => $model->mime,
26+
'created_at' => $model->created_at->toIso8601String(),
27+
];
28+
}
29+
30+
31+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Schema;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Database\Migrations\Migration;
6+
7+
class CreateAssetsTable extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('assets', function (Blueprint $table) {
17+
$table->increments('id');
18+
$table->integer('user_id')->unsigned()->nullable();
19+
$table->uuid('uuid');
20+
$table->string('type', 45)->nullable();
21+
$table->string('path', 45)->nullable();
22+
$table->string('mime', 45)->nullable();
23+
$table->timestamps();
24+
$table->softDeletes();
25+
$table->foreign('user_id')->references('id')->on('users');
26+
});
27+
}
28+
29+
/**
30+
* Reverse the migrations.
31+
*
32+
* @return void
33+
*/
34+
public function down()
35+
{
36+
Schema::dropIfExists('assets');
37+
}
38+
}

docs/api/blueprint/apidocs.apib

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,7 @@ The API uses conventional HTTP response codes to indicate the success or failure
4545
<!-- include(dataStructures/users.apib) -->
4646
<!-- include(dataStructures/roles.apib) -->
4747
<!-- include(dataStructures/permissions.apib) -->
48+
<!-- include(dataStructures/assets.apib) -->
4849

49-
<!-- include(routes/users.apib) -->
50+
<!-- include(routes/users.apib) -->
51+
<!-- include(routes/assets.apib) -->
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Asset Object (object)
2+
+ id: `08279320-dfc9-11e5-a450-0002a5d5c51b`(string) - The asset ID
3+
+ mime: `image/jpeg` (string) - The asset mime type
4+
+ type: `image` (string) - The asset type
5+
+ path: `12342bh3y3bn3i4i5ii5b43o3n.jpg` (string) - The asset path in the storage
6+
+ created_at : `1997-07-16T19:20:30+01:00` (string) - Date in format iso 8601
7+

docs/api/blueprint/routes/assets.apib

Lines changed: 41 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)