Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added file upload for publications #133

Merged
merged 17 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions docs/dcat_example.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
"type": "application/json",
"published": "29-12-2020",
"modified": "30 december 2020, 17:09 (UTC+01:00)",
"accessURL": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer",
"downloadURL": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer"
"accessUrl": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer",
"downloadUrl": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer"
},
{
"title": "voorlopige energielabels met BAG-kenmerken ",
Expand All @@ -34,8 +34,8 @@
"type": "application/geopackage+sqlite3",
"published": "29-12-2020",
"modified": "30 december 2020, 17:10 (UTC+01:00)",
"accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GPKG/Voorlopige-labels-december-2019.gpkg",
"downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GPKG\\Voorlopige-labels-december-2019.gpkg"
"accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GPKG/Voorlopige-labels-december-2019.gpkg",
"downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GPKG\\Voorlopige-labels-december-2019.gpkg"
},
{
"title": "voorlopige energielabels met BAG-kenmerken ",
Expand All @@ -44,8 +44,8 @@
"type": "application/vnd.esri.filegdb",
"published": "29-12-2020",
"modified": "30 december 2020, 17:11 (UTC+01:00)",
"accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb",
"downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GDB\\Voorlopige-labels-december-2019.gdb"
"accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb",
"downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GDB\\Voorlopige-labels-december-2019.gdb"
},
{
"title": "voorlopige energielabels met BAG-kenmerken ",
Expand All @@ -54,8 +54,8 @@
"type": "application/x-shapefile",
"published": "29-12-2020",
"modified": "30 december 2020, 17:11 (UTC+01:00)",
"accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb",
"downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\SHP\\*.shp"
"accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb",
"downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\SHP\\*.shp"
},
{
"title": "productbeschrijving: voorlopige energielabels met BAG kenmerken",
Expand All @@ -64,8 +64,8 @@
"type": "text/html",
"published": "7-4-2020",
"modified": "30 december 2020, 17:12 (UTC+01:00)",
"accessURL": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A",
"downloadURL": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A"
"accessUrl": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A",
"downloadUrl": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A"
}
],
"attachment_count": 5,
Expand Down
214 changes: 172 additions & 42 deletions lib/Controller/AttachmentsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace OCA\OpenCatalogi\Controller;

use Exception;
use GuzzleHttp\Exception\GuzzleException;
use OCA\OpenCatalogi\Db\AttachmentMapper;
use OCA\OpenCatalogi\Service\ElasticSearchService;
Expand All @@ -12,6 +13,7 @@
use OCP\AppFramework\Http\JSONResponse;
use OCP\IAppConfig;
use OCP\IRequest;
use OCP\IUserSession;
use Symfony\Component\Uid\Uuid;

class AttachmentsController extends Controller
Expand All @@ -23,11 +25,11 @@ public function __construct
IRequest $request,
private readonly IAppConfig $config,
private readonly AttachmentMapper $attachmentMapper,
private readonly FileService $fileService
private readonly FileService $fileService,
private readonly IUserSession $userSession,
)
{
parent::__construct($appName, $request);
$this->fileService->setAppName($appName);
}

private function insertNestedObjects(array $object, ObjectService $objectService, array $config): array
Expand Down Expand Up @@ -94,8 +96,8 @@ public function catalog(string|int $id): TemplateResponse
*/
public function index(ObjectService $objectService): JSONResponse
{
if($this->config->hasKey($this->appName, 'mongoStorage') === false
|| $this->config->getValueString($this->appName, 'mongoStorage') !== '1'
if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
return new JSONResponse(['results' =>$this->attachmentMapper->findAll()]);
}
Expand All @@ -119,14 +121,15 @@ public function index(ObjectService $objectService): JSONResponse
return new JSONResponse($results);
}


