Skip to content

Commit 80b6385

Browse files
authored
Allow file upload from url (#680)
* feat: allow file upload from url * Fix styling * fix: wip * Fix styling --------- Co-authored-by: binaryk <binaryk@users.noreply.github.com>
1 parent b65eecc commit 80b6385

File tree

6 files changed

+152
-17
lines changed

6 files changed

+152
-17
lines changed

src/Commands/DevCommand.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class DevCommand extends Command
1313

1414
protected $signature = 'restify:dev {--path= : The path to the root local directory.}
1515
{--git : Use the latest vcs git repository}
16+
{--revert : Revert composer.json to remove local development setup}
1617
';
1718

1819
protected $description = 'Add laravel-restify from a local directory.';
@@ -23,6 +24,10 @@ public function handle()
2324
return true;
2425
}
2526

27+
if ($this->option('revert')) {
28+
return $this->revert();
29+
}
30+
2631
$this->addRepositoryToRootComposer();
2732

2833
$this->info('Added local path to repositories.');
@@ -127,4 +132,63 @@ private function resolveDefaultPath()
127132
? 'git@github.com:BinarCode/laravel-restify.git'
128133
: '../../binarcode/laravel-restify';
129134
}
135+
136+
protected function revert(): int
137+
{
138+
$this->removeRepositoryFromComposer();
139+
140+
$this->info('Removed local path from repositories.');
141+
142+
$this->restorePackageVersion();
143+
144+
$this->info('Restored package version in composer.json.');
145+
146+
$this->composerUpdate();
147+
148+
$this->info('Composer updated. Development setup reverted.');
149+
150+
return 0;
151+
}
152+
153+
protected function removeRepositoryFromComposer(): void
154+
{
155+
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
156+
157+
if (! array_key_exists('repositories', $composer)) {
158+
return;
159+
}
160+
161+
$composer['repositories'] = collect($composer['repositories'])->filter(function ($repository) {
162+
if (! array_key_exists('url', $repository)) {
163+
return true;
164+
}
165+
166+
return ! Str::contains($repository['url'], 'laravel-restify');
167+
})->values()->toArray();
168+
169+
if (empty($composer['repositories'])) {
170+
unset($composer['repositories']);
171+
}
172+
173+
file_put_contents(
174+
base_path('composer.json'),
175+
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
176+
);
177+
}
178+
179+
protected function restorePackageVersion(): void
180+
{
181+
$composer = json_decode(file_get_contents(base_path('composer.json')), true);
182+
183+
if (! array_key_exists('require', $composer) || ! array_key_exists('binaryk/laravel-restify', $composer['require'])) {
184+
return;
185+
}
186+
187+
$composer['require']['binaryk/laravel-restify'] = '^10.0';
188+
189+
file_put_contents(
190+
base_path('composer.json'),
191+
json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
192+
);
193+
}
130194
}

src/Fields/FieldCollection.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,11 @@ public function inList(array $columns = []): self
198198
->filter(fn (Field $field) => in_array($field->getAttribute(), $columns, true))
199199
->values();
200200
}
201+
202+
public function areFiles(): self
203+
{
204+
return $this
205+
->filter(fn (Field $field) => $field instanceof File)
206+
->values();
207+
}
201208
}

src/Fields/File.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
use Carbon\CarbonInterface;
1313
use Closure;
1414
use Illuminate\Http\Request;
15+
use Illuminate\Http\UploadedFile;
1516
use Illuminate\Support\Facades\Storage;
17+
use RuntimeException;
1618

