mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Added basic attachment editing functionality
This commit is contained in:
parent
89509b487a
commit
867fc8be64
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
};
|
||||
|
@ -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 () {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user