/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function show(string|int $id, ObjectService $objectService): JSONResponse
{
if($this->config->hasKey($this->appName, 'mongoStorage') === false
|| $this->config->getValueString($this->appName, 'mongoStorage') !== '1'
if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
return new JSONResponse($this->attachmentMapper->find(id: (int) $id));
}
Expand All @@ -143,32 +146,157 @@ public function show(string|int $id, ObjectService $objectService): JSONResponse


/**
* @NoAdminRequired
* @NoCSRFRequired
* @throws GuzzleException In case the file upload to NextCloud fails.
* Gets info about the uploaded file from the request body, looks specifically for the field '_file'.
* If there is no file or there is an error loading it this will return an error response.
*
* @return JSONResponse|array An error response or an array containing the info about the uploaded file.
*/
public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse
{
private function checkUploadedFile(): JSONResponse|array
{
$uploadedFile = $this->request->getUploadedFile(key: '_file');

if (empty($uploadedFile) === true) {
return new JSONResponse(data: ['error' => 'No file uploaded for key "_file"'], statusCode: 400);
}

// Check for upload errors
if ($uploadedFile['error'] !== UPLOAD_ERR_OK) {
return new JSONResponse(data: ['error' => 'File upload error: '.$uploadedFile['error']], statusCode: 400);
}

return $uploadedFile;
}

/**
* Gets all params from the request body and then validates if the URL fields are actual valid urls (or null).
*
* @return JSONResponse|array An error response if there are validation errors or an array containing all request body params.
*/
private function checkRequestBody(): JSONResponse|array
{
$data = $this->request->getParams();

$uploadedFile = $this->request->getUploadedFile('_file');
// Todo: $uploadedFile['content'] does not contain the file content...
$this->fileService->uploadFile(content: $uploadedFile['content'], filePath: $uploadedFile['name']);
$data['downloadUrl'] = $this->fileService->createShareLink(path: $uploadedFile['name']);
$errorMsg = [];
if (empty($data['accessUrl']) === false && filter_var(value: $data['accessUrl'], filter: FILTER_VALIDATE_URL) === false) {
$errorMsg[] = "accessUrl is not a valid url";
}

if (empty($data['downloadUrl']) === false && filter_var(value: $data['downloadUrl'], filter: FILTER_VALIDATE_URL) === false) {
$errorMsg[] = "downloadUrl is not a valid url";
}

if (empty($errorMsg) === false) {
return new JSONResponse(data: ['validation_errors' => $errorMsg], statusCode: 400);
}

return $data;
}

/**
* If it does not already exist creates a folder for the publication the new Attachment belongs to in NextCloud,
* so that the uploaded file(s) for that publication can be saved there. After that saves the uploaded file in that folder.
* If the file is created without error this will return the full path to the file from the root/user folder.
*
* @param array $uploadedFile Information about the uploaded file from the request body.
*
* @return JSONResponse|string An error response if creating the file in NextCloud failed or a string path to the created file.
* @throws Exception In case creating a folder or new file fails.
*/
private function handleFile(array $uploadedFile): JSONResponse|string
{
// Create the Attachments folder and the Publication specific folder.
$this->fileService->createFolder(folderPath: 'Attachments');
$publicationFolder = '(' . $this->request->getHeader('Publication-Id') . ') '
. $this->request->getHeader('Publication-Title');
$this->fileService->createFolder(folderPath: "Attachments/$publicationFolder");

// Save the uploaded file
$filePath = "Attachments/$publicationFolder/" . $uploadedFile['name']; // Add a file version to the file name?
$created = $this->fileService->uploadFile(
content: file_get_contents(filename: $uploadedFile['tmp_name']),
filePath: $filePath
);

if ($created === false) {
return new JSONResponse(data: ['error' => "Failed to upload file. This file: $filePath might already exist"], statusCode: 400);
}

return $filePath;
}


/**
* Adds information about the uploaded file to the appropriate Attachment fields. And removes fields we do not want to post.
*
* @param array $data The form-data fields and their values (/request body) that we are going to update before posting the Attachment.
* @param array $uploadedFile Information about the uploaded file from the request body.
* @param string $filePath The full file path to where the file is stored in NextCloud.
*
* @return array The updated $data array
* @throws Exception In case creating the share(link) fails.
*/
private function AddFileInfoToData(array $data, array $uploadedFile, string $filePath): array
{
// Update Attachment data
$currentUser = $this->userSession->getUser();
$userId = $currentUser ? $currentUser->getUID() : 'Guest';
$data['reference'] = "$userId/$filePath";
$data['type'] = $uploadedFile['type'];
$data['size'] = $uploadedFile['size'];
$explodedName = explode('.', $uploadedFile['name']);
$explodedName = explode(separator: '.', string: $uploadedFile['name']);
$data['title'] = $explodedName[0];
$data['extension'] = end($explodedName);
$data['extension'] = end(array: $explodedName);

// Create ShareLink
$shareLink = $this->fileService->createShareLink(path: $filePath);
if (empty($data['accessUrl']) === true) {
$data['accessUrl'] = $shareLink;
}
$data['downloadUrl'] = "$shareLink/download";

// Remove fields we should never post
unset($data['id']);
foreach($data as $key => $value) {
if(str_starts_with($key, '_')) {
if(str_starts_with(haystack: $key, needle: '_')) {
unset($data[$key]);
}
}

if($this->config->hasKey($this->appName, 'mongoStorage') === false
|| $this->config->getValueString($this->appName, 'mongoStorage') !== '1'
return $data;
}


/**
* @NoAdminRequired
* @NoCSRFRequired
* @throws Exception In case creating a new folder, the file upload to NextCloud, or creating the share link fails.
* @throws GuzzleException In case saving the Attachment to MongoDB fails.
*/
public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse
{
// Check if a file was uploaded
$uploadedFile = $this->checkUploadedFile();
if ($uploadedFile instanceof JSONResponse) {
return $uploadedFile;
}

// Get form-data field/request body.
$data = $this->checkRequestBody();
if ($data instanceof JSONResponse) {
return $data;
}

// Handle saving the uploaded file in NextCloud
$filePath = $this->handleFile(uploadedFile: $uploadedFile);
if ($filePath instanceof JSONResponse) {
return $filePath;
}

// Update Attachment data
$data = $this->AddFileInfoToData(data: $data, uploadedFile: $uploadedFile, filePath: $filePath);

if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
return new JSONResponse($this->attachmentMapper->createFromArray(object: $data));
}
Expand All @@ -191,32 +319,22 @@ public function create(ObjectService $objectService, ElasticSearchService $elast
/**
* @NoAdminRequired
* @NoCSRFRequired
* @throws GuzzleException In case updating the file in NextCloud fails.
*/
public function update(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse
{
$data = $this->request->getParams();

$uploadedFile = $this->request->getUploadedFile('_file');
// Todo: $uploadedFile['content'] does not contain the file content...
$this->fileService->uploadFile(content: $uploadedFile['content'], filePath: $uploadedFile['name'], update: true);
// $data['downloadUrl'] = $this->fileService->createShareLink(path: $uploadedFile['name']);
$data['type'] = $uploadedFile['type'];
$data['size'] = $uploadedFile['size'];
$explodedName = explode('.', $uploadedFile['name']);
$data['title'] = $explodedName[0];
$data['extension'] = end($explodedName);

// Remove fields we should never post
unset($data['id']);
foreach($data as $key => $value) {
if(str_starts_with($key, '_')) {
if(str_starts_with(haystack: $key, needle: '_')) {
unset($data[$key]);
}
}
if (isset($data['id'])) {
unset( $data['id']);
}

if($this->config->hasKey($this->appName, 'mongoStorage') === false
|| $this->config->getValueString($this->appName, 'mongoStorage') !== '1'
if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
return new JSONResponse($this->attachmentMapper->updateFromArray(id: (int) $id, object: $data));
}
Expand Down Expand Up @@ -246,14 +364,26 @@ public function update(string|int $id, ObjectService $objectService, ElasticSear
*/
public function destroy(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse
{
$attachment = $this->show($id, $objectService)->getData()->jsonSerialize();
// Todo: are we sure this is the best way to do this (how do we save the full path to this file in nextCloud)
$this->fileService->deleteFile(filePath: $attachment['title']. '.' .$attachment['extension']);
$attachment = $this->show(id: $id, objectService: $objectService)->getData();
if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
$attachment = $attachment->jsonSerialize();
}

if($this->config->hasKey($this->appName, 'mongoStorage') === false
|| $this->config->getValueString($this->appName, 'mongoStorage') !== '1'
// Todo: are we sure this is the best way to do this (how do we save the full path to this file in nextCloud)
// $publicationFolder = '(' . $this->request->getHeader('Publication-Id') . ') '
// . $this->request->getHeader('Publication-Title');
// $this->fileService->deleteFile(filePath: "Attachments/$publicationFolder" . $attachment['title'] . '.' . $attachment['extension']);
$filePath = explode(separator: '/', string: $attachment['reference']);
array_shift(array: $filePath);
$filePath = implode(separator: '/', array: $filePath);
$this->fileService->deleteFile(filePath: $filePath);

if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false
|| $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1'
) {
$this->attachmentMapper->delete($this->attachmentMapper->find((int) $id));
$this->attachmentMapper->delete(entity: $this->attachmentMapper->find(id: (int) $id));

return new JSONResponse([]);
}
Expand Down
2 changes: 2 additions & 0 deletions lib/Controller/CatalogiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public function create(ObjectService $objectService, DirectoryService $directory
{
$data = $this->request->getParams();

// Remove fields we should never post
unset($data['id']);
foreach ($data as $key => $value) {
if (str_starts_with($key, '_')) {
unset($data[$key]);
Expand Down
4 changes: 1 addition & 3 deletions lib/Controller/ConfigurationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ public function index(): JSONResponse
'elasticIndex' => '',
'organisationName' => 'my-organisation',
'organisationOin' => '',
'organisationPki' => '',
'adminUsername' => '',
'adminPassword' => ''
'organisationPki' => ''
];

try {
Expand Down
5 changes: 2 additions & 3 deletions lib/Controller/DirectoryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,13 @@ public function update(string|int $id, ObjectService $objectService): JSONRespon

$data = $this->request->getParams();

// Remove fields we should never post
unset($data['id']);
foreach($data as $key => $value) {
if(str_starts_with($key, '_')) {
unset($data[$key]);
}
}
if (isset($data['id'])) {
unset( $data['id']);
}


if($this->config->hasKey($this->appName, 'mongoStorage') === false
Expand Down
Loading
Loading