Compare commits

...

54 Commits

Author SHA1 Message Date
snipe
fa07f21d11 Commiting this so I don’t lose the history, but will open a smaller PR
Signed-off-by: snipe <snipe@snipe.net>
2025-06-27 10:53:04 +01:00
snipe
f3bfbc888a Nicer formatting for gallery view
Signed-off-by: snipe <snipe@snipe.net>
2025-06-03 03:53:59 +01:00
snipe
5c5405dbd5 Nicer custom view
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:23:55 +01:00
snipe
b0451fc552 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:23:43 +01:00
snipe
94e6e5e210 Removed debugging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:04:54 +01:00
snipe
27c31312d6 Fixed multiple upload for users GUI controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 15:04:45 +01:00
snipe
98eaa9e2da Updated webpack for new custom view library
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:57 +01:00
snipe
82723a3eb6 Added custom view library
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:44 +01:00
snipe
f93089904a Added generic file delete translation
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:30 +01:00
snipe
03efa06f90 Added custom view
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:19 +01:00
snipe
16f316967e Passed object to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:29:04 +01:00
snipe
35cbd58dad Fixed route name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:51 +01:00
snipe
d50bcd6f55 Change spaces to dashes in filename
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:39 +01:00
snipe
83565b2a1e Removed weird conditional
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:23 +01:00
snipe
cc01f15f28 Updated translation
Signed-off-by: snipe <snipe@snipe.net>
2025-05-22 14:28:05 +01:00
snipe
e38ea126d5 Switched order
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:45:37 +01:00
snipe
a9a6a80e04 Switched order in API
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:45:30 +01:00
snipe
53f97680a4 Removed asset files controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:18:29 +01:00
snipe
9be02c62c8 Added use statement
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:16:17 +01:00
snipe
1e4d452a03 Added file upload translations
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:15:47 +01:00
snipe
0a4d6e3631 Added trailing slashes, standardized translation strings
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 16:15:34 +01:00
snipe
d862da2345 Updated tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:18 +01:00
snipe
73ffa3d751 Comments for routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:10 +01:00
snipe
f77ed72111 Added method formatters
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:07:02 +01:00
snipe
702f1d0343 Consolidated file type display
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:06:12 +01:00
snipe
0bacbe175a Fixed path name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:06:01 +01:00
snipe
851cb29fc6 Set note to null if blank
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:05:51 +01:00
snipe
e3e164fa3b Added additional object types
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 15:04:38 +01:00
snipe
7ef83dee56 Fixed routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-20 12:15:31 +01:00
snipe
7183cec43f Allow gifs as inlineable
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 22:06:40 +01:00
snipe
3456c99255 Changed route name
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 22:06:30 +01:00
snipe
966745e852 Added download column to presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:05:52 +01:00
snipe
f9372df48f Use new action log methods for file path and url
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:04:51 +01:00
snipe
3e26acd5cb Added loggable to user model
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:03:39 +01:00
snipe
6cc10b9992 Added two helper methods on action log model
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:03:30 +01:00
snipe
732e125a1d Removed repeated routes
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:59 +01:00
snipe
32f7eae6a3 New uploads controller
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:48 +01:00
snipe
bd635cedba Updated blades
Signed-off-by: snipe <snipe@snipe.net>
2025-05-19 19:02:40 +01:00
snipe
676f94175e Added fields to transformer
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:02:39 +02:00
snipe
35b47cd57f Added method to check file extension
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:02:16 +02:00
snipe
5e1ee05e19 Fixed test
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:59 +02:00
snipe
1413ba0b0b Pass uploads list route
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:52 +02:00
snipe
a182c43d8e Added icon to presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:18 +02:00
snipe
dbd864bf32 Added a search to the listing
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:01:09 +02:00
snipe
0009142b76 Added route comments
Signed-off-by: snipe <snipe@snipe.net>
2025-05-15 17:00:52 +02:00
snipe
4058312eb9 Fixed tests
Signed-off-by: snipe <snipe@snipe.net>
2025-05-14 13:42:19 +02:00
snipe
b65cb967be Added catch for validation exception specifically
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:41:22 +02:00
snipe
b5dc39e70e Removed update_at
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:29:20 +02:00
snipe
b81416456a Nicer error messages on failure for bulk files
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:28:49 +02:00
snipe
2671e8197a Added validation error handler
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:28:35 +02:00
snipe
9cf23d18e2 Removed logging
Signed-off-by: snipe <snipe@snipe.net>
2025-05-13 20:27:21 +02:00
snipe
c5ecc8f839 Use uploads presenter
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:05:05 +01:00
snipe
c1c5814000 Route model binding, gather payload
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:04:24 +01:00
snipe
b68c2e0f11 Route model binding
Signed-off-by: snipe <snipe@snipe.net>
2025-05-11 16:04:07 +01:00
34 changed files with 3485 additions and 799 deletions

View File

@@ -61,14 +61,12 @@ class Handler extends ExceptionHandler
public function render($request, Throwable $e)
{
// CSRF token mismatch error
if ($e instanceof \Illuminate\Session\TokenMismatchException) {
return redirect()->back()->with('error', trans('general.token_expired'));
}
// Invalid JSON exception
// TODO: don't understand why we have to do this when we have the invalidJson() method, below, but, well, whatever
if ($e instanceof JsonException) {
return response()->json(Helper::formatStandardApiResponse('error', null, 'Invalid JSON'), 422);
}
@@ -88,8 +86,9 @@ class Handler extends ExceptionHandler
return redirect()->back()->withInput()->with('error', trans('validation.date', ['attribute' => 'date']));
}
// Handle API requests that fail
if ($request->ajax() || $request->wantsJson()) {
if ($request->ajax() || $request->wantsJson() || ($request->route() && $request->route()->named('api.files.*'))) {
// Handle API requests that fail because Carbon cannot parse the date on validation (when a submitted date value is definitely not a date)
if ($e instanceof InvalidFormatException) {
@@ -102,6 +101,7 @@ class Handler extends ExceptionHandler
return response()->json(Helper::formatStandardApiResponse('error', null, $className . ' not found'), 200);
}
// Handle API requests that fail because of an HTTP status code and return a useful error message
if ($this->isHttpException($e)) {
@@ -116,12 +116,24 @@ class Handler extends ExceptionHandler
return response()->json(Helper::formatStandardApiResponse('error', null, 'Method not allowed'), 405);
default:
return response()->json(Helper::formatStandardApiResponse('error', null, $statusCode), $statusCode);
}
}
// This handles API validation exceptions that happen at the Form Request level, so they
// never even get to the controller where we normally nicely format JSON responses
if ($e instanceof ValidationException) {
$response = $this->invalidJson($request, $e);
return response()->json(Helper::formatStandardApiResponse('error', null, $e->errors()), 200);
}
// return response()->json(Helper::formatStandardApiResponse('error', null, 'Undefined exception'), 200);
}
// This is traaaaash but it handles models that are not found while using route model binding :(
// The only alternative is to set that at *each* route, which is crazypants
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {

View File

@@ -48,17 +48,29 @@ class StorageHelper
'avif',
'webp',
'png',
'gif',
];
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path) && (in_array(pathinfo($file_with_path, PATHINFO_EXTENSION), $allowed_inline))) {
return true;
}
return false;
}
public static function getFiletype($file_with_path) {
// The file exists and is allowed to be displayed inline
if (Storage::exists($file_with_path)) {
return pathinfo($file_with_path, PATHINFO_EXTENSION);
}
return null;
}
/**
* Decide whether to show the file inline or download it.
*/

