Skip to content

Commit

Permalink
Merge pull request #49 from RyanNerd/paperless
Browse files Browse the repository at this point in the history
⛲ feat Document Management (upload/download files)
  • Loading branch information
RyanNerd authored Mar 6, 2022
2 parents cd87312 + 33d5b4d commit 253b0b2
Show file tree
Hide file tree
Showing 30 changed files with 560 additions and 68 deletions.
4 changes: 2 additions & 2 deletions app/Controllers/Authenticate/AuthenticatePostValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

class AuthenticatePostValidator
{
private const ALLOWED = ['username', 'password', 'id'];
private const ALLOWED = ['username', 'password', 'id', 'uploaded_files'];

/**
* @throws JsonException
Expand All @@ -40,7 +40,7 @@ public function __invoke(Request $request, RequestHandler $handler): ResponseInt
}

// id can be part of the request, but it MUST be null/empty
if (V::exists()->validate($parsedRequest['id']) && V::notEmpty()->validate($parsedRequest['id'])) {
if (V::key('id')->validate($parsedRequest) && V::notEmpty()->validate($parsedRequest['id'])) {
$responseBody->registerParam('invalid', 'id', 'null');
}

Expand Down
16 changes: 11 additions & 5 deletions app/Controllers/DeleteActionBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@ abstract class DeleteActionBase extends ActionBase
public function __invoke(Request $request, Response $response, array $args): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');
$parsedRequest = $responseBody->getParsedRequest();
$model = $this->model;
$id = $args['id'];
$model = $model->find($id);

// Destroy the model given the id.
if ($model::destroy($args['id']) === 1) {
$status = ResponseCodes::HTTP_OK;
} else {
$status = ResponseCodes::HTTP_NOT_FOUND;
$status = ResponseCodes::HTTP_NOT_FOUND;
if ($model !== null) {
if (array_key_exists('force', $parsedRequest) && $parsedRequest['force'] === "true") {
$isDeleted = $model->forceDelete();
} else {
$isDeleted = $model->delete();
}
$status= $isDeleted ? ResponseCodes::HTTP_OK : ResponseCodes::HTTP_INTERNAL_SERVER_ERROR;
}

// Set the status and data of the ResponseBody
Expand Down
29 changes: 29 additions & 0 deletions app/Controllers/File/FileController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use Slim\Interfaces\RouteCollectorProxyInterface;
use Willow\Controllers\IController;

class FileController implements IController
{
/**
* Register routes and actions
* @param RouteCollectorProxyInterface $group
*/
final public function register(RouteCollectorProxyInterface $group): void {
$group->post('/file/upload/{client_id}', FileUploadAction::class)
->add(FileUploadValidator::class);

$group->get('/file/download/{id}', FileDownloadAction::class);

$group->get('/file/{id}', FileGetAction::class);

$group->post('/file', FileUpdateAction::class);

$group->delete('/file/{id}', FileDeleteAction::class);

$group->get('/file/load/{client_id}', FileLoadAction::class);
}
}
18 changes: 18 additions & 0 deletions app/Controllers/File/FileDeleteAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use Willow\Controllers\DeleteActionBase;
use Willow\Models\File;

class FileDeleteAction extends DeleteActionBase
{
/**
* FileDeleteAction constructor
* @param File $model
*/
public function __construct(File $model) {
$this->model = $model;
}
}
61 changes: 61 additions & 0 deletions app/Controllers/File/FileDownloadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use JsonException;
use Psr\Http\Message\ResponseInterface;
use Slim\Psr7\Factory\StreamFactory;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use Willow\Models\File;
use Willow\Models\FileRepresentation;

class FileDownloadAction
{
/**
* FileGetAction constructor.
* @param File $file
*/
public function __construct(private File $file) {
}

/**
* Handle 'file/download' request
* @see https://dev.to/nauleyco/how-to-download-a-csv-file-with-slim-4-21a7
* @param Request $request
* @param Response $response
* @param array $args
* @return ResponseInterface
* @throws JsonException
*/
public function __invoke(Request $request, Response $response, array $args): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');


/**
* Load the File Model with the given id (PK)
* @var File|FileRepresentation|null $fileModel
*/
$fileModel = $this->file->makeVisible('Image')->find($args['id']);

// If the record is not found then 404 error, otherwise status is 200.
if ($fileModel === null) {
$responseBody = $responseBody
->setData(null)
->setStatus(ResponseCodes::HTTP_NOT_FOUND);
return $responseBody();
}

return $response
->withHeader('Content-Type', 'application/octet-stream')
->withHeader('Content-Disposition', 'attachment; filename="' . $fileModel->FileName . '"')
->withAddedHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->withHeader('Cache-Control', 'post-check=0, pre-check=0')
->withHeader('Pragma', 'no-cache')
->withBody((new StreamFactory())->createStream($fileModel->Image));
}
}
18 changes: 18 additions & 0 deletions app/Controllers/File/FileGetAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use Willow\Controllers\GetActionBase;
use Willow\Models\File;

class FileGetAction extends GetActionBase
{
/**
* FileGetAction constructor.
* @param File $model
*/
public function __construct(File $model) {
$this->model = $model;
}
}
59 changes: 59 additions & 0 deletions app/Controllers/File/FileLoadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use JsonException;
use Psr\Http\Message\ResponseInterface;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use Willow\Models\File;

