From 8a4559f7481028de92032de14aa5ca377d1fcf5e Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Fri, 10 Oct 2025 20:12:04 +0300 Subject: [PATCH 1/4] feat: allow file upload from url --- src/Commands/DevCommand.php | 64 ++++++++++++++++++++ src/Fields/FieldCollection.php | 7 +++ src/Fields/File.php | 42 ++++++++++--- src/MCP/Actions/SchemaAttributes.php | 6 +- src/MCP/Concerns/FieldMcpSchemaDetection.php | 15 ++--- src/MCP/Concerns/McpStoreTool.php | 36 +++++++++++ 6 files changed, 153 insertions(+), 17 deletions(-) diff --git a/src/Commands/DevCommand.php b/src/Commands/DevCommand.php index 260f4aadd..e55ebca7d 100644 --- a/src/Commands/DevCommand.php +++ b/src/Commands/DevCommand.php @@ -13,6 +13,7 @@ class DevCommand extends Command protected $signature = 'restify:dev {--path= : The path to the root local directory.} {--git : Use the latest vcs git repository} + {--revert : Revert composer.json to remove local development setup} '; protected $description = 'Add laravel-restify from a local directory.'; @@ -23,6 +24,10 @@ public function handle() return true; } + if ($this->option('revert')) { + return $this->revert(); + } + $this->addRepositoryToRootComposer(); $this->info('Added local path to repositories.'); @@ -127,4 +132,63 @@ private function resolveDefaultPath() ? 'git@github.com:BinarCode/laravel-restify.git' : '../../binarcode/laravel-restify'; } + + protected function revert(): int + { + $this->removeRepositoryFromComposer(); + + $this->info('Removed local path from repositories.'); + + $this->restorePackageVersion(); + + $this->info('Restored package version in composer.json.'); + + $this->composerUpdate(); + + $this->info('Composer updated. Development setup reverted.'); + + return 0; + } + + protected function removeRepositoryFromComposer(): void + { + $composer = json_decode(file_get_contents(base_path('composer.json')), true); + + if (! array_key_exists('repositories', $composer)) { + return; + } + + $composer['repositories'] = collect($composer['repositories'])->filter(function ($repository) { + if (! array_key_exists('url', $repository)) { + return true; + } + + return ! Str::contains($repository['url'], 'laravel-restify'); + })->values()->toArray(); + + if (empty($composer['repositories'])) { + unset($composer['repositories']); + } + + file_put_contents( + base_path('composer.json'), + json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + } + + protected function restorePackageVersion(): void + { + $composer = json_decode(file_get_contents(base_path('composer.json')), true); + + if (! array_key_exists('require', $composer) || ! array_key_exists('binaryk/laravel-restify', $composer['require'])) { + return; + } + + $composer['require']['binaryk/laravel-restify'] = '^10.0'; + + file_put_contents( + base_path('composer.json'), + json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) + ); + } } diff --git a/src/Fields/FieldCollection.php b/src/Fields/FieldCollection.php index db50df12e..b301aa7b8 100644 --- a/src/Fields/FieldCollection.php +++ b/src/Fields/FieldCollection.php @@ -198,4 +198,11 @@ public function inList(array $columns = []): self ->filter(fn (Field $field) => in_array($field->getAttribute(), $columns, true)) ->values(); } + + public function areFiles(): self + { + return $this + ->filter(fn (Field $field) => $field instanceof File) + ->values(); + } } diff --git a/src/Fields/File.php b/src/Fields/File.php index c235c3540..6fe38273a 100644 --- a/src/Fields/File.php +++ b/src/Fields/File.php @@ -12,7 +12,9 @@ use Carbon\CarbonInterface; use Closure; use Illuminate\Http\Request; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; +use RuntimeException; class File extends Field implements DeletableContract, StorableContract { @@ -81,8 +83,11 @@ public function storeAs($storeAs): self * * @return $this */ - public function resolveUsingTemporaryUrl(bool $resolveTemporaryUrl = true, ?CarbonInterface $expiration = null, array $options = []): self - { + public function resolveUsingTemporaryUrl( + bool $resolveTemporaryUrl = true, + ?CarbonInterface $expiration = null, + array $options = [] + ): self { if (! $resolveTemporaryUrl) { return $this; } @@ -161,13 +166,32 @@ public function storeSize($column) return $this; } + protected function resolveFileFromRequest(Request $request): ?UploadedFile + { + if (($file = $request->input($this->attribute)) instanceof UploadedFile && $file->isValid()) { + return $file; + } + + if (($file = $request->file($this->attribute)) && $file->isValid()) { + return $file; + } + + return null; + } + protected function storeFile(Request $request, string $requestAttribute) { + $file = $this->resolveFileFromRequest($request); + + if (! $file) { + throw new RuntimeException("No valid file found in the request for attribute {$requestAttribute}"); + } + if (! $this->storeAs) { - return $request->file($requestAttribute)->store($this->getStorageDir(), $this->getStorageDisk()); + return $file->store($this->getStorageDir(), $this->getStorageDisk()); } - return $request->file($requestAttribute)->storeAs( + return $file->storeAs( $this->getStorageDir(), is_callable($this->storeAs) ? call_user_func($this->storeAs, $request) : $this->storeAs, $this->getStorageDisk() @@ -196,7 +220,7 @@ public function store($storageCallback): self */ protected function mergeExtraStorageColumns($request, array $attributes): array { - $file = $request->file($this->attribute); + $file = $this->resolveFileFromRequest($request); if ($this->originalNameColumn) { $attributes[$this->originalNameColumn] = $file->getClientOriginalName(); @@ -229,6 +253,10 @@ protected function columnsThatShouldBeDeleted(): array public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = null) { + if ($this->storeCallback instanceof Closure) { + return call_user_func($this->storeCallback, $request, $model, $this->attribute); + } + // Handle URL input first if ($request->has($this->attribute) && is_string($request->input($this->attribute))) { $url = $request->input($this->attribute); @@ -254,11 +282,11 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n } } - // Existing file upload logic - if (is_null($file = $request->file($this->attribute)) || ! $file->isValid()) { + if (! $this->resolveFileFromRequest($request)) { return $this; } + if ($this->isPrunable()) { call_user_func( $this->deleteCallback, diff --git a/src/MCP/Actions/SchemaAttributes.php b/src/MCP/Actions/SchemaAttributes.php index ab0de0c99..a21713b08 100644 --- a/src/MCP/Actions/SchemaAttributes.php +++ b/src/MCP/Actions/SchemaAttributes.php @@ -955,7 +955,7 @@ public function validateFile(string $attribute, $schema, array $parameters) return $existing; } - return $schema->string()->description('Must be a valid file'); + return $schema->string()->description('Must be a valid file, or file absolute path'); } /** @@ -1266,7 +1266,7 @@ public function validateMimes(string $attribute, $schema, array $parameters) return $schema->string()->description("Allowed file extensions: {$extensions}"); } - return $schema->string()->description('Must be a valid file with allowed extension'); + return $schema->string()->description('Must be a valid file, or file absolute path with allowed extension'); } /** @@ -1289,7 +1289,7 @@ public function validateMimetypes(string $attribute, $schema, array $parameters) return $schema->string()->description("Allowed MIME types: {$types}"); } - return $schema->string()->description('Must be a valid file with allowed MIME type'); + return $schema->string()->description('Must be a valid file, or file absolute path with allowed MIME type'); } /** diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index d0d1edeeb..73621afa3 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -57,9 +57,15 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } + $partialDescription = ''; + + if ($this instanceof File) { + $partialDescription = ' -- Important: This should be an absolute path or URL to the file that can be read.'; + } + if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) { if (is_string($description)) { - return $description; + return $description . ' ' . $partialDescription; } } @@ -78,11 +84,6 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - // Add file information for file fields - if ($this instanceof File) { - $description .= '. Upload a file'; - } - // Add examples based on field type and name if ($this->jsonSchema instanceof Type) { $examples = $this->generateFieldExamples($this->jsonSchema); @@ -92,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - return $description; + return $description . ' ' . $partialDescription; } /** diff --git a/src/MCP/Concerns/McpStoreTool.php b/src/MCP/Concerns/McpStoreTool.php index d113e9c97..7aefba6f0 100644 --- a/src/MCP/Concerns/McpStoreTool.php +++ b/src/MCP/Concerns/McpStoreTool.php @@ -3,7 +3,9 @@ namespace Binaryk\LaravelRestify\MCP\Concerns; use Binaryk\LaravelRestify\Fields\Field; +use Binaryk\LaravelRestify\Fields\File; use Binaryk\LaravelRestify\MCP\Requests\McpStoreRequest; +use Illuminate\Http\UploadedFile; use Illuminate\JsonSchema\JsonSchema; /** @@ -13,6 +15,40 @@ trait McpStoreTool { public function storeTool(McpStoreRequest $request): array { + $this->collectFields($request) + ->forStore($request, $this) + ->areFiles() + ->each(function (File $file) use ($request) { + if (! $request->has($file->attribute)) { + return; + } + + $filePath = $request->input($file->attribute); + $actualPath = null; + $fileName = null; + + if (file_exists($filePath) && is_readable($filePath)) { + $actualPath = $filePath; + $fileName = basename($filePath); + } elseif (filter_var($filePath, FILTER_VALIDATE_URL)) { + $actualPath = tempnam(sys_get_temp_dir(), 'upload_'); + file_put_contents($actualPath, file_get_contents($filePath)); + $fileName = basename(parse_url($filePath, PHP_URL_PATH)); + } + + if ($actualPath) { + $uploadedFile = new UploadedFile( + $actualPath, + $fileName, + mime_content_type($actualPath), + null, + true // Mark it as test mode to allow local files + ); + + $request->merge([$file->attribute => $uploadedFile]); + } + }); + return $this ->allowToStore($request) ->store($request) From 3c8f33efb8f85dfb3e2a02e9d3fff17fbca0fd53 Mon Sep 17 00:00:00 2001 From: binaryk Date: Fri, 10 Oct 2025 17:12:31 +0000 Subject: [PATCH 2/4] Fix styling --- src/Fields/File.php | 1 - src/MCP/Concerns/FieldMcpSchemaDetection.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Fields/File.php b/src/Fields/File.php index 6fe38273a..7934d37ef 100644 --- a/src/Fields/File.php +++ b/src/Fields/File.php @@ -286,7 +286,6 @@ public function fillAttribute(RestifyRequest $request, $model, ?int $bulkRow = n return $this; } - if ($this->isPrunable()) { call_user_func( $this->deleteCallback, diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index 73621afa3..0d2ef05fe 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -65,7 +65,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) { if (is_string($description)) { - return $description . ' ' . $partialDescription; + return $description.' '.$partialDescription; } } @@ -93,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - return $description . ' ' . $partialDescription; + return $description.' '.$partialDescription; } /** From 74cc6745f4d1f468144bb4348b58eb561f02582c Mon Sep 17 00:00:00 2001 From: Eduard Lupacescu Date: Fri, 10 Oct 2025 20:12:53 +0300 Subject: [PATCH 3/4] fix: wip --- src/MCP/Concerns/FieldMcpSchemaDetection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index 73621afa3..4bd57f10d 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -65,7 +65,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) { if (is_string($description)) { - return $description . ' ' . $partialDescription; + return $description . $partialDescription; } } From 04225897ee614a84502d2ca6edb9d761b6d3b875 Mon Sep 17 00:00:00 2001 From: binaryk Date: Fri, 10 Oct 2025 17:13:35 +0000 Subject: [PATCH 4/4] Fix styling --- src/MCP/Concerns/FieldMcpSchemaDetection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index 4bd57f10d..f9986fc3b 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -65,7 +65,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): if ($description = data_get($this->jsonSchema()?->toArray(), 'description')) { if (is_string($description)) { - return $description . $partialDescription; + return $description.$partialDescription; } } @@ -93,7 +93,7 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - return $description . ' ' . $partialDescription; + return $description.' '.$partialDescription; } /**