View File

@@ -1,200 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use App\Http\Transformers\UploadedFilesTransformer;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Illuminate\Http\Request;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
class AssetFilesController extends Controller
{
/**
* Accepts a POST to upload a file to the server.
*
* @param \App\Http\Requests\UploadFileRequest $request
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function store(UploadFileRequest $request, $assetId = null) : JsonResponse
{
// Start by checking if the asset being acted upon exists
if (! $asset = Asset::find($assetId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/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/assets')) {
Storage::makeDirectory('private_uploads/assets', 775);
}
// Loop over the attached files and add them to the asset
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/assets/','hardware-'.$asset->id, $file);
$asset->logUpload($file_name, e($request->get('notes')));
}
// All done - report success
return response()->json(Helper::formatStandardApiResponse('success', $asset, trans('admin/hardware/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/hardware/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function list(Asset $asset, Request $request) : JsonResponse | array
{
$this->authorize('view', $asset);
$allowed_columns =
[
'id',
'filename',
'eol',
'notes',
'created_at',
'updated_at',
];
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')->where('item_type', '=', Asset::class)->where('item_id', '=', $asset->id);
if ($request->filled('search')) {
$files = $files->TextSearch($request->input('search'));
}
// Make sure the offset and limit are actually integers and do not exceed system limits
$offset = ($request->input('offset') > $files->count()) ? $files->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'created_at';
$files = $files->orderBy($sort, $order);
$files = $files->skip($offset)->take($limit)->get();
return (new UploadedFilesTransformer())->transformFiles($files, $files->count());
}
/**
* Check for permissions and display the file.
*
* @param int $assetId
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function show(Asset $asset, $fileId = null) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
// the asset is valid
if (isset($asset->id)) {
$this->authorize('view', $asset);
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')->where('item_id', $asset->id)->find($fileId)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.download.no_match', ['id' => $fileId])), 404);
}
// Form the full filename with path
$file = 'private_uploads/assets/'.$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/hardware/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/hardware/message.download.error', ['id' => $fileId])), 500);
}
/**
* Delete the associated file
*
* @param int $assetId
* @param int $fileId
* @since [v6.0]
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
public function destroy(Asset $asset, $fileId = null) : JsonResponse
{
$rel_path = 'private_uploads/assets';
// the asset is valid
if (isset($asset->id)) {
$this->authorize('update', $asset);
// 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/hardware/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/hardware/message.deletefile.error')), 500);
}
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/hardware/message.deletefile.error')), 500);
}
}

View File

@@ -1,184 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\StorageHelper;
use Illuminate\Support\Facades\Storage;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\AssetModel;
use App\Models\Actionlog;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\AssetModelsTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
/**
* This class controls file related actions related
* to assets for the Snipe-IT Asset Management application.
*
* Based on the Assets/AssetFilesController by A. Gianotto <snipe@snipe.net>
*
* @version v1.0
* @author [T. Scarsbrook] [<snipe@scarzybrook.co.uk>]
*/
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', $assetModel);
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-'.$assetModel->id, $file);
$assetModel->logUpload($file_name, e($request->get('notes')));
}
// All done - report 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
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.upload.nofiles')), 500);
}
/**
* List the files for an asset.
*
* @param int $assetmodel
* @since [v7.0.12]
* @author [r-xyz]
*/
public function list($assetmodel_id) : JsonResponse | array
{
// Start by checking if the asset being acted upon exists
if (! $assetModel = AssetModel::find($assetmodel_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('admin/models/message.does_not_exist')), 404);
}
$assetmodel = AssetModel::with('uploads')->find($assetmodel_id);
$this->authorize('view', $assetmodel);
return (new AssetModelsTransformer)->transformAssetModelFiles($assetmodel, $assetmodel->uploads()->count());
}
/**
* 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);
}
}

View File

@@ -0,0 +1,242 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Http\Controllers\Controller;
use App\Http\Requests\UploadFileRequest;
use App\Http\Transformers\UploadedFilesTransformer;
use App\Models\Accessory;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\AssetModel;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class UploadedFilesController extends Controller
{
static $map_object_type = [
'accessories' => Accessory::class,
'assets' => Asset::class,
'components' => Component::class,
'consumables' => Consumable::class,
'locations' => Location::class,
'models' => AssetModel::class,
'users' => User::class,
];
static $map_storage_path = [
'accessories' => 'private_uploads/accessories/',
'assets' => 'private_uploads/assets/',
'components' => 'private_uploads/components/',
'consumables' => 'private_uploads/consumables/',
'locations' => 'private_uploads/locations/',
'models' => 'private_uploads/assetmodels/',
'users' => 'private_uploads/users/',
];
static $map_file_prefix= [
'accessories' => 'accessory',
'assets' => 'asset',
'components' => 'component',
'consumables' => 'consumable',
'locations' => 'location',
'models' => 'model',
'users' => 'user',
];
/**
* List the files for an object.
*
* @since [v7.0.12]
* @author [r-xyz]
*/
public function index(Request $request, $object_type, $id) : JsonResponse | array
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Columns allowed for sorting
$allowed_columns =
[
'id',
'filename',
'action_type',
'note',
'created_at',
];
$uploads = $object->uploads();
$offset = ($request->input('offset') > $object->count()) ? $object->count() : abs($request->input('offset'));
$limit = app('api_limit_value');
$order = $request->input('order') === 'asc' ? 'asc' : 'desc';
$sort = in_array($request->input('sort'), $allowed_columns) ? $request->input('sort') : 'action_logs.created_at';
// Text search on action_logs fields
// We could use the normal Actionlogs text scope, but it's a very heavy query since it's searcghing across all relations
// And we generally won't need that here
if ($request->filled('search')) {
$uploads->where(function ($query) use ($request) {
$query->where('filename', 'LIKE', '%' . $request->input('search') . '%')
->orWhere('note', 'LIKE', '%' . $request->input('search') . '%');
});
}
$uploads = $uploads->skip($offset)->take($limit)->orderBy($sort, $order)->get();
return (new UploadedFilesTransformer())->transformFiles($uploads, $uploads->count());
}
/**
* 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, $object_type, $id) : JsonResponse
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// If the file storage directory doesn't exist, create it
if (! Storage::exists(self::$map_storage_path[$object_type])) {
Storage::makeDirectory(self::$map_storage_path[$object_type], 775);
}
if ($request->hasFile('file')) {
// Loop over the attached files and add them to the object
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile(self::$map_storage_path[$object_type],self::$map_file_prefix[$object_type].'-'.$object->id, $file);
$files[] = $file_name;
$object->logUpload($file_name, $request->get('notes'));
}
$files = Actionlog::select('action_logs.*')->where('action_type', '=', 'uploaded')
->where('item_type', '=', self::$map_object_type[$object_type])
->where('item_id', '=', $id)->whereIn('filename', $files)
->get();
return response()->json(Helper::formatStandardApiResponse('success', (new UploadedFilesTransformer())->transformFiles($files, count($files)), trans_choice('general.file_upload_status.upload.success', count($files))));
}
// No files were submitted
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.nofiles')));
}
/**
* Check for permissions and display the file.
*
* @param AssetModel $model
* @param int $fileId
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Auth\Access\AuthorizationException
* @since [v7.0.12]
* @author [r-xyz]
*/
public function show($object_type, $id, $file_id) : JsonResponse | StreamedResponse | Storage | StorageHelper | BinaryFileResponse
{
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('view', $object);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check that the file being requested exists for the asset
if (! $log = Actionlog::whereNotNull('filename')
->where('item_type', AssetModel::class)
->where('item_id', $object->id)->find($file_id)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_id')), 404);
}
if (! Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.file_not_found'), 200));
}
if (request('inline') == 'true') {
$headers = [
'Content-Disposition' => 'inline',
];
return Storage::download(self::$map_storage_path[$object_type].'/'.$log->filename, $log->filename, $headers);
}
return StorageHelper::downloader(self::$map_storage_path[$object_type].'/'.$log->filename);
}
/**
* Delete the associated file
*
* @param AssetModel $model
* @param int $fileId
* @since [v7.0.12]
* @author [r-xyz]
*/
public function destroy($object_type, $id, $file_id) : JsonResponse
{
\Log::error('destroy called for '.$object_type.' with id '.$id.' and file_id '.$file_id);
$object = self::$map_object_type[$object_type]::find($id);
$this->authorize('update', self::$map_object_type[$object_type]);
if (!$object) {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('general.file_upload_status.invalid_object')));
}
// Check for the file
$log = Actionlog::find($file_id)->where('item_type', self::$map_object_type[$object_type])
->where('item_id', $object->id)->first();
if ($log) {
// Check the file actually exists, and delete it
if (Storage::exists(self::$map_storage_path[$object_type].'/'.$log->filename)) {
Storage::delete(self::$map_storage_path[$object_type].'/'.$log->filename);
}
// Delete the record of the file
if ($log->delete()) {
return response()->json(Helper::formatStandardApiResponse('success', null, trans_choice('general.file_upload_status.delete.success', 1)), 200);
}
}
// The file doesn't seem to really exist, so report an error
return response()->json(Helper::formatStandardApiResponse('error', null, trans_choice('general.file_upload_status.delete.error', 1)), 500);
}
}