class FileLoadAction
{
/**
* FileLoadAction - helper action to quickly rehydrate just the File records for a client
* @param File $file File model
*/
public function __construct(private File $file) {
}

/**
* Handle Load Request by returning all File records for a given client_id
* @param Request $request
* @param Response $response
* @param array $args
* @return ResponseInterface
* @throws JsonException
*/
public function __invoke(Request $request, Response $response, array $args): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');

// Load all models for the given client_id
$file = $this->file->clone();
$documents = $file
->where('ResidentId', '=', $args['client_id'])
->orderBy('Updated', 'desc')
->get(['Id', 'ResidentId', 'FileName', 'Description', 'MediaType', 'Size', 'Created', 'Updated']);

// If the record is not found then 404 error, otherwise status is 200.
if ($documents !== null && count($documents) > 0) {
$data = $documents->toArray();
$status = ResponseCodes::HTTP_OK;
} else {
$data = null;
$status = ResponseCodes::HTTP_NOT_FOUND;
}

// Set the status and data of the ResponseBody
$responseBody = $responseBody
->setData($data)
->setStatus($status);

// Return the response as JSON
return $responseBody();
}
}
18 changes: 18 additions & 0 deletions app/Controllers/File/FileUpdateAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use Willow\Controllers\WriteActionBase;
use Willow\Models\File;

class FileUpdateAction extends WriteActionBase
{
/**
* FIleUpdateAction constructor.
* @param File $model
*/
public function __construct(File $model) {
$this->model = $model;
}
}
65 changes: 65 additions & 0 deletions app/Controllers/File/FileUploadAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php
namespace Willow\Controllers\File;

use JsonException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use Willow\Models\File;
use Willow\Models\FileRepresentation;

class FileUploadAction
{
public function __construct(private File $file) {
}

/**
* @param Request $request
* @param Response $response
* @return ResponseInterface
* @throws JsonException
*/
public function __invoke(Request $request, Response $response): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');

$parsedRequest = $responseBody->getParsedRequest();

/**
* @var $files UploadedFileInterface[]
* @phpstan-ignore-next-line
*/
$files = $parsedRequest['uploaded_files'];

$file = $files['single_file'];
$clientId = $parsedRequest['client_id'];

/** @var File|FileRepresentation $document */
$document = clone $this->file;
$document->ResidentId = $clientId;
$document->Size = $file->getSize();
$document->FileName = $file->getClientFilename() ?? 'unknown';
$document->MediaType = $file->getClientMediaType();
$document->Image = $file->getStream()->getContents();
if ($document->save()) {
$responseBody = $responseBody->setData(
[
'Id' => $document->Id,
'Size' => $document->Size,
'FileName' => $document->FileName,
'Type' => $document->MediaType
]
)->setStatus(ResponseCodes::HTTP_OK);
return $responseBody();
}

$responseBody = $responseBody
->setData(null)
->setStatus(ResponseCodes::HTTP_INTERNAL_SERVER_ERROR)
->setMessage('Unable to save file');
return $responseBody();
}
}
70 changes: 70 additions & 0 deletions app/Controllers/File/FileUploadValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);

namespace Willow\Controllers\File;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Request;
use Willow\Middleware\ResponseBody;
use Willow\Middleware\ResponseCodes;
use JsonException;
use Respect\Validation\Validator as V;

class FileUploadValidator
{
/**
* @param Request $request
* @param RequestHandler $handler
* @return ResponseInterface
* @throws JsonException
*/
public function __invoke(Request $request, RequestHandler $handler): ResponseInterface {
/** @var ResponseBody $responseBody */
$responseBody = $request->getAttribute('response_body');
$parsedRequest = $responseBody->getParsedRequest();

/**
* @var $files UploadedFileInterface[]
* @phpstan-ignore-next-line
*/
$files = $parsedRequest['uploaded_files'] ?? [];

// Only 1 file allowed to be uploaded
if (count($files) !== 1) {
$responseBody->registerParam('required', 'uploaded_files', 'array', 'There must be only one uploaded file');
}

// File is hard coded as 'single_file'
$file = $files['single_file'] ?? null;

// The single_file array element label is required
if ($file === null) {
$responseBody->registerParam('required', 'single_file', 'file', 'File must be labeled as single_file');
}

// File size must be under 100MB
if ($file && $file->getSize() > 104_857_600) {
$responseBody->registerParam('invalid', 'single_file', 'file', 'File size exceeds maximum allowed');
}

// client_id is required and must be an integer
$clientId = $parsedRequest['client_id'] ?? null;
if (!V::notEmpty()->validate($clientId)) {
$responseBody->registerParam('required', 'client_id', 'integer', 'client_id is empty or invalid');
} else {
if (!V::intVal()->validate($clientId)) {
$responseBody->registerParam('invalid', 'client_id', 'integer', 'client_id must be an integer');
}
}

// If there are any invalid or missing required then send bad request response
if ($responseBody->hasMissingRequiredOrInvalid()) {
$responseBody = $responseBody->setData(null)->setStatus(ResponseCodes::HTTP_BAD_REQUEST);
return $responseBody();
}

return $handler->handle($request);
}
}
Loading

0 comments on commit 253b0b2

Please sign in to comment.