From 4ec361c7183547cd90e67c8c86e87055e55605be Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:49:51 +0200 Subject: [PATCH 1/4] Add AssetModel files endpoints to API --- .../Api/AssetModelFilesController.php | 200 ++++++++++++++++++ resources/lang/en-GB/admin/models/message.php | 6 + routes/api.php | 33 +++ .../AssetModels/Api/AssetModelFilesTest.php | 120 +++++++++++ 4 files changed, 359 insertions(+) create mode 100644 app/Http/Controllers/Api/AssetModelFilesController.php create mode 100644 tests/Feature/AssetModels/Api/AssetModelFilesTest.php diff --git a/app/Http/Controllers/Api/AssetModelFilesController.php b/app/Http/Controllers/Api/AssetModelFilesController.php new file mode 100644 index 000000000..9d17de4ae --- /dev/null +++ b/app/Http/Controllers/Api/AssetModelFilesController.php @@ -0,0 +1,200 @@ + + * + * @version v1.0 + * @author [T. Scarsbrook] [] + */ +class AssetModelFilesController extends Controller +{ + /** + * Accepts a POST to upload a file to the server. + * + * @param \App\Http\Requests\UploadFileRequest $request + * @param int $assetModelId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function store(UploadFileRequest $request, $assetModelId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // Make sure we are allowed to update this asset + $this->authorize('update', $asset); + + if ($request->hasFile('file')) { + // If the file storage directory doesn't exist; create it + if (! Storage::exists('private_uploads/assetmodels')) { + Storage::makeDirectory('private_uploads/assetmodels', 775); + } + + // Loop over the attached files and add them to the asset + foreach ($request->file('file') as $file) { + $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$asset->id, $file); + + $asset->logUpload($file_name, e($request->get('notes'))); + } + + // All done - report success + return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/models/message.upload.success'))); + } + + // We only reach here if no files were included in the POST, so tell the user this + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500); + } + + /** + * List the files for an asset. + * + * @param int $assetModelId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function list($assetModelId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('view', $assetModel); + + // Check that there are some uploads on this asset that can be listed + if ($assetModel->uploads->count() > 0) { + $files = array(); + foreach ($assetModel->uploads as $upload) { + array_push($files, $upload); + } + // Give the list of files back to the user + return response()->json(Helper::formatStandardApiResponse('success', $files, trans('admin/models/message.upload.success'))); + } + + // There are no files. + return response()->json(Helper::formatStandardApiResponse('success', array(), trans('admin/models/message.upload.success'))); + } + + // Send back an error message + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error')), 500); + } + + /** + * Check for permissions and display the file. + * + * @param int $assetModelId + * @param int $fileId + * @return \Illuminate\Http\JsonResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + * @since [v7.0.12] + * @author [r-xyz] + */ + public function show($assetModelId = null, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('view', $assetModel); + + // Check that the file being requested exists for the asset + if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $assetModel->id)->find($fileId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.no_match', ['id' => $fileId])), 404); + } + + // Form the full filename with path + $file = 'private_uploads/assetmodels/'.$log->filename; + Log::debug('Checking for '.$file); + + if ($log->action_type == 'audit') { + $file = 'private_uploads/audits/'.$log->filename; + } + + // Check the file actually exists on the filesystem + if (! Storage::exists($file)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.does_not_exist', ['id' => $fileId])), 404); + } + + if (request('inline') == 'true') { + + $headers = [ + 'Content-Disposition' => 'inline', + ]; + + return Storage::download($file, $log->filename, $headers); + } + + return StorageHelper::downloader($file); + } + + // Send back an error message + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.download.error', ['id' => $fileId])), 500); + } + + /** + * Delete the associated file + * + * @param int $assetModelId + * @param int $fileId + * @since [v7.0.12] + * @author [r-xyz] + */ + public function destroy($assetModelId = null, $fileId = null) : JsonResponse + { + // Start by checking if the asset being acted upon exists + if (! $assetModel = AssetModel::find($assetModelId)) { + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404); + } + + $rel_path = 'private_uploads/assetmodels'; + + // the asset is valid + if (isset($assetModel->id)) { + $this->authorize('update', $assetModel); + + // Check for the file + $log = Actionlog::find($fileId); + if ($log) { + // Check the file actually exists, and delete it + if (Storage::exists($rel_path.'/'.$log->filename)) { + Storage::delete($rel_path.'/'.$log->filename); + } + // Delete the record of the file + $log->delete(); + + // All deleting done - notify the user of success + return response()->json(Helper::formatStandardApiResponse('success', null, trans('admin/models/message.deletefile.success')), 200); + } + + // The file doesn't seem to really exist, so report an error + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500); + } + + return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.deletefile.error')), 500); + } +} diff --git a/resources/lang/en-GB/admin/models/message.php b/resources/lang/en-GB/admin/models/message.php index ae3bc34ee..18a3fb10c 100644 --- a/resources/lang/en-GB/admin/models/message.php +++ b/resources/lang/en-GB/admin/models/message.php @@ -43,5 +43,11 @@ return array( 'success' => 'Model deleted!|:success_count models deleted!', 'success_partial' => ':success_count model(s) were deleted, however :fail_count were unable to be deleted because they still have assets associated with them.' ), + 'download' => [ + 'error' => 'File(s) not downloaded. Please try again.', + 'success' => 'File(s) successfully downloaded.', + 'does_not_exist' => 'No file exists', + 'no_match' => 'No matching record for that asset/file', + ], ); diff --git a/routes/api.php b/routes/api.php index 0eb0d834c..b57ed0897 100644 --- a/routes/api.php +++ b/routes/api.php @@ -791,6 +791,12 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ] )->name('api.models.assets'); + Route::post('{id}/restore', + [ + Api\AssetModelsController::class, + 'restore' + ] + )->name('api.models.restore'); Route::post('{id}/restore', [ Api\AssetModelsController::class, @@ -798,6 +804,33 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ] )->name('api.models.restore'); + Route::post('{model_id}/files', + [ + Api\AssetModelFilesController::class, + 'store' + ] + )->name('api.models.files'); + + Route::get('{model_id}/files', + [ + Api\AssetModelFilesController::class, + 'list' + ] + )->name('api.models.files'); + + Route::get('{model_id}/file/{file_id}', + [ + Api\AssetModelFilesController::class, + 'show' + ] + )->name('api.models.assets.file'); + + Route::delete('{model_id}/file/{file_id}', + [ + Api\AssetModelFilesController::class, + 'destroy' + ] + )->name('api.models.file'); }); Route::resource('models', diff --git a/tests/Feature/AssetModels/Api/AssetModelFilesTest.php b/tests/Feature/AssetModels/Api/AssetModelFilesTest.php new file mode 100644 index 000000000..c22609c0c --- /dev/null +++ b/tests/Feature/AssetModels/Api/AssetModelFilesTest.php @@ -0,0 +1,120 @@ +count(1)->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + } + + public function testAssetModelApiListsFiles() + { + // List all files on a model + + // Create an model to work with + $model = AssetModel::factory()->count(1)->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + // List the files + $this->actingAsForApi($user) + ->getJson( + route('api.models.files.index', ['model_id' => $model[0]["id"]])) + ->assertOk() + ->assertJsonStructure([ + 'status', + 'messages', + 'payload', + ]); + } + + public function testAssetModelApiDownloadsFile() + { + // Download a file from a model + + // Create a model to work with + $model = AssetModel::factory()->count(1)->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.models.files.index', ['model_id' => $model[0]["id"]])) + ->assertOk(); + + // Get the file + $this->actingAsForApi($user) + ->get( + route('api.models.files.show', [ + 'model_id' => $model[0]["id"], + 'file_id' => $result->decodeResponseJson()->json()["payload"][0]["id"], + ])) + ->assertOk(); + } + + public function testAssetModelApiDeletesFile() + { + // Delete a file from a model + + // Create a model to work with + $model = AssetModel::factory()->count(1)->create(); + + // Create a superuser to run this as + $user = User::factory()->superuser()->create(); + + //Upload a file + $this->actingAsForApi($user) + ->post( + route('api.models.files.store', ['model_id' => $model[0]["id"]]), [ + 'file' => [UploadedFile::fake()->create("test.jpg", 100)] + ]) + ->assertOk(); + + // List the files to get the file ID + $result = $this->actingAsForApi($user) + ->getJson( + route('api.models.files.index', ['model_id' => $model[0]["id"]])) + ->assertOk(); + + // Delete the file + $this->actingAsForApi($user) + ->delete( + route('api.models.files.destroy', [ + 'model_id' => $model[0]["id"], + 'file_id' => $result->decodeResponseJson()->json()["payload"][0]["id"], + ])) + ->assertOk(); + } +} From da7313bc9d2ee8614967abaf601782fe85e35113 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:24:22 +0200 Subject: [PATCH 2/4] Fix models files API routes. --- routes/api.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/routes/api.php b/routes/api.php index b57ed0897..b4d5b5d52 100644 --- a/routes/api.php +++ b/routes/api.php @@ -809,28 +809,28 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi Api\AssetModelFilesController::class, 'store' ] - )->name('api.models.files'); + )->name('api.models.files.store'); Route::get('{model_id}/files', [ Api\AssetModelFilesController::class, 'list' ] - )->name('api.models.files'); + )->name('api.models.files.index'); Route::get('{model_id}/file/{file_id}', [ Api\AssetModelFilesController::class, 'show' ] - )->name('api.models.assets.file'); + )->name('api.models.files.show'); Route::delete('{model_id}/file/{file_id}', [ Api\AssetModelFilesController::class, 'destroy' ] - )->name('api.models.file'); + )->name('api.models.files.destroy'); }); Route::resource('models', From cd7db5c4a8448239b632507ea53a25545b41d972 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:24:08 +0200 Subject: [PATCH 3/4] Fix some typos in models file handler. --- app/Http/Controllers/Api/AssetModelFilesController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/AssetModelFilesController.php b/app/Http/Controllers/Api/AssetModelFilesController.php index 9d17de4ae..90d283f72 100644 --- a/app/Http/Controllers/Api/AssetModelFilesController.php +++ b/app/Http/Controllers/Api/AssetModelFilesController.php @@ -42,7 +42,7 @@ class AssetModelFilesController extends Controller } // Make sure we are allowed to update this asset - $this->authorize('update', $asset); + $this->authorize('update', $assetModel); if ($request->hasFile('file')) { // If the file storage directory doesn't exist; create it @@ -52,13 +52,13 @@ class AssetModelFilesController extends Controller // Loop over the attached files and add them to the asset foreach ($request->file('file') as $file) { - $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$asset->id, $file); + $file_name = $request->handleFile('private_uploads/assetmodels/','model-'.$assetModel->id, $file); - $asset->logUpload($file_name, e($request->get('notes'))); + $assetModel->logUpload($file_name, e($request->get('notes'))); } // All done - report success - return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/models/message.upload.success'))); + return response()->json(Helper::formatStandardApiResponse('success', $assetModel, trans('admin/models/message.upload.success'))); } // We only reach here if no files were included in the POST, so tell the user this From a8eb76fd8dae5f2ad6b716307b32ba477d31ed32 Mon Sep 17 00:00:00 2001 From: r-xyz <100710244+r-xyz@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:25:41 +0200 Subject: [PATCH 4/4] Fixed model files API routes. --- routes/api.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/routes/api.php b/routes/api.php index b4d5b5d52..35e6c9206 100644 --- a/routes/api.php +++ b/routes/api.php @@ -791,12 +791,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi ] )->name('api.models.assets'); - Route::post('{id}/restore', - [ - Api\AssetModelsController::class, - 'restore' - ] - )->name('api.models.restore'); Route::post('{id}/restore', [ Api\AssetModelsController::class,