View File

@@ -45,7 +45,7 @@ class AssetFilesController extends Controller
return redirect()->back()->withFragment('files')->with('success', trans('admin/hardware/message.upload.success'));
}
return redirect()->back()->with('error', trans('admin/hardware/message.upload.nofiles'));
return redirect()->back()->with('error', trans('general.file_upload_status.nofiles'));
}
/**

View File

@@ -25,32 +25,25 @@ class UserFilesController extends Controller
public function store(UploadFileRequest $request, User $user)
{
$this->authorize('update', $user);
$files = $request->file('file');
if (is_null($files)) {
return redirect()->back()->with('error', trans('admin/users/message.upload.nofiles'));
}
foreach ($files as $file) {
$file_name = $request->handleFile('private_uploads/users/', 'user-'.$user->id, $file);
if ($request->hasFile('file')) {
//Log the uploaded file to the log
$logAction = new Actionlog();
$logAction->item_id = $user->id;
$logAction->item_type = User::class;
$logAction->created_by = auth()->id();
$logAction->note = $request->input('notes');
$logAction->target_id = null;
$logAction->created_at = date("Y-m-d H:i:s");
$logAction->filename = $file_name;
$logAction->action_type = 'uploaded';
if (! $logAction->save()) {
return JsonResponse::create(['error' => 'Failed validation: '.print_r($logAction->getErrors(), true)], 500);
if (! Storage::exists('private_uploads/users')) {
Storage::makeDirectory('private_uploads/users', 775);
}
return redirect()->back()->withFragment('files')->with('success', trans('admin/users/message.upload.success'));
$file_count = 0;
foreach ($request->file('file') as $file) {
$file_name = $request->handleFile('private_uploads/users/','hardware-'.$user->id, $file);
$user->logUpload($file_name, $request->get('notes'));
$file_count++;
}
return redirect()->back()->withFragment('files')->with('success', trans_choice('general.file_upload_status.upload.success', $file_count));
}
return redirect()->back()->with('error', trans('general.file_upload_status.nofiles'));
}

View File

@@ -6,6 +6,7 @@ use App\Http\Traits\ConvertsBase64ToFiles;
use enshrined\svgSanitize\Sanitizer;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use \App\Helpers\Helper;
class UploadFileRequest extends Request
{
@@ -27,7 +28,7 @@ class UploadFileRequest extends Request
*/
public function rules()
{
$max_file_size = \App\Helpers\Helper::file_upload_max_size();
$max_file_size = Helper::file_upload_max_size();
return [
'file.*' => 'required|mimes:png,gif,jpg,svg,jpeg,doc,docx,pdf,txt,zip,rar,xls,xlsx,lic,xml,rtf,json,webp,avif|max:'.$max_file_size,
@@ -37,34 +38,52 @@ class UploadFileRequest extends Request
/**
* Sanitizes (if needed) and Saves a file to the appropriate location
* Returns the 'short' (storage-relative) filename
*
* TODO - this has a lot of similarities to UploadImageRequest's handleImage; is there
* a way to merge them or extend one into the other?
*/
public function handleFile(string $dirname, string $name_prefix, $file): string
{
$extension = $file->getClientOriginalExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_slug(basename($file->getClientOriginalName(), '.'.$extension)).'.'.$file->guessExtension();
$file_name = $name_prefix.'-'.str_random(8).'-'.str_replace(' ', '-', $file->getClientOriginalName());
// Check for SVG and sanitize it
if ($file->getMimeType() === 'image/svg+xml') {
Log::debug('This is an SVG');
Log::debug($file_name);
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
$cleanSVG = $sanitizer->sanitize($dirtySVG);
try {
Storage::put($dirname.$file_name, $cleanSVG);
} catch (\Exception $e) {
Log::debug('Upload no workie :( ');
Log::debug($e);
}
$uploaded_file = $this->handleSVG($file);
} else {
$put_results = Storage::put($dirname.$file_name, file_get_contents($file));
$uploaded_file = file_get_contents($file);
}
try {
Storage::put($dirname.$file_name, $uploaded_file);
} catch (\Exception $e) {
Log::debug($e);
}
return $file_name;
}
public function handleSVG($file) {
$sanitizer = new Sanitizer();
$dirtySVG = file_get_contents($file->getRealPath());
return $sanitizer->sanitize($dirtySVG);
}
/**
* Get the validation error messages that apply to the request, but
* replace the attribute name with the name of the file that was attempted and failed
* to make it clearer to the user which file is the bad one.
* @return array
*/
public function attributes(): array
{
$attributes = [];
if ($this->file) {
for ($i = 0; $i < count($this->file); $i++) {
$attributes['file.'.$i] = $this->file[$i]->getClientOriginalName();
}
}
return $attributes;
}
}

View File

@@ -4,6 +4,10 @@ namespace App\Http\Transformers;
class DatatablesTransformer
{
/**
* Transform data for bootstrap tables and API responses for lists of things
**/
public function transformDatatables($objects, $total = null)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
@@ -11,4 +15,15 @@ class DatatablesTransformer
return $objects_array;
}
/**
* Transform data for returning the status of items within a bulk action
**/
public function transformBulkResponseWithStatusAndObjects($objects, $total)
{
(isset($total)) ? $objects_array['total'] = $total : $objects_array['total'] = count($objects);
$objects_array['rows'] = $objects;
return $objects_array;
}
}

View File

@@ -3,10 +3,10 @@
namespace App\Http\Transformers;
use App\Helpers\Helper;
use App\Helpers\StorageHelper;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Support\Facades\Gate;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
class UploadedFilesTransformer
@@ -26,23 +26,26 @@ class UploadedFilesTransformer
{
$snipeModel = $file->item_type;
// This will be used later as we extend out this transformer to handle more types of uploads
if ($file->item_type == Asset::class) {
$file_url = route('show/assetfile', [$file->item_id, $file->id]);
}
$array = [
'id' => (int) $file->id,
'icon' => Helper::filetype_icon($file->filename),
'name' => e($file->filename),
'item' => ($file->item_type) ? [
'id' => (int) $file->item_id,
'type' => strtolower(class_basename($file->item_type)),
] : null,
'filename' => e($file->filename),
'url' => $file_url,
'filetype' => StorageHelper::getFiletype($file->uploads_file_path()),
'url' => $file->uploads_file_url(),
'note' => ($file->note) ? e($file->note) : null,
'created_by' => ($file->adminuser) ? [
'id' => (int) $file->adminuser->id,
'name'=> e($file->adminuser->present()->fullName),
] : null,
'created_at' => Helper::getFormattedDateObject($file->created_at, 'datetime'),
'updated_at' => Helper::getFormattedDateObject($file->updated_at, 'datetime'),
'deleted_at' => Helper::getFormattedDateObject($file->deleted_at, 'datetime'),
'inline' => StorageHelper::allowSafeInline($file->uploads_file_path()),
'exists_on_disk' => (Storage::exists($file->uploads_file_path()) ? true : false),
];
$permissions_array['available_actions'] = [
@@ -53,4 +56,5 @@ class UploadedFilesTransformer
return $array;
}
}

View File

@@ -444,6 +444,62 @@ class Actionlog extends SnipeModel
}
public function uploads_file_url()
{
switch ($this->item_type) {
case Accessory::class:
return route('show/accessoryfile', [$this->item_id, $this->id]);
case Asset::class:
return route('show/assetfile', [$this->item_id, $this->id]);
case AssetModel::class:
return route('show/modelfile', [$this->item_id, $this->id]);
case Consumable::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case Component::class:
return route('show/componentsfile', [$this->item_id, $this->id]);
case License::class:
return route('show/licensesfile', [$this->item_id, $this->id]);
case Location::class:
return route('show/locationsfile', [$this->item_id, $this->id]);
case User::class:
return route('show/userfile', [$this->item_id, $this->id]);
default:
return null;
}
}
public function uploads_file_path()
{
switch ($this->item_type) {
case Accessory::class:
return 'private_uploads/accessories/'.$this->filename;
case Asset::class:
return 'private_uploads/assets/'.$this->filename;
case AssetModel::class:
return 'private_uploads/assetmodels/'.$this->filename;
case Consumable::class:
return 'private_uploads/consumables/'.$this->filename;
case Component::class:
return 'private_uploads/components/'.$this->filename;
case License::class:
return 'private_uploads/licenses/'.$this->filename;
case Location::class:
return 'private_uploads/locations/'.$this->filename;
case User::class:
return 'private_uploads/users/'.$this->filename;
default:
return null;
}
}
// Manually sets $this->source for determineActionSource()
public function setActionSource($source = null): void
{

View File

@@ -34,6 +34,7 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
use Notifiable;
use Presentable;
use Searchable;
use Loggable;
protected $hidden = ['password', 'remember_token', 'permissions', 'reset_password_code', 'persist_code'];
protected $table = 'users';

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Presenters;
/**
* Class AccessoryPresenter
*/
class UploadsPresenter extends Presenter
{
/**
* Json Column Layout for bootstrap table
* @return string
*/
public static function dataTableLayout($object)
{
if ($object =='assets') {
$object = 'hardware';
}
$layout = [
[
'field' => 'id',
'searchable' => false,
'sortable' => true,
'switchable' => true,
'title' => trans('general.id'),
'visible' => false,
],
[
'field' => 'icon',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('general.type'),
'formatter' => 'iconFormatter',
],
[
'field' => 'image',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.image'),
'formatter' => 'inlineImageFormatter',
],
[
'field' => 'filename',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.file_name'),
'visible' => true,
'formatter' => 'fileUploadNameFormatter',
],
[
'field' => 'download',
'searchable' => false,
'sortable' => false,
'switchable' => true,
'title' => trans('general.download'),
'visible' => true,
'formatter' => 'downloadOrOpenInNewWindowFormatter',
],
[
'field' => 'note',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.notes'),
'visible' => true,
],
[
'field' => 'created_by',
'searchable' => false,
'sortable' => true,
'title' => trans('general.created_by'),
'visible' => false,
'formatter' => 'usersLinkObjFormatter',
],
[
'field' => 'created_at',
'searchable' => true,
'sortable' => true,
'switchable' => true,
'title' => trans('general.created_at'),
'visible' => false,
'formatter' => 'dateDisplayFormatter',
], [
'field' => 'available_actions',
'searchable' => false,
'sortable' => false,
'switchable' => false,
'title' => trans('table.actions'),
'formatter' => 'deleteUploadFormatter',
],
];
return json_encode($layout);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -110,6 +110,6 @@
"/css/dist/skins/skin-yellow.min.css": "/css/dist/skins/skin-yellow.min.css?id=7b315b9612b8fde8f9c5b0ddb6bba690",
"/css/dist/bootstrap-table.css": "/css/dist/bootstrap-table.css?id=54d676a6ea8677dd48f6c4b3041292cf",
"/js/build/vendor.js": "/js/build/vendor.js?id=89dffa552c6e3abe3a2aac6c9c7b466b",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=783d3a8076337744f0176d60e1041ea4",
"/js/dist/bootstrap-table.js": "/js/dist/bootstrap-table.js?id=03bcd02b9ca5e53e1390ee840cd0a561",
"/js/dist/all.js": "/js/dist/all.js?id=8c6d7286f667eeb62a0a28a09851a6c3"
}

View File

@@ -626,4 +626,23 @@ return [
],
],
'file_upload_status' => [
'upload' => [
'success' => 'File successfully uploaded |:count files successfully uploaded',
'error' => 'File upload failed |:count file uploads failed',
],
'delete' => [
'success' => 'File successfully deleted |:count files successfully deleted',
'error' => 'File deletion failed |:count file deletions failed',
],
'file_not_found' => 'The selected file was not found on server',
'invalid_id' => 'That file ID is invalid',
'invalid_object' => 'That object ID is invalid',
'nofiles' => 'No files were included for upload',
'confirm_delete' => 'Are you sure you want to delete this file?',
],
];

View File

@@ -154,6 +154,7 @@
filepath="private_uploads/accessories/"
showfile_routename="show.accessoryfile"
deletefile_routename="delete/accessoryfile"
object_type="accessories"
:object="$accessory" />
</div>
</div>

View File

@@ -0,0 +1,32 @@
{{-- IMPORTANT!!! Make sure there is no newline at the end of this file, or it will break the loaders for the tables --}}
@props([
'route',
'id',
'method',
])
<div {{ $attributes->merge() }} id="dataConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h2 class="modal-title" id="myModalLabel">&nbsp;</h2>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="{{ isset($id) ?? $id }}" role="form"{!! isset($route) ?? ' action="'.route($route).'"' !!}>
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="button" class="btn btn-default pull-left" data-dismiss="modal">
{{ trans('general.cancel') }}
</button>
<button type="submit" class="btn btn-outline" id="dataConfirmOK">
{{ trans('general.yes') }}
</button>
</form>
</div>
</div>
</div>
</div>

View File

@@ -4,139 +4,74 @@
'object',
'showfile_routename',
'deletefile_routename',
'object_type',
])
<!-- begin non-ajaxed file listing table -->
<div class="table-responsive">
<table
data-columns="{{ \App\Presenters\UploadsPresenter::dataTableLayout($object_type) }}"
data-cookie-id-table="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
data-id-table="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
id="{{ str_slug($object->name ?? $object->id) }}UploadsTable"
data-search="true"
data-show-custom-view="true"
data-custom-view="fileGalleryFormatter"
data-show-custom-view-button="true"
data-pagination="true"
data-side-pagination="client"
data-side-pagination="server"
data-show-columns="true"
data-show-fullscreen="true"
data-show-export="true"
data-show-footer="true"
data-toolbar="#upload-toolbar"
data-show-refresh="true"
data-card-view="true"
data-show-toggle="true"
data-sort-order="asc"
data-sort-name="name"
class="table table-striped snipe-table"
data-url="{{ route("api.files.index", ['object_type' => $object_type, 'id' => $object->id]) }}"
data-export-options='{
"fileName": "export-license-uploads-{{ str_slug($object->name) }}-{{ date('Y-m-d') }}",
"fileName": "export-uploads-{{ str_slug($object->name) }}-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","delete","download","icon"]
}'>
<thead>
<tr>
<th data-visible="false" data-field="id" data-sortable="true">
{{trans('general.id')}}
</th>
<th data-visible="true" data-field="type" data-sortable="true">
{{trans('general.file_type')}}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="image">
{{ trans('general.image') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="filename" data-sortable="true">
{{ trans('general.file_name') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="filesize">
{{ trans('general.filesize') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="notes" data-sortable="true">
{{ trans('general.notes') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="download">
{{ trans('general.download') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="created_at" data-sortable="true">
{{ trans('general.created_at') }}
</th>
<th class="col-md-2" data-searchable="true" data-visible="true" data-field="created_by" data-sortable="true">
{{ trans('general.created_by') }}
</th>
<th class="col-md-1" data-searchable="true" data-visible="true" data-field="actions">
{{ trans('table.actions') }}
</th>
</tr>
</thead>
<tbody>
@foreach ($object->uploads as $file)
<tr>
<td>
{{ $file->id }}
</td>
<td data-sort-value="{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}">
@if (Storage::exists($filepath.$file->filename))
<span class="sr-only">{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}</span>
<i class="{{ Helper::filetype_icon($file->filename) }} icon-med" aria-hidden="true" data-tooltip="true" data-title="{{ pathinfo($filepath.$file->filename, PATHINFO_EXTENSION) }}"></i>
@endif
</td>
<td>
@if (($file->filename) && (Storage::exists($filepath.$file->filename)))
@if (Helper::checkUploadIsImage($file->get_src(str_plural(strtolower(class_basename(get_class($object)))))))
<a href="{{ route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) }}" data-toggle="lightbox" data-type="image">
<img src="{{ route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) }}" class="img-thumbnail" style="max-width: 50px;">
</a>
@else
{{ trans('general.preview_not_available') }}
@endif
@else
<x-icon type="x" class="text-danger" />
{{ trans('general.file_not_found') }}
@endif
</td>
<td>
{{ $file->filename }}
</td>
<td data-value="{{ (Storage::exists($filepath.$file->filename)) ? Storage::size($filepath.$file->filename) : '' }}">
{{ (Storage::exists($filepath.$file->filename)) ? Helper::formatFilesizeUnits(Storage::size($filepath.$file->filename)) : '' }}
</td>
<td>
@if ($file->note)
{{ $file->note }}
@endif
</td>
<td style="white-space: nowrap;">
@if ($file->filename)
@if (Storage::exists($filepath.$file->filename))
<a href="{{ route($showfile_routename, [$object->id, $file->id]) }}" class="btn btn-sm btn-default">
<x-icon type="download" />
<span class="sr-only">
{{ trans('general.download') }}
</span>
</a>
<a href="{{ StorageHelper::allowSafeInline($filepath.$file->filename) ? route($showfile_routename, [$object->id, $file->id, 'inline' => 'true']) : '#' }}" class="btn btn-sm btn-default{{ StorageHelper::allowSafeInline($filepath.$file->filename) ? '' : ' disabled' }}" target="_blank">
<x-icon type="external-link" />
</a>
@endif
@endif
</td>
<td>
{{ $file->created_at }}
</td>
<td>
{{ ($file->adminuser) ? $file->adminuser->present()->getFullNameAttribute() : '' }}
</td>
<td>
<a class="btn delete-asset btn-danger btn-sm hidden-print" href="{{ route($deletefile_routename, [$object->id, $file->id]) }}" data-content="Are you sure you wish to delete this file?" data-title="{{ trans('general.delete') }} {{ $file->filename }}?">
<x-icon type="delete" />
<span class="sr-only">{{ trans('general.delete') }}</span>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- this is used by the bootstrap-table partial to format the file gallery -->
<template id="fileGalleryTemplate">
<div class="col-md-4">
<div class="panel panel-%PANEL_CLASS%">
<div class="panel-heading">
<h3 class="panel-title" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<i class="%ICON%"></i> %FILENAME%
</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
%INLINE_IMAGE%
<br>
<p>
<strong>{{ trans('general.created_at') }}:</strong> %CREATED_AT% <br>
<strong>{{ trans('general.created_by') }}:</strong> %CREATED_BY% <br>
<strong>{{ trans('general.notes') }}:</strong> %NOTE%
</p>
</div>
</div>
</div>
<div class="panel-footer">
<div class="pull-right">
%DOWNLOAD_BUTTON% %NEW_WINDOW_BUTTON%
</div>
%DELETE_BUTTON%
</div>
</div>
</div>
</template>
<!-- end non-ajaxed file listing table -->

View File

@@ -146,6 +146,7 @@
filepath="private_uploads/components/"
showfile_routename="show.componentfile"
deletefile_routename="delete/componentfile"
object_type="components"
:object="$component" />
</div>
</div>

View File

@@ -437,6 +437,7 @@
filepath="private_uploads/consumables/"
showfile_routename="show.consumablefile"
deletefile_routename="delete/consumablefile"
object_type="consumables"
:object="$consumable" />
</div>

View File

@@ -1479,6 +1479,7 @@
filepath="private_uploads/assets/"
showfile_routename="show/assetfile"
deletefile_routename="delete/assetfile"
object_type="assets"
:object="$asset" />
</div> <!-- /.col-md-12 -->
</div> <!-- /.row -->
@@ -1494,6 +1495,7 @@
filepath="private_uploads/assetmodels/"
showfile_routename="show/modelfile"
deletefile_routename="delete/modelfile"
object_type="models"
:object="$asset->model" />
</div> <!-- /.col-md-12 -->

View File

@@ -962,55 +962,8 @@ dir="{{ Helper::determineLanguageDirection() }}">
<!-- end main container -->
<div class="modal modal-danger fade" id="dataConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h2 class="modal-title" id="myModalLabel">&nbsp;</h2>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="deleteForm" role="form">
{{ csrf_field() }}
{{ method_field('DELETE') }}
<button type="button" class="btn btn-default pull-left"
data-dismiss="modal">{{ trans('general.cancel') }}</button>
<button type="submit" class="btn btn-outline"
id="dataConfirmOK">{{ trans('general.yes') }}</button>
</form>
</div>
</div>
</div>
</div>
<div class="modal modal-warning fade" id="restoreConfirmModal" tabindex="-1" role="dialog"
aria-labelledby="confirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="confirmModalLabel">&nbsp;</h4>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<form method="post" id="restoreForm" role="form">
{{ csrf_field() }}
{{ method_field('POST') }}
<button type="button" class="btn btn-default pull-left"
data-dismiss="modal">{{ trans('general.cancel') }}</button>
<button type="submit" class="btn btn-outline"
id="dataConfirmOK">{{ trans('general.yes') }}</button>
</form>
</div>
</div>
</div>
</div>
<x-confirm-modal class="modal modal-danger fade" id="deleteModal" method="DELETE"/>
<x-confirm-modal class="modal modal-warning fade" id="restoreModal" method="POST"/>
{{-- Javascript files --}}

View File

@@ -469,6 +469,7 @@
filepath="private_uploads/licenses/"
showfile_routename="show.licensefile"
deletefile_routename="delete/licensefile"
object_type="licenses"
:object="$license" />
</div> <!-- /.tab-pane -->

View File

@@ -414,6 +414,7 @@
filepath="private_uploads/locations/"
showfile_routename="show/locationsfile"
deletefile_routename="delete/locationsfile"
object_type="locations"
:object="$location" />
</div> <!-- /.col-md-12 -->

View File

@@ -121,6 +121,7 @@
filepath="private_uploads/assetmodels/"
showfile_routename="show/modelfile"
deletefile_routename="delete/modelfile"
object_type="models"
:object="$model" />
</div> <!-- /.col-md-12 -->

View File

@@ -344,6 +344,48 @@
return '<a href="{{ config('app.url') }}/hardware/' + row.id + '/audit" class="actions btn btn-sm btn-primary" data-tooltip="true" title="{{ trans('general.audit') }}"><x-icon type="audit" /><span class="sr-only">{{ trans('general.audit') }}</span></a>&nbsp;';
}
function deleteUploadFormatter(value, row) {
var item_type = row.item.type;
if (item_type == 'assetmodel') {
item_type = 'models'
}
if ((row.available_actions) && (row.available_actions.delete === true)) {
return '<a href="{{ config('app.url') }}/' + item_type + '/' + row.item.id + '/showfile/' + row.id + '/delete" '
+ ' class="actions btn btn-danger btn-sm delete-asset" data-tooltip="true" '
+ ' data-toggle="modal" '
+ ' data-content="{{ trans('general.file_upload_status.confirm_delete') }} ' + row.filename + '?" '
+ ' data-title="{{ trans('general.delete') }}" onClick="return false;">'
+ '<x-icon type="delete" /><span class="sr-only">{{ trans('general.delete') }}</span></a>&nbsp;';
}
}
// This handles the custom view for the filestable blade component
window.fileGalleryFormatter = data => {
const template = $('#fileGalleryTemplate').html()
let view = ''
$.each(data, function (i, row) {
view += template.replace('%ID%', row.id)
.replace('%ICON%', row.icon)
.replace('%FILETYPE%', row.filetype)
.replace('%FILE_URL%', row.url)
.replace('%LINK_URL%', row.url)
.replace('%FILENAME%', (row.exists_on_disk === true) ? row.filename : '<x-icon type="x" /> <del>' + row.filename + '</del>')
.replace('%CREATED_AT%', row.created_at.formatted)
.replace('%CREATED_BY%', row.created_by.name)
.replace('%NOTE%', row.note)
.replace('%PANEL_CLASS%', (row.exists_on_disk === true) ? 'default' : 'danger')
.replace('%INLINE_IMAGE%', (row.inline === true) ? '<a href="' + row.url + '" data-toggle="lightbox" data-type="image"><img src="' + row.url + '" alt="" class="img-thumbnail" style="max-height: 50px; width: auto;"></a>' : '')
.replace('%DOWNLOAD_BUTTON%', (row.exists_on_disk === true) ? '<a href="'+ row.url +'" class="btn btn-sm btn-default"><x-icon type="download" /></a> ' : '<span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}"><x-icon type="download" /></span>')
.replace('%NEW_WINDOW_BUTTON%', (row.exists_on_disk === true) ? '<a href="'+ row.url +'?inline=true" class="btn btn-sm btn-default" target="_blank"><x-icon type="external-link" /></a> ' : '<span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}" ><x-icon type="external-link"/></span>')
.replace('%DELETE_BUTTON%', ((row.exists_on_disk === true) && row.available_actions.delete === true) ? '<a href="'+ row.url +'" class="btn btn-sm btn-danger"><x-icon type="delete" /></a> ' : '<span class="btn btn-sm btn-danger disabled" data-tooltip="true" title="{{ trans('general.file_upload_status.file_not_found') }}" ><x-icon type="delete"/></span>');
})
return `<div class="row">${view}</div>`
}
// Make the edit/delete buttons
function genericActionsFormatter(owner_name, element_name) {
@@ -378,7 +420,8 @@
if ((row.available_actions) && (row.available_actions.update === true)) {
actions += '<a href="{{ config('app.url') }}/' + dest + '/' + row.id + '/edit" class="actions btn btn-sm btn-warning" data-tooltip="true" title="{{ trans('general.update') }}"><x-icon type="edit" /><span class="sr-only">{{ trans('general.update') }}</span></a>&nbsp;';
} else {
if ((row.available_actions) && (row.available_actions.update != true)) {
// check that row.available_actions.update is set in the API response - if not, don't even show the button
if ((row.available_actions) && (row.available_actions.update) && (row.available_actions.update != true)) {
actions += '<span data-tooltip="true" title="{{ trans('general.cannot_be_edited') }}"><a class="btn btn-warning btn-sm disabled" onClick="return false;"><x-icon type="edit" /></a></span>&nbsp;';
}
}
@@ -785,9 +828,13 @@
}
}
function iconFormatter(value) {
function iconFormatter(value, row) {
if (value) {
if (row.filetype) {
return '<span data-tooltip="true" title=" ' + row.filetype + '"><i class="' + value + ' icon-med"></i></span>';
}
return '<i class="' + value + ' icon-med"></i>';
}
}
@@ -850,6 +897,12 @@
}
}
function inlineImageFormatter(value, row){
if (row.inline) {
return '<a href="' + row.url + '" data-toggle="lightbox" data-type="image"><img src="' + row.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a>'
}
}
function imageFormatter(value, row) {
@@ -868,25 +921,51 @@
return '<a href="' + value + '" data-toggle="lightbox" data-type="image"><img src="' + value + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt="' + altName + '"></a>';
}
}
function downloadFormatter(value) {
if (value) {
return '<a href="' + value + '" target="_blank"><x-icon type="download" /></a>';
function downloadOrOpenInNewWindowFormatter(value, row) {
if (row && row.url)
{
if (row.exists_on_disk) {
return '<span style="white-space: nowrap;"><a href="' + row.url + '" class="btn btn-sm btn-default"><x-icon type="download" /></a> <a href="' + row.url + '?inline=true" class="btn btn-sm btn-default" target="_blank"><x-icon type="external-link" /></a></span>';
} else {
return '<span style="white-space: nowrap;"><span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="download" /></span> <span class="btn btn-sm btn-default disabled" data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="external-link" /></span></span>';
}
}
}
function fileUploadFormatter(value) {
if ((value) && (value.url) && (value.inlineable)) {
return '<a href="' + value.url + '" data-toggle="lightbox" data-type="image"><img src="' + value.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a>';
return '<nowrap><a href="' + value.url + '" data-toggle="lightbox" data-type="image"><img src="' + value.url + '" style="max-height: {{ $snipeSettings->thumbnail_max_h }}px; width: auto;" class="img-responsive" alt=""></a></nowrap>';
} else if ((value) && (value.url)) {
return '<a href="' + value.url + '" class="btn btn-default"><x-icon type="download" /></a>';
}
}
function fileUploadNameFormatter(value) {
console.dir(value);
function fileUploadNameFormatter(row, value) {
if ((value) && (value.filename) && (value.url)) {
return '<a href="' + value.url + '">' + value.filename + '</a>';
if (value.exists_on_disk) {
return '<a href="' + value.url + '?inline=true" target="_blank">' + value.filename + '</a>';
} else {
return '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="x" class="text-danger" /> <del>' + value.filename + '</del></span>';
}
}
}
function fileUploadDeleteFormatter(row, value) {
if ((value) && (value.filename) && (value.url)) {
if (value.exists_on_disk) {
return '<a href="' + value.url + '?inline=true" target="_blank">' + value.filename + '</a>';
} else {
return '<span data-tooltip="true" title="{{ trans('general.file_does_not_exist') }}"><x-icon type="x" class="text-danger" /> <del>' + value.filename + '</del></span>';
}
}
}

View File

@@ -963,6 +963,8 @@
filepath="private_uploads/users/"
showfile_routename="show/userfile"
deletefile_routename="userfile.destroy"
object_type="users"
data_route="api.files.index"
:object="$user" />
</div>
</div> <!--/ROW-->

View File

@@ -549,34 +549,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.assets.restore');
Route::post('{asset}/files',
[
Api\AssetFilesController::class,
'store'
]
)->name('api.assets.files.store');
Route::get('{asset}/files',
[
Api\AssetFilesController::class,
'list'
]
)->name('api.assets.files.index');
Route::get('{asset_id}/file/{file_id}',
[
Api\AssetFilesController::class,
'show'
]
)->name('api.assets.files.show');
Route::delete('{asset_id}/file/{file_id}',
[
Api\AssetFilesController::class,
'destroy'
]
)->name('api.assets.files.destroy');
/** Begin assigned routes */
@@ -767,6 +739,8 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.locations.assigned_accessories');
/** End assigned routes */
});
Route::resource('locations',
@@ -846,33 +820,6 @@ 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.store');
Route::get('{model_id}/files',
[
Api\AssetModelFilesController::class,
'list'
]
)->name('api.models.files.index');
Route::get('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'show'
]
)->name('api.models.files.show');
Route::delete('{model_id}/file/{file_id}',
[
Api\AssetModelFilesController::class,
'destroy'
]
)->name('api.models.files.destroy');
});
Route::resource('models',
@@ -1129,12 +1076,6 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.users.licenselist');
Route::post('{user}/upload',
[
Api\UsersController::class,
'postUpload'
]
)->name('api.users.uploads');
Route::post('{user}/restore',
[
@@ -1143,6 +1084,8 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
]
)->name('api.users.restore');
});
Route::resource('users',
@@ -1347,4 +1290,46 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'throttle:api']], functi
// end generate label routes
/**
* Uploaded files API routes
*/
// List files
Route::get('{object_type}/{id}/files',
[
Api\UploadedFilesController::class,
'index'
]
)->name('api.files.index')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Get a file
Route::get('{object_type}/{id}/files/{file_id}',
[
Api\UploadedFilesController::class,
'show'
]
)->name('api.files.show')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Upload files(s)
Route::post('{object_type}/{id}/files',
[
Api\UploadedFilesController::class,
'store'
]
)->name('api.files.store')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
// Delete files(s)
Route::delete('{object_type}/{id}/files/{file_id}/delete',
[
Api\UploadedFilesController::class,
'destroy'
]
)->name('api.files.destroy')
->where(['object_type' => 'assets|models|users|locations|accessories|consumables|licenses|components']);
}); // end API routes

View File

@@ -5,7 +5,6 @@ use App\Http\Controllers\Assets\AssetsController;
use App\Http\Controllers\Assets\BulkAssetsController;
use App\Http\Controllers\Assets\AssetCheckoutController;
use App\Http\Controllers\Assets\AssetCheckinController;
use App\Http\Controllers\Assets\AssetFilesController;
use App\Models\Setting;
use Tabuna\Breadcrumbs\Trail;
use Illuminate\Support\Facades\Route;
@@ -141,17 +140,6 @@ Route::group(
[AssetsController::class, 'getRestore']
)->name('restore/hardware')->withTrashed();
Route::post('{asset}/upload',
[AssetFilesController::class, 'store']
)->name('upload/asset')->withTrashed();
Route::get('{asset}/showfile/{fileId}/{download?}',
[AssetFilesController::class, 'show']
)->name('show/assetfile')->withTrashed();
Route::delete('{asset}/showfile/{fileId}/delete',
[AssetFilesController::class, 'destroy']
)->name('delete/assetfile')->withTrashed();
Route::post(
'bulkedit',

View File

@@ -11,6 +11,10 @@ use Tabuna\Breadcrumbs\Trail;
Route::group(['prefix' => 'models', 'middleware' => ['auth']], function () {
Route::delete('{model}/showfile/{fileId}/delete',
[AssetModelsFilesController::class, 'destroy']
)->name('delete/modelfile')->withTrashed();
Route::post('{model}/upload',
[AssetModelsFilesController::class, 'store']
)->name('upload/models')->withTrashed();
@@ -19,9 +23,7 @@ Route::group(['prefix' => 'models', 'middleware' => ['auth']], function () {
[AssetModelsFilesController::class, 'show']
)->name('show/modelfile')->withTrashed();
Route::delete('{model}/showfile/{fileId}/delete',
[AssetModelsFilesController::class, 'destroy']
)->name('delete/modelfile')->withTrashed();
Route::get(
'{model}/clone',

View File

@@ -14,17 +14,17 @@ class AssetModelFilesTest extends TestCase
// Upload a file to a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->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)]
])
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
}
@@ -33,20 +33,55 @@ class AssetModelFilesTest extends TestCase
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk()
->assertJsonStructure([
'rows',
'total',
]);
}
public function testAssetModelFailsIfInvalidTypePassedInUrl()
{
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->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([
'rows',
'total',
]);
route('api.files.index', ['object_type' => 'shibboleeeeeet', 'id' => $model->id]))
->assertStatus(404);
}
public function testAssetModelFailsIfInvalidIdPassedInUrl()
{
// List all files on a model
// Create an model to work with
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => 100000]))
->assertOk()
->assertStatusMessageIs('error');
}
public function testAssetModelApiDownloadsFile()
@@ -54,70 +89,68 @@ class AssetModelFilesTest extends TestCase
// Download a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->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()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file with notes
$this->actingAsForApi($user)
->post(
route('api.models.files.store', ['model_id' => $model[0]["id"]]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
'notes' => 'manual'
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// Upload a file with notes
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)],
'notes' => 'manual'
])
->assertOk()
->assertJsonStructure([
'status',
'messages',
]);
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.models.files.index', ['model_id' => $model[0]["id"]]))
->assertOk()
->assertJsonStructure([
'total',
'rows'=>[
'*' => [
'id',
'filename',
'url',
'created_by',
'created_at',
'updated_at',
'deleted_at',
'note',
'available_actions'
]
]
])
->assertJsonPath('rows.0.note','')
->assertJsonPath('rows.1.note','manual');
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk()
->assertJsonStructure([
'total',
'rows'=>[
'*' => [
'id',
'filename',
'url',
'created_by',
'created_at',
'deleted_at',
'note',
'available_actions'
]
]
])
->assertJsonPath('rows.0.note',null)
->assertJsonPath('rows.1.note','manual');
// Get the file
$this->actingAsForApi($user)
->get(
route('api.models.files.show', [
'model_id' => $model[0]["id"],
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk();
// Get the file
$this->actingAsForApi($user)
->get(
route('api.files.show', [
'object_type' => 'models',
'id' => $model->id,
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk();
}
public function testAssetModelApiDeletesFile()
@@ -125,30 +158,31 @@ class AssetModelFilesTest extends TestCase
// Delete a file from a model
// Create a model to work with
$model = AssetModel::factory()->count(1)->create();
$model = AssetModel::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->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();
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.files.store', ['object_type' => 'models', 'id' => $model->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();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.files.index', ['object_type' => 'models', 'id' => $model->id]))
->assertOk();
// Delete the file
$this->actingAsForApi($user)
// Delete the file
$this->actingAsForApi($user)
->delete(
route('api.models.files.destroy', [
'model_id' => $model[0]["id"],
route('api.files.destroy', [
'object_type' => 'models',
'id' => $model->id,
'file_id' => $result->decodeResponseJson()->json()["rows"][0]["id"],
]))
->assertOk()

View File

@@ -14,7 +14,7 @@ class AssetFilesTest extends TestCase
// Upload a file to an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
@@ -22,7 +22,7 @@ class AssetFilesTest extends TestCase
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.assets.files.store', $asset), [
route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
@@ -33,14 +33,14 @@ class AssetFilesTest extends TestCase
// List all files on an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
// List the files
$this->actingAsForApi($user)
->getJson(route('api.assets.files.index', $asset))
->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk()
->assertJsonStructure([
'rows',
@@ -53,21 +53,21 @@ class AssetFilesTest extends TestCase
// Download a file from an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
//Upload a file
$this->actingAsForApi($user)
->post(route('api.assets.files.store', $asset), [
->post(route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(route('api.assets.files.index', $asset))
->getJson(route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk();
}
@@ -76,7 +76,7 @@ class AssetFilesTest extends TestCase
// Delete a file from an asset
// Create an asset to work with
$asset = Asset::factory()->count(1)->create();
$asset = Asset::factory()->create();
// Create a superuser to run this as
$user = User::factory()->superuser()->create();
@@ -84,7 +84,7 @@ class AssetFilesTest extends TestCase
//Upload a file
$this->actingAsForApi($user)
->post(
route('api.assets.files.store', $asset), [
route('api.files.store', ['object_type' => 'assets', 'id' => $asset->id]), [
'file' => [UploadedFile::fake()->create("test.jpg", 100)]
])
->assertOk();
@@ -92,7 +92,7 @@ class AssetFilesTest extends TestCase
// List the files to get the file ID
$result = $this->actingAsForApi($user)
->getJson(
route('api.assets.files.index', $asset))
route('api.files.index', ['object_type' => 'assets', 'id' => $asset->id]))
->assertOk();
}

View File

@@ -128,7 +128,7 @@ mix.combine(
"./node_modules/ekko-lightbox/dist/ekko-lightbox.js",
"./resources/assets/js/extensions/pGenerator.jquery.js",
"./node_modules/chart.js/dist/Chart.js",
"./resources/assets/js/signature_pad.js", //dupe?
"./resources/assets/js/signature_pad.js", //dupe?
"./node_modules/jquery-validation/dist/jquery.validate.js",
"./node_modules/list.js/dist/list.js",
"./node_modules/clipboard/dist/clipboard.js",
@@ -149,6 +149,7 @@ mix
'./node_modules/bootstrap-table/dist/extensions/cookie/bootstrap-table-cookie.js',
'./node_modules/bootstrap-table/dist/extensions/sticky-header/bootstrap-table-sticky-header.js',
'./node_modules/bootstrap-table/dist/extensions/addrbar/bootstrap-table-addrbar.js',
'./node_modules/bootstrap-table/dist/extensions/custom-view/bootstrap-table-custom-view.js',
'./resources/assets/js/extensions/jquery.base64.js',
'./node_modules/tableexport.jquery.plugin/tableExport.min.js',
'./node_modules/tableexport.jquery.plugin/libs/jsPDF/jspdf.umd.min.js',