Added basic attachment editing functionality

This commit is contained in:
Dan Brown 2016-10-11 20:39:11 +01:00
parent 89509b487a
commit 867fc8be64
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 307 additions and 79 deletions

View File

@ -57,6 +57,70 @@ class FileController extends Controller
return response()->json($file);
}
/**
* Update an uploaded file.
* @param int $fileId
* @param Request $request
* @return mixed
*/
public function uploadUpdate($fileId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$file = $this->file->findOrFail($fileId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('file-create', $file);
if (intval($pageId) !== intval($file->uploaded_to)) {
return $this->jsonError('Page mismatch during attached file update');
}
$uploadedFile = $request->file('file');
try {
$file = $this->fileService->saveUpdatedUpload($uploadedFile, $file);
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($file);
}
/**
* Update the details of an existing file.
* @param $fileId
* @param Request $request
* @return File|mixed
*/
public function update($fileId, Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'string|max:255',
'link' => 'url'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$file = $this->file->findOrFail($fileId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('file-create', $file);
if (intval($pageId) !== intval($file->uploaded_to)) {
return $this->jsonError('Page mismatch during attachment update');
}
$file = $this->fileService->updateFile($file, $request->all());
return $file;
}
/**
* Attach a link to a page as a file.
* @param Request $request
@ -66,8 +130,8 @@ class FileController extends Controller
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'string',
'link' => 'url'
'name' => 'string|max:255',
'link' => 'url|max:255'
]);
$pageId = $request->get('uploaded_to');

View File

@ -32,26 +32,7 @@ class FileService extends UploadService
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$fileName = $uploadedFile->getClientOriginalName();
$fileData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $fileBasePath;
$uploadFileName = $fileName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$filePath = $fileBasePath . $uploadFileName;
$fileStoragePath = $this->getStorageBasePath() . $filePath;
try {
$storage->put($fileStoragePath, $fileData);
} catch (Exception $e) {
throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
}
$filePath = $this->putFileInStorage($fileName, $uploadedFile);
$largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order');
$file = File::forceCreate([
@ -66,6 +47,30 @@ class FileService extends UploadService
return $file;
}
/**
* Store a upload, saving to a file and deleting any existing uploads
* attached to that file.
* @param UploadedFile $uploadedFile
* @param File $file
* @return File
* @throws FileUploadException
*/
public function saveUpdatedUpload(UploadedFile $uploadedFile, File $file)
{
if (!$file->external) {
$this->deleteFileInStorage($file);
}
$fileName = $uploadedFile->getClientOriginalName();
$filePath = $this->putFileInStorage($fileName, $uploadedFile);
$file->name = $fileName;
$file->path = $filePath;
$file->external = false;
$file->save();
return $file;
}
/**
* Save a new File attachment from a given link and name.
* @param string $name
@ -109,8 +114,29 @@ class FileService extends UploadService
}
}
/**
* Delete a file and any empty folders the deletion leaves.
* Update the details of a file.
* @param File $file
* @param $requestData
* @return File
*/
public function updateFile(File $file, $requestData)
{
$file->name = $requestData['name'];
if (isset($requestData['link']) && trim($requestData['link']) !== '') {
$file->path = $requestData['link'];
if (!$file->external) {
$this->deleteFileInStorage($file);
$file->external = true;
}
}
$file->save();
return $file;
}
/**
* Delete a File from the database and storage.
* @param File $file
*/
public function deleteFile(File $file)
@ -120,6 +146,17 @@ class FileService extends UploadService
return;
}
$this->deleteFileInStorage($file);
$file->delete();
}
/**
* Delete a file from the filesystem it sits on.
* Cleans any empty leftover folders.
* @param File $file
*/
protected function deleteFileInStorage(File $file)
{
$storedFilePath = $this->getStorageBasePath() . $file->path;
$storage = $this->getStorage();
$dirPath = dirname($storedFilePath);
@ -128,8 +165,37 @@ class FileService extends UploadService
if (count($storage->allFiles($dirPath)) === 0) {
$storage->deleteDirectory($dirPath);
}
}
$file->delete();
/**
* Store a file in storage with the given filename
* @param $fileName
* @param UploadedFile $uploadedFile
* @return string
* @throws FileUploadException
*/
protected function putFileInStorage($fileName, UploadedFile $uploadedFile)
{
$fileData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
$storageBasePath = $this->getStorageBasePath() . $fileBasePath;
$uploadFileName = $fileName;
while ($storage->exists($storageBasePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}
$filePath = $fileBasePath . $uploadFileName;
$fileStoragePath = $this->getStorageBasePath() . $filePath;
try {
$storage->put($fileStoragePath, $fileData);
} catch (Exception $e) {
throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
}
return $filePath;
}
}

View File

@ -536,6 +536,14 @@ module.exports = function (ngApp, events) {
const pageId = $scope.uploadedTo = $attrs.pageId;
let currentOrder = '';
$scope.files = [];
$scope.editFile = false;
$scope.file = getCleanFile();
function getCleanFile() {
return {
page_id: pageId
};
}
// Angular-UI-Sort options
$scope.sortOptions = {
@ -559,15 +567,16 @@ module.exports = function (ngApp, events) {
currentOrder = newOrder;
$http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => {
events.emit('success', resp.data.message);
});
}, checkError);
}
/**
* Used by dropzone to get the endpoint to upload to.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return window.baseUrl('/files/upload');
$scope.getUploadUrl = function (file) {
let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
return window.baseUrl(`/files/upload${suffix}`);
};
/**
@ -578,7 +587,7 @@ module.exports = function (ngApp, events) {
$http.get(url).then(resp => {
$scope.files = resp.data;
currentOrder = resp.data.map(file => {return file.id}).join(':');
});
}, checkError);
}
getFiles();
@ -595,6 +604,24 @@ module.exports = function (ngApp, events) {
events.emit('success', 'File uploaded');
};
/**
* Upload and overwrite an existing file.
* @param file
* @param data
*/
$scope.uploadSuccessUpdate = function (file, data) {
$scope.$apply(() => {
let search = filesIndexOf(data);
if (search !== -1) $scope.files[search] = file;
if ($scope.editFile) {
$scope.editFile = data;
data.link = '';
}
});
events.emit('success', 'File updated');
};
/**
* Delete a file from the server and, on success, the local listing.
* @param file
@ -603,21 +630,77 @@ module.exports = function (ngApp, events) {
$http.delete(`/files/${file.id}`).then(resp => {
events.emit('success', resp.data.message);
$scope.files.splice($scope.files.indexOf(file), 1);
});
}, checkError);
};
$scope.attachLinkSubmit = function(fileName, fileLink) {
$http.post('/files/link', {
uploaded_to: pageId,
name: fileName,
link: fileLink
}).then(resp => {
/**
* Attach a link to a page.
* @param fileName
* @param fileLink
*/
$scope.attachLinkSubmit = function(file) {
$http.post('/files/link', file).then(resp => {
$scope.files.unshift(resp.data);
events.emit('success', 'Link attached');
});
$scope.fileName = $scope.fileLink = '';
$scope.file = getCleanFile();
}, checkError);
};
/**
* Start the edit mode for a file.
* @param fileId
*/
$scope.startEdit = function(file) {
$scope.editFile = angular.copy(file);
if (!file.external) $scope.editFile.link = '';
};
/**
* Cancel edit mode
*/
$scope.cancelEdit = function() {
$scope.editFile = false;
};
/**
* Update the name and link of a file.
* @param file
*/
$scope.updateFile = function(file) {
$http.put(`/files/${file.id}`, file).then(resp => {
let search = filesIndexOf(resp.data);
if (search !== -1) $scope.files[search] = file;
if ($scope.editFile && !file.external) {
$scope.editFile.link = '';
}
events.emit('success', 'Attachment details updated');
});
};
/**
* Search the local files via another file object.
* Used to search via object copies.
* @param file
* @returns int
*/
function filesIndexOf(file) {
for (let i = 0; i < $scope.files.length; i++) {
if ($scope.files[i].id == file.id) return file.id;
}
return -1;
}
/**
* Check for an error response in a ajax request.
* @param response
*/
function checkError(response) {
if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
events.emit('error', response.data.error);
}
}
}]);
};

View File

@ -116,6 +116,7 @@ module.exports = function (ngApp, events) {
uploadedTo: '@'
},
link: function (scope, element, attrs) {
if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
url: scope.uploadUrl,
init: function () {

View File

@ -228,21 +228,6 @@
padding-top: $-s;
position: relative;
}
button.pos {
position: absolute;
bottom: 0;
display: block;
width: 100%;
padding: $-s;
height: 45px;
border: 0;
margin: 0;
box-shadow: none;
border-radius: 0;
&:hover{
box-shadow: none;
}
}
.handle {
user-select: none;
cursor: move;

View File

@ -4,7 +4,9 @@
<div class="tabs primary-background-light">
<span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
<span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
<span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
@if(userCan('file-create-all'))
<span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
@endif
</div>
<div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
@ -35,35 +37,60 @@
</div>
</div>
<div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
<h4>Attached Files</h4>
<div class="padded files">
<p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p>
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
@if(userCan('file-create-all'))
<div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
<h4>Attached Files</h4>
<div class="padded files">
<hr class="even">
<div id="file-list" ng-show="!editFile">
<p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p>
<drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
<hr class="even">
<div class="form-group">
<label for="attachment-via-link">File Name</label>
<input type="text" placeholder="File name" ng-model="file.name">
</div>
<div class="form-group">
<label for="attachment-via-link">Link to file</label>
<input type="text" placeholder="File url" ng-model="file.link">
</div>
<button type="button" ng-click="attachLinkSubmit(file)" class="button pos">Attach</button>
<table class="no-style" tag-autosuggestions style="width: 100%;">
<tbody ui-sortable="sortOptions" ng-model="files" >
<tr ng-repeat="file in files track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
<td ng-bind="file.name"></td>
<td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
<td width="10" ng-click="startEdit(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
</tr>
</tbody>
</table>
</div>
<div id="file-edit" ng-if="editFile">
<h5>Edit File</h5>
<div class="form-group">
<label for="attachment-name-edit">File Name</label>
<input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
</div>
<hr class="even">
<drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
<hr class="even">
<div class="form-group">
<label for="attachment-link-edit">Link to file</label>
<input type="text" id="attachment-link-edit" placeholder="File url" ng-model="editFile.link">
</div>
<button type="button" class="button" ng-click="cancelEdit()">Back</button>
<button type="button" class="button pos" ng-click="updateFile(editFile)">Save</button>
</div>
<div class="form-group">
<label for="attachment-via-link">File Name</label>
<input type="text" placeholder="File name" ng-model="fileName">
</div>
<div class="form-group">
<label for="attachment-via-link">Link to file</label>
<input type="text" placeholder="File url" ng-model="fileLink">
</div>
<button type="button" ng-click="attachLinkSubmit(fileName, fileLink)" class="button pos">Attach</button>
<table class="no-style" tag-autosuggestions style="width: 100%;">
<tbody ui-sortable="sortOptions" ng-model="files" >
<tr ng-repeat="file in files track by $index">
<td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
<td ng-bind="file.name"></td>
<td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
</tr>
</tbody>
</table>
</div>
</div>
@endif
</div>

View File

@ -90,7 +90,9 @@ Route::group(['middleware' => 'auth'], function () {
// File routes
Route::get('/files/{id}', 'FileController@get');
Route::post('/files/upload', 'FileController@upload');
Route::post('/files/upload/{id}', 'FileController@uploadUpdate');
Route::post('/files/link', 'FileController@attachLink');
Route::put('/files/{id}', 'FileController@update');
Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
Route::delete('/files/{id}', 'FileController@delete');