1719
class File extends Field implements DeletableContract, StorableContract
1820
{
@@ -81,8 +83,11 @@ public function storeAs($storeAs): self
8183
*
8284
* @return $this
8385
*/
84-
public function resolveUsingTemporaryUrl(bool $resolveTemporaryUrl = true, ?CarbonInterface $expiration = null, array $options = []): self
85-
{
86+
public function resolveUsingTemporaryUrl(
87+
bool $resolveTemporaryUrl = true,
88+
?CarbonInterface $expiration = null,
89+
array $options = []
90+
): self {
8691
if (! $resolveTemporaryUrl) {
8792
return $this;
8893
}
@@ -161,13 +166,32 @@ public function storeSize($column)
161166
return $this;
162167
}
163168

169+
protected function resolveFileFromRequest(Request $request): ?UploadedFile
170+
{
171+
if (($file = $request->input($this->attribute)) instanceof UploadedFile && $file->isValid()) {
172+
return $file;
173+
}
174+
175+
if (($file = $request->file($this->attribute)) && $file->isValid()) {
176+
return $file;
177+
}
178+
179+
return null;
180+
}
181+
164182
protected function storeFile(Request $request, string $requestAttribute)
165183
{
184+
$file = $this->resolveFileFromRequest($request);
185+
186+
if (! $file) {
187+
throw new RuntimeException("No valid file found in the request for attribute {$requestAttribute}");
188+
}
189+
166190
if (! $this->storeAs) {
167-
return $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk());
191+
return $file->store($this->getStorageDir(), $this->getStorageDisk());
168192
}
169193

170-
return $request->file($requestAttribute)->storeAs(
194+
return $file->storeAs(
171195
$this->getStorageDir(),
172196
is_callable($this->storeAs) ? call_user_func($this->storeAs, $request) : $this->storeAs,
173197
$this->getStorageDisk()
@@ -196,7 +220,7 @@ public function store($storageCallback): self
196220
*/
197221
protected function mergeExtraStorageColumns($request, array $attributes): array
198222
{
199-
$file = $request->file($this->attribute);
223+
$file = $this->resolveFileFromRequest($request);
200224

201225
if ($this->originalNameColumn) {
202226
$attributes[$this->originalNameColumn] = $file->getClientOriginalName();
@@ -229,6 +253,10 @@ protected function columnsThatShouldBeDeleted(): array
229253

230254
public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = null)
231255
{
256+
if ($this->storeCallback instanceof Closure) {
257+
return call_user_func($this->storeCallback, $request, $model, $this->attribute);
258+
}
259+
232260
// Handle URL input first
233261
if ($request->has($this->attribute) && is_string($request->input($this->attribute))) {
234262
$url = $request->input($this->attribute);
@@ -254,8 +282,7 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n
254282
}
255283
}
256284

257-
// Existing file upload logic
258-
if (is_null($file = $request->file($this->attribute)) || ! $file->isValid()) {
285+
if (! $this->resolveFileFromRequest($request)) {
259286
return $this;
260287
}
261288

src/MCP/Actions/SchemaAttributes.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -955,7 +955,7 @@ public function validateFile(string $attribute, $schema, array $parameters)
955955
return $existing;
956956
}
957957

958-
return $schema->string()->description('Must be a valid file');
958+
return $schema->string()->description('Must be a valid file, or file absolute path');
959959
}
960960

961961
/**
@@ -1266,7 +1266,7 @@ public function validateMimes(string $attribute, $schema, array $parameters)
12661266
return $schema->string()->description("Allowed file extensions: {$extensions}");
12671267
}
12681268

1269-
return $schema->string()->description('Must be a valid file with allowed extension');
1269+
return $schema->string()->description('Must be a valid file, or file absolute path with allowed extension');
12701270
}
12711271

12721272
/**
@@ -1289,7 +1289,7 @@ public function validateMimetypes(string $attribute, $schema, array $parameters)
12891289
return $schema->string()->description("Allowed MIME types: {$types}");
12901290
}
12911291

1292-
return $schema->string()->description('Must be a valid file with allowed MIME type');
1292+
return $schema->string()->description('Must be a valid file, or file absolute path with allowed MIME type');
12931293
}
12941294

12951295
/**

src/MCP/Concerns/FieldMcpSchemaDetection.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,15 @@ public function getDescription(RestifyRequest $request, Repository $repository):
5757
}
5858
}
5959

60+
$partialDescription = '';
61+
62+
if ($this instanceof File) {
63+
$partialDescription = ' -- Important: This should be an absolute path or URL to the file that can be read.';
64+
}
65+
6066
if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) {
6167
if (is_string($description)) {
62-
return $description;
68+
return $description.$partialDescription;
6369
}
6470
}
6571

@@ -78,11 +84,6 @@ public function getDescription(RestifyRequest $request, Repository $repository):
7884
}
7985
}
8086

81-
// Add file information for file fields
82-
if ($this instanceof File) {
83-
$description .= '. Upload a file';
84-
}
85-
8687
// Add examples based on field type and name
8788
if ($this->jsonSchema instanceof Type) {
8889
$examples = $this->generateFieldExamples($this->jsonSchema);
@@ -92,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository):
9293
}
9394
}
9495

95-
return $description;
96+
return $description.' '.$partialDescription;
9697
}
9798

9899
/**

src/MCP/Concerns/McpStoreTool.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Binaryk\LaravelRestify\MCP\Concerns;
44

55
use Binaryk\LaravelRestify\Fields\Field;
6+
use Binaryk\LaravelRestify\Fields\File;
67
use Binaryk\LaravelRestify\MCP\Requests\McpStoreRequest;
8+
use Illuminate\Http\UploadedFile;
79
use Illuminate\JsonSchema\JsonSchema;
810

911
/**
@@ -13,6 +15,40 @@ trait McpStoreTool
1315
{
1416
public function storeTool(McpStoreRequest $request): array
1517
{
18+
$this->collectFields($request)
19+
->forStore($request, $this)
20+
->areFiles()
21+
->each(function (File $file) use ($request) {
22+
if (! $request->has($file->attribute)) {
23+
return;
24+
}
25+
26+
$filePath = $request->input($file->attribute);
27+
$actualPath = null;
28+
$fileName = null;
29+
30+
if (file_exists($filePath) && is_readable($filePath)) {
31+
$actualPath = $filePath;
32+
$fileName = basename($filePath);
33+
} elseif (filter_var($filePath, FILTER_VALIDATE_URL)) {
34+
$actualPath = tempnam(sys_get_temp_dir(), 'upload_');
35+
file_put_contents($actualPath, file_get_contents($filePath));
36+
$fileName = basename(parse_url($filePath, PHP_URL_PATH));
37+
}
38+
39+
if ($actualPath) {
40+
$uploadedFile = new UploadedFile(
41+
$actualPath,
42+
$fileName,
43+
mime_content_type($actualPath),
44+
null,
45+
true // Mark it as test mode to allow local files
46+
);
47+
48+
$request->merge([$file->attribute => $uploadedFile]);
49+
}
50+
});
51+
1652
return $this
1753
->allowToStore($request)
1854
->store($request)

0 commit comments

Comments
 (0)