From f1fc0ee54921d5a528bca131c7cc365fce6df17c Mon Sep 17 00:00:00 2001 From: Bernd Bestel Date: Mon, 1 Oct 2018 20:20:50 +0200 Subject: [PATCH] Finished first version of "pictures for products" (references #58) --- controllers/FilesApiController.php | 29 +++++++++- grocy.openapi.json | 73 +++++++++++++++++++++---- helpers/extensions.php | 2 +- public/js/extensions.js | 10 ++++ public/js/grocy.js | 56 ++++++++++++++++--- public/viewjs/components/productcard.js | 12 ++++ public/viewjs/productform.js | 49 +++++++++++++---- public/viewjs/stockoverview.js | 19 ++++--- routes.php | 5 +- views/components/productcard.blade.php | 4 ++ views/productform.blade.php | 29 +++++++--- views/stockoverview.blade.php | 20 ++++--- 12 files changed, 252 insertions(+), 56 deletions(-) diff --git a/controllers/FilesApiController.php b/controllers/FilesApiController.php index 656ecc09..4bef5531 100644 --- a/controllers/FilesApiController.php +++ b/controllers/FilesApiController.php @@ -14,7 +14,7 @@ class FilesApiController extends BaseApiController protected $FilesService; - public function Upload(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + public function UploadFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { try { @@ -69,4 +69,31 @@ class FilesApiController extends BaseApiController return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); } } + + public function DeleteFile(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + try + { + if (isset($request->getQueryParams()['file_name']) && !empty($request->getQueryParams()['file_name']) && IsValidFileName($request->getQueryParams()['file_name'])) + { + $fileName = $request->getQueryParams()['file_name']; + } + else + { + throw new \Exception('file_name query parameter missing or contains an invalid filename'); + } + + $filePath = $this->FilesService->GetFilePath($args['group'], $fileName); + if (file_exists($filePath)) + { + unlink($filePath); + } + + return $this->ApiResponse(array('success' => true)); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } } diff --git a/grocy.openapi.json b/grocy.openapi.json index d50fd8f2..59ee8cb4 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -431,9 +431,58 @@ } } }, - "/files/upload/{group}": { - "post": { - "description": "Uploads a single file to /data/storage/{group}/{file_name}", + "/file/{group}": { + "get": { + "description": "Serves the given file (with proper Content-Type header)", + "tags": [ + "Files" + ], + "parameters": [ + { + "in": "path", + "name": "group", + "required": true, + "description": "The file group", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "file_name", + "required": true, + "description": "The file name (including extension)", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The binary file contents (Content-Type header is automatically set based on the file type)", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + }, + "put": { + "description": "Uploads a single file to /data/storage/{group}/{file_name} (you need to remember the group and file name to get or delete it again)", "tags": [ "Files" ], @@ -489,11 +538,9 @@ } } } - } - }, - "/files/get/{group}": { - "get": { - "description": "Serves the given file", + }, + "delete": { + "description": "Deletes the given file", "tags": [ "Files" ], @@ -519,12 +566,11 @@ ], "responses": { "200": { - "description": "The binary file contents (mime type is set based on file extension)", + "description": "A VoidApiActionResponse object", "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/VoidApiActionResponse" } } } @@ -1683,6 +1729,9 @@ "minimum": 0, "default": 0 }, + "picture_file_name": { + "type": "string" + }, "row_created_timestamp": { "type": "string", "format": "date-time" diff --git a/helpers/extensions.php b/helpers/extensions.php index 3c17414f..06e11b85 100644 --- a/helpers/extensions.php +++ b/helpers/extensions.php @@ -192,7 +192,7 @@ function Pluralize($number, $singularForm, $pluralForm) function IsValidFileName($fileName) { - if(preg_match('#^[a-z0-9]+\.[a-z]+?$#i', $fileName)) + if(preg_match('=^[^/?*;:{}\\\\]+\.[^/?*;:{}\\\\]+$=', $fileName)) { return true; } diff --git a/public/js/extensions.js b/public/js/extensions.js index 693d3acd..16419e21 100644 --- a/public/js/extensions.js +++ b/public/js/extensions.js @@ -54,3 +54,13 @@ BoolVal = function(test) return false; } } + +GetFileNameFromPath = function(path) +{ + return path.split("/").pop().split("\\").pop(); +} + +GetFileExtension = function(pathOrFileName) +{ + return pathOrFileName.split(".").pop(); +} diff --git a/public/js/grocy.js b/public/js/grocy.js index 3b21527d..f0257cf5 100644 --- a/public/js/grocy.js +++ b/public/js/grocy.js @@ -177,15 +177,10 @@ Grocy.Api.Post = function(apiFunction, jsonData, success, error) xhr.send(JSON.stringify(jsonData)); }; -Grocy.Api.UploadFile = function(fileInput, group, success, error) +Grocy.Api.UploadFile = function(file, group, fileName, success, error) { - if (fileInput[0].files.length === 0) - { - return; - } - var xhr = new XMLHttpRequest(); - var url = U('/api/files/upload/' + group + '?file_name=' + encodeURIComponent(fileInput[0].files[0].name)); + var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); xhr.onreadystatechange = function() { @@ -208,9 +203,40 @@ Grocy.Api.UploadFile = function(fileInput, group, success, error) } }; - xhr.open('POST', url, true); + xhr.open('PUT', url, true); xhr.setRequestHeader('Content-type', 'application/octet-stream'); - xhr.send(fileInput[0].files[0]); + xhr.send(file); +}; + +Grocy.Api.DeleteFile = function(fileName, group, success, error) +{ + var xhr = new XMLHttpRequest(); + var url = U('/api/file/' + group + '?file_name=' + encodeURIComponent(fileName)); + + xhr.onreadystatechange = function() + { + if (xhr.readyState === XMLHttpRequest.DONE) + { + if (xhr.status === 200) + { + if (success) + { + success(JSON.parse(xhr.responseText)); + } + } + else + { + if (error) + { + error(xhr); + } + } + } + }; + + xhr.open('DELETE', url, true); + xhr.setRequestHeader('Content-type', 'application/json'); + xhr.send(); }; Grocy.FrontendHelpers = { }; @@ -284,3 +310,15 @@ $(".user-setting-control").on("change", function() } ); }); + +// Show file name Bootstrap custom file input +$('input.custom-file-input').on('change', function() +{ + $(this).next('.custom-file-label').html(GetFileNameFromPath($(this).val())); +}); + +// Translation of "Browse"-button of Bootstrap custom file input +if ($(".custom-file-label").length > 0) +{ + $(" +@endpush + @section('content')
@@ -74,14 +82,12 @@ data-consume-amount="{{ $currentStockEntry->amount }}"> {{ $L('All') }} - - - - - {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }} + + {{ FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->name }}@if(!empty(FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->picture_file_name)) @endif {{ $currentStockEntry->amount }} {{ Pluralize($currentStockEntry->amount, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name, FindObjectInArrayByPropertyValue($quantityunits, 'id', FindObjectInArrayByPropertyValue($products, 'id', $currentStockEntry->product_id)->qu_id_stock)->name_plural) }}