mirror of
https://github.com/grocy/grocy.git
synced 2025-04-29 17:45:39 +00:00
Add user-field-type "file" (#977)
* Add user-field-type "file" * Add userfield-type "picture" * Also limit image height on userfieldsform * Prevent empty userfields (cause warnings in tables after deleting a file) * Show files in dialogs Co-authored-by: Bernd Bestel <bernd@berrnd.de>
This commit is contained in:
parent
318db53818
commit
07beee93a9
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace Grocy\Controllers;
|
namespace Grocy\Controllers;
|
||||||
|
|
||||||
use Grocy\Controllers\Users\User;
|
|
||||||
use \Grocy\Services\FilesService;
|
use \Grocy\Services\FilesService;
|
||||||
|
use Slim\Exception\HttpNotFoundException;
|
||||||
|
|
||||||
class FilesApiController extends BaseApiController
|
class FilesApiController extends BaseApiController
|
||||||
{
|
{
|
||||||
@ -16,14 +16,7 @@ class FilesApiController extends BaseApiController
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsValidFileName(base64_decode($args['fileName'])))
|
$fileName = $this->checkFileName($args['fileName']);
|
||||||
{
|
|
||||||
$fileName = base64_decode($args['fileName']);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new \Exception('Invalid filename');
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $request->getBody()->getContents();
|
$data = $request->getBody()->getContents();
|
||||||
file_put_contents($this->getFilesService()->GetFilePath($args['group'], $fileName), $data);
|
file_put_contents($this->getFilesService()->GetFilePath($args['group'], $fileName), $data);
|
||||||
@ -40,41 +33,9 @@ class FilesApiController extends BaseApiController
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (IsValidFileName(base64_decode($args['fileName'])))
|
$fileName = $this->checkFileName($args['fileName']);
|
||||||
{
|
|
||||||
$fileName = base64_decode($args['fileName']);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new \Exception('Invalid filename');
|
|
||||||
}
|
|
||||||
|
|
||||||
$forceServeAs = null;
|
$filePath = $this->getFilePath($args['group'], $fileName, $request->getQueryParams());
|
||||||
if (isset($request->getQueryParams()['force_serve_as']) && !empty($request->getQueryParams()['force_serve_as']))
|
|
||||||
{
|
|
||||||
$forceServeAs = $request->getQueryParams()['force_serve_as'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($forceServeAs == FilesService::FILE_SERVE_TYPE_PICTURE)
|
|
||||||
{
|
|
||||||
$bestFitHeight = null;
|
|
||||||
if (isset($request->getQueryParams()['best_fit_height']) && !empty($request->getQueryParams()['best_fit_height']) && is_numeric($request->getQueryParams()['best_fit_height']))
|
|
||||||
{
|
|
||||||
$bestFitHeight = $request->getQueryParams()['best_fit_height'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$bestFitWidth = null;
|
|
||||||
if (isset($request->getQueryParams()['best_fit_width']) && !empty($request->getQueryParams()['best_fit_width']) && is_numeric($request->getQueryParams()['best_fit_width']))
|
|
||||||
{
|
|
||||||
$bestFitWidth = $request->getQueryParams()['best_fit_width'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$filePath = $this->getFilesService()->DownscaleImage($args['group'], $fileName, $bestFitHeight, $bestFitWidth);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$filePath = $this->getFilesService()->GetFilePath($args['group'], $fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($filePath))
|
if (file_exists($filePath))
|
||||||
{
|
{
|
||||||
@ -85,12 +46,39 @@ class FilesApiController extends BaseApiController
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return $this->GenericErrorResponse($response, 'File not found', 404);
|
throw new HttpNotFoundException($request, 'File not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (\Exception $ex)
|
catch (\Exception $ex)
|
||||||
{
|
{
|
||||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
throw new HttpNotFoundException($request, $ex->getMessage(), $ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ShowFile(\Psr\Http\Message\ServerRequestInterface $request, \Psr\Http\Message\ResponseInterface $response, array $args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$fileInfo = explode('_', $args['fileName']);
|
||||||
|
$fileName = $this->checkFileName($fileInfo[1]);
|
||||||
|
|
||||||
|
$filePath = $this->getFilePath($args['group'], base64_decode($fileInfo[0]), $request->getQueryParams());
|
||||||
|
|
||||||
|
if (file_exists($filePath))
|
||||||
|
{
|
||||||
|
$response->write(file_get_contents($filePath));
|
||||||
|
$response = $response->withHeader('Cache-Control', 'max-age=2592000');
|
||||||
|
$response = $response->withHeader('Content-Type', mime_content_type($filePath));
|
||||||
|
return $response->withHeader('Content-Disposition', 'inline; filename="' . $fileName . '"');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new HttpNotFoundException($request, 'File not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (\Exception $ex)
|
||||||
|
{
|
||||||
|
throw new HttpNotFoundException($request, $ex->getMessage(), $ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,4 +108,50 @@ class FilesApiController extends BaseApiController
|
|||||||
return $this->GenericErrorResponse($response, $ex->getMessage());
|
return $this->GenericErrorResponse($response, $ex->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $group The group the requested files belongs to.
|
||||||
|
* @param string $fileName The name of the requested file.
|
||||||
|
* @param array $queryParams Parameter, e.g. for scaling. Optional.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getFilePath(string $group, string $fileName, array $queryParams = [])
|
||||||
|
{
|
||||||
|
$forceServeAs = null;
|
||||||
|
if (isset($queryParams['force_serve_as']) && !empty($queryParams['force_serve_as'])) {
|
||||||
|
$forceServeAs = $queryParams['force_serve_as'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($forceServeAs == FilesService::FILE_SERVE_TYPE_PICTURE) {
|
||||||
|
$bestFitHeight = null;
|
||||||
|
if (isset($queryParams['best_fit_height']) && !empty($queryParams['best_fit_height']) && is_numeric($queryParams['best_fit_height'])) {
|
||||||
|
$bestFitHeight = $queryParams['best_fit_height'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$bestFitWidth = null;
|
||||||
|
if (isset($queryParams['best_fit_width']) && !empty($queryParams['best_fit_width']) && is_numeric($queryParams['best_fit_width'])) {
|
||||||
|
$bestFitWidth = $queryParams['best_fit_width'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $this->getFilesService()->DownscaleImage($group, $fileName, $bestFitHeight, $bestFitWidth);
|
||||||
|
} else {
|
||||||
|
$filePath = $this->getFilesService()->GetFilePath($group, $fileName);
|
||||||
|
}
|
||||||
|
return $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $fileName base64-encoded file-name
|
||||||
|
* @return false|string the decoded file-name
|
||||||
|
* @throws \Exception if the file-name is invalid.
|
||||||
|
*/
|
||||||
|
protected function checkFileName(string $fileName)
|
||||||
|
{
|
||||||
|
if (IsValidFileName(base64_decode($fileName))) {
|
||||||
|
$fileName = base64_decode($fileName);
|
||||||
|
} else {
|
||||||
|
throw new \Exception('Invalid filename');
|
||||||
|
}
|
||||||
|
return $fileName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,3 +41,9 @@ msgstr ""
|
|||||||
|
|
||||||
msgid "link"
|
msgid "link"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "file"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "image"
|
||||||
|
msgstr ""
|
||||||
|
16
migrations/0112.sql
Normal file
16
migrations/0112.sql
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
DELETE FROM userfield_values
|
||||||
|
WHERE IFNULL(value, '') = '';
|
||||||
|
|
||||||
|
CREATE TRIGGER prevent_empty_userfields_INS AFTER INSERT ON userfield_values
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM userfield_values
|
||||||
|
WHERE id = NEW.id
|
||||||
|
AND IFNULL(value, '') = '';
|
||||||
|
END;
|
||||||
|
|
||||||
|
CREATE TRIGGER prevent_empty_userfields_UPD2 AFTER UPDATE ON userfield_values
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM userfield_values
|
||||||
|
WHERE id = NEW.id
|
||||||
|
AND IFNULL(value, '') = '';
|
||||||
|
END;
|
@ -172,3 +172,6 @@ function animateCSS(selector, animationName, callback, speed = "faster")
|
|||||||
|
|
||||||
nodes.on('animationend', handleAnimationEnd);
|
nodes.on('animationend', handleAnimationEnd);
|
||||||
}
|
}
|
||||||
|
function RandomString() {
|
||||||
|
return Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
|
||||||
|
}
|
||||||
|
@ -28,6 +28,36 @@ Grocy.Components.UserfieldsForm.Save = function(success, error)
|
|||||||
jsonData[fieldName] = "1";
|
jsonData[fieldName] = "1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (input.attr("type") == "file")
|
||||||
|
{
|
||||||
|
var old_file = input.data('old-file')
|
||||||
|
if (old_file) {
|
||||||
|
Grocy.Api.Delete('files/userfiles/' + old_file, null, null,
|
||||||
|
function (xhr) {
|
||||||
|
Grocy.FrontendHelpers.ShowGenericError('Could not delete file', xhr);
|
||||||
|
});
|
||||||
|
jsonData[fieldName] = "";
|
||||||
|
}
|
||||||
|
if (input[0].files.length > 0){
|
||||||
|
// Files service requires an extension
|
||||||
|
var fileName = RandomString() + '.' + input[0].files[0].name.split('.').reverse()[0];
|
||||||
|
|
||||||
|
jsonData[fieldName] = btoa(fileName) + '_' + btoa(input[0].files[0].name);
|
||||||
|
Grocy.Api.UploadFile(input[0].files[0], 'userfiles', fileName,
|
||||||
|
function (result)
|
||||||
|
{
|
||||||
|
},
|
||||||
|
function (xhr)
|
||||||
|
{
|
||||||
|
Grocy.FrontendHelpers.ShowGenericError('Error while saving, probably this item already exists', xhr.response)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//jsonData[fieldName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if ($(this).hasAttr("multiple"))
|
else if ($(this).hasAttr("multiple"))
|
||||||
{
|
{
|
||||||
jsonData[fieldName] = $(this).val().join(",");
|
jsonData[fieldName] = $(this).val().join(",");
|
||||||
@ -79,6 +109,28 @@ Grocy.Components.UserfieldsForm.Load = function()
|
|||||||
input.val(value.split(","));
|
input.val(value.split(","));
|
||||||
$(".selectpicker").selectpicker("render");
|
$(".selectpicker").selectpicker("render");
|
||||||
}
|
}
|
||||||
|
if (input.attr('type') == "file")
|
||||||
|
{
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
var file_name = atob(value.split('_')[1]);
|
||||||
|
var file_src = value.split('_')[0];
|
||||||
|
input.hide();
|
||||||
|
var file_info = input.siblings('.userfield-file');
|
||||||
|
file_info.removeClass('d-none');
|
||||||
|
file_info.find('a.userfield-current-file')
|
||||||
|
.attr('href', U('/files/userfiles/' + value))
|
||||||
|
.text(file_name);
|
||||||
|
file_info.find('img.userfield-current-file')
|
||||||
|
.attr('src', U('/files/userfiles/' + value + '?force_serve_as=picture&best_fit_width=250&best_fit_height=250'))
|
||||||
|
file_info.find('button.userfield-file-delete').click(
|
||||||
|
function () {
|
||||||
|
file_info.addClass('d-none');
|
||||||
|
input.data('old-file', file_src);
|
||||||
|
input.show();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
input.val(value);
|
input.val(value);
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
if ($("#instruction-manual")[0].files.length > 0)
|
if ($("#instruction-manual")[0].files.length > 0)
|
||||||
{
|
{
|
||||||
var someRandomStuff = Math.random().toString(36).substring(2, 100) + Math.random().toString(36).substring(2, 100);
|
var someRandomStuff = RandomString();
|
||||||
jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name;
|
jsonData.instruction_manual_file_name = someRandomStuff + $("#instruction-manual")[0].files[0].name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,8 @@ $app->group('', function(RouteCollectorProxy $group)
|
|||||||
$group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList');
|
$group->get('/user/{userId}/permissions', '\Grocy\Controllers\UsersController:PermissionList');
|
||||||
$group->get('/usersettings', '\Grocy\Controllers\UsersController:UserSettings');
|
$group->get('/usersettings', '\Grocy\Controllers\UsersController:UserSettings');
|
||||||
|
|
||||||
|
$group->get('/files/{group}/{fileName}', '\Grocy\Controllers\FilesApiController:ShowFile');
|
||||||
|
|
||||||
// Stock routes
|
// Stock routes
|
||||||
if (GROCY_FEATURE_FLAG_STOCK)
|
if (GROCY_FEATURE_FLAG_STOCK)
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,8 @@ class UserfieldsService extends BaseService
|
|||||||
const USERFIELD_TYPE_PRESET_LIST = 'preset-list';
|
const USERFIELD_TYPE_PRESET_LIST = 'preset-list';
|
||||||
const USERFIELD_TYPE_PRESET_CHECKLIST = 'preset-checklist';
|
const USERFIELD_TYPE_PRESET_CHECKLIST = 'preset-checklist';
|
||||||
const USERFIELD_TYPE_LINK = 'link';
|
const USERFIELD_TYPE_LINK = 'link';
|
||||||
|
const USERFIELD_TYPE_FILE = 'file';
|
||||||
|
const USERFIELD_TYPE_IMAGE = 'image';
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
@ -12,6 +12,13 @@
|
|||||||
{!! str_replace(',', '<br>', $userfieldObject->value) !!}
|
{!! str_replace(',', '<br>', $userfieldObject->value) !!}
|
||||||
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_LINK)
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_LINK)
|
||||||
<a href="{{ $userfieldObject->value }}" target="_blank">{{ $userfieldObject->value }}</a>
|
<a href="{{ $userfieldObject->value }}" target="_blank">{{ $userfieldObject->value }}</a>
|
||||||
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_FILE)
|
||||||
|
<a class="show-as-dialog-link" href="{{ $U('/files/userfiles/'. $userfieldObject->value) }}" target="_blank">{{ base64_decode(explode('_', $userfieldObject->value)[1]) }}</a>
|
||||||
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_IMAGE)
|
||||||
|
<a class="show-as-dialog-link" href="{{ $U('/files/userfiles/'. $userfieldObject->value . '?force_serve_as=picture') }}">
|
||||||
|
<img src="{{ $U('/files/userfiles/'. $userfieldObject->value . '?force_serve_as=picture&best_fit_width=32&best_fit_height=32') }}"
|
||||||
|
class="lazy" title="{{ base64_decode(explode('_', $userfieldObject->value)[1]) }}" alt="{{ base64_decode(explode('_', $userfieldObject->value)[1]) }}">
|
||||||
|
</a>
|
||||||
@else
|
@else
|
||||||
{{ $userfieldObject->value }}
|
{{ $userfieldObject->value }}
|
||||||
@endif
|
@endif
|
||||||
|
@ -98,6 +98,28 @@
|
|||||||
<label for="name">{{ $userfield->caption }}</label>
|
<label for="name">{{ $userfield->caption }}</label>
|
||||||
<input type="link" class="form-control userfield-input" data-userfield-name="{{ $userfield->name }}">
|
<input type="link" class="form-control userfield-input" data-userfield-name="{{ $userfield->name }}">
|
||||||
</div>
|
</div>
|
||||||
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_FILE)
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ $userfield->name }}">{{ $userfield->caption }}</label>
|
||||||
|
<input type="file" class="form-control userfield-input" data-userfield-name="{{ $userfield->name }}">
|
||||||
|
<div class="d-none userfield-file">
|
||||||
|
<a href="" class="userfield-current-file" data-uf-name="{{ $userfield->name }}"></a>
|
||||||
|
<button type="button" class="userfield-current-file btn btn-danger userfield-file-delete" data-uf-name="{{ $userfield->name }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif($userfield->type == \Grocy\Services\UserfieldsService::USERFIELD_TYPE_IMAGE)
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ $userfield->name }}">{{ $userfield->caption }}</label>
|
||||||
|
<input type="file" class="form-control userfield-input" data-userfield-name="{{ $userfield->name }}">
|
||||||
|
<div class="d-none userfield-file">
|
||||||
|
<img src="" alt="{{ $userfield->name }}" class="userfield-current-file" data-uf-name="{{ $userfield->name }}"/>
|
||||||
|
<button type="button" class="userfield-current-file btn btn-danger userfield-file-delete" data-uf-name="{{ $userfield->name }}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@endforeach
|
@endforeach
|
||||||
|
Loading…
x
Reference in New Issue
Block a user