Dan Brown

Added basic attachment editing functionality

...@@ -58,6 +58,70 @@ class FileController extends Controller ...@@ -58,6 +58,70 @@ class FileController extends Controller
58 } 58 }
59 59
60 /** 60 /**
61 + * Update an uploaded file.
62 + * @param int $fileId
63 + * @param Request $request
64 + * @return mixed
65 + */
66 + public function uploadUpdate($fileId, Request $request)
67 + {
68 + $this->validate($request, [
69 + 'uploaded_to' => 'required|integer|exists:pages,id',
70 + 'file' => 'required|file'
71 + ]);
72 +
73 + $pageId = $request->get('uploaded_to');
74 + $page = $this->pageRepo->getById($pageId);
75 + $file = $this->file->findOrFail($fileId);
76 +
77 + $this->checkOwnablePermission('page-update', $page);
78 + $this->checkOwnablePermission('file-create', $file);
79 +
80 + if (intval($pageId) !== intval($file->uploaded_to)) {
81 + return $this->jsonError('Page mismatch during attached file update');
82 + }
83 +
84 + $uploadedFile = $request->file('file');
85 +
86 + try {
87 + $file = $this->fileService->saveUpdatedUpload($uploadedFile, $file);
88 + } catch (FileUploadException $e) {
89 + return response($e->getMessage(), 500);
90 + }
91 +
92 + return response()->json($file);
93 + }
94 +
95 + /**
96 + * Update the details of an existing file.
97 + * @param $fileId
98 + * @param Request $request
99 + * @return File|mixed
100 + */
101 + public function update($fileId, Request $request)
102 + {
103 + $this->validate($request, [
104 + 'uploaded_to' => 'required|integer|exists:pages,id',
105 + 'name' => 'string|max:255',
106 + 'link' => 'url'
107 + ]);
108 +
109 + $pageId = $request->get('uploaded_to');
110 + $page = $this->pageRepo->getById($pageId);
111 + $file = $this->file->findOrFail($fileId);
112 +
113 + $this->checkOwnablePermission('page-update', $page);
114 + $this->checkOwnablePermission('file-create', $file);
115 +
116 + if (intval($pageId) !== intval($file->uploaded_to)) {
117 + return $this->jsonError('Page mismatch during attachment update');
118 + }
119 +
120 + $file = $this->fileService->updateFile($file, $request->all());
121 + return $file;
122 + }
123 +
124 + /**
61 * Attach a link to a page as a file. 125 * Attach a link to a page as a file.
62 * @param Request $request 126 * @param Request $request
63 * @return mixed 127 * @return mixed
...@@ -66,8 +130,8 @@ class FileController extends Controller ...@@ -66,8 +130,8 @@ class FileController extends Controller
66 { 130 {
67 $this->validate($request, [ 131 $this->validate($request, [
68 'uploaded_to' => 'required|integer|exists:pages,id', 132 'uploaded_to' => 'required|integer|exists:pages,id',
69 - 'name' => 'string', 133 + 'name' => 'string|max:255',
70 - 'link' => 'url' 134 + 'link' => 'url|max:255'
71 ]); 135 ]);
72 136
73 $pageId = $request->get('uploaded_to'); 137 $pageId = $request->get('uploaded_to');
......
...@@ -32,26 +32,7 @@ class FileService extends UploadService ...@@ -32,26 +32,7 @@ class FileService extends UploadService
32 public function saveNewUpload(UploadedFile $uploadedFile, $page_id) 32 public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
33 { 33 {
34 $fileName = $uploadedFile->getClientOriginalName(); 34 $fileName = $uploadedFile->getClientOriginalName();
35 - $fileData = file_get_contents($uploadedFile->getRealPath()); 35 + $filePath = $this->putFileInStorage($fileName, $uploadedFile);
36 -
37 - $storage = $this->getStorage();
38 - $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
39 - $storageBasePath = $this->getStorageBasePath() . $fileBasePath;
40 -
41 - $uploadFileName = $fileName;
42 - while ($storage->exists($storageBasePath . $uploadFileName)) {
43 - $uploadFileName = str_random(3) . $uploadFileName;
44 - }
45 -
46 - $filePath = $fileBasePath . $uploadFileName;
47 - $fileStoragePath = $this->getStorageBasePath() . $filePath;
48 -
49 - try {
50 - $storage->put($fileStoragePath, $fileData);
51 - } catch (Exception $e) {
52 - throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
53 - }
54 -
55 $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order'); 36 $largestExistingOrder = File::where('uploaded_to', '=', $page_id)->max('order');
56 37
57 $file = File::forceCreate([ 38 $file = File::forceCreate([
...@@ -67,6 +48,30 @@ class FileService extends UploadService ...@@ -67,6 +48,30 @@ class FileService extends UploadService
67 } 48 }
68 49
69 /** 50 /**
51 + * Store a upload, saving to a file and deleting any existing uploads
52 + * attached to that file.
53 + * @param UploadedFile $uploadedFile
54 + * @param File $file
55 + * @return File
56 + * @throws FileUploadException
57 + */
58 + public function saveUpdatedUpload(UploadedFile $uploadedFile, File $file)
59 + {
60 + if (!$file->external) {
61 + $this->deleteFileInStorage($file);
62 + }
63 +
64 + $fileName = $uploadedFile->getClientOriginalName();
65 + $filePath = $this->putFileInStorage($fileName, $uploadedFile);
66 +
67 + $file->name = $fileName;
68 + $file->path = $filePath;
69 + $file->external = false;
70 + $file->save();
71 + return $file;
72 + }
73 +
74 + /**
70 * Save a new File attachment from a given link and name. 75 * Save a new File attachment from a given link and name.
71 * @param string $name 76 * @param string $name
72 * @param string $link 77 * @param string $link
...@@ -109,8 +114,29 @@ class FileService extends UploadService ...@@ -109,8 +114,29 @@ class FileService extends UploadService
109 } 114 }
110 } 115 }
111 116
117 +
118 + /**
119 + * Update the details of a file.
120 + * @param File $file
121 + * @param $requestData
122 + * @return File
123 + */
124 + public function updateFile(File $file, $requestData)
125 + {
126 + $file->name = $requestData['name'];
127 + if (isset($requestData['link']) && trim($requestData['link']) !== '') {
128 + $file->path = $requestData['link'];
129 + if (!$file->external) {
130 + $this->deleteFileInStorage($file);
131 + $file->external = true;
132 + }
133 + }
134 + $file->save();
135 + return $file;
136 + }
137 +
112 /** 138 /**
113 - * Delete a file and any empty folders the deletion leaves. 139 + * Delete a File from the database and storage.
114 * @param File $file 140 * @param File $file
115 */ 141 */
116 public function deleteFile(File $file) 142 public function deleteFile(File $file)
...@@ -120,6 +146,17 @@ class FileService extends UploadService ...@@ -120,6 +146,17 @@ class FileService extends UploadService
120 return; 146 return;
121 } 147 }
122 148
149 + $this->deleteFileInStorage($file);
150 + $file->delete();
151 + }
152 +
153 + /**
154 + * Delete a file from the filesystem it sits on.
155 + * Cleans any empty leftover folders.
156 + * @param File $file
157 + */
158 + protected function deleteFileInStorage(File $file)
159 + {
123 $storedFilePath = $this->getStorageBasePath() . $file->path; 160 $storedFilePath = $this->getStorageBasePath() . $file->path;
124 $storage = $this->getStorage(); 161 $storage = $this->getStorage();
125 $dirPath = dirname($storedFilePath); 162 $dirPath = dirname($storedFilePath);
...@@ -128,8 +165,37 @@ class FileService extends UploadService ...@@ -128,8 +165,37 @@ class FileService extends UploadService
128 if (count($storage->allFiles($dirPath)) === 0) { 165 if (count($storage->allFiles($dirPath)) === 0) {
129 $storage->deleteDirectory($dirPath); 166 $storage->deleteDirectory($dirPath);
130 } 167 }
168 + }
131 169
132 - $file->delete(); 170 + /**
171 + * Store a file in storage with the given filename
172 + * @param $fileName
173 + * @param UploadedFile $uploadedFile
174 + * @return string
175 + * @throws FileUploadException
176 + */
177 + protected function putFileInStorage($fileName, UploadedFile $uploadedFile)
178 + {
179 + $fileData = file_get_contents($uploadedFile->getRealPath());
180 +
181 + $storage = $this->getStorage();
182 + $fileBasePath = 'uploads/files/' . Date('Y-m-M') . '/';
183 + $storageBasePath = $this->getStorageBasePath() . $fileBasePath;
184 +
185 + $uploadFileName = $fileName;
186 + while ($storage->exists($storageBasePath . $uploadFileName)) {
187 + $uploadFileName = str_random(3) . $uploadFileName;
188 + }
189 +
190 + $filePath = $fileBasePath . $uploadFileName;
191 + $fileStoragePath = $this->getStorageBasePath() . $filePath;
192 +
193 + try {
194 + $storage->put($fileStoragePath, $fileData);
195 + } catch (Exception $e) {
196 + throw new FileUploadException('File path ' . $fileStoragePath . ' could not be uploaded to. Ensure it is writable to the server.');
197 + }
198 + return $filePath;
133 } 199 }
134 200
135 } 201 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -536,6 +536,14 @@ module.exports = function (ngApp, events) { ...@@ -536,6 +536,14 @@ module.exports = function (ngApp, events) {
536 const pageId = $scope.uploadedTo = $attrs.pageId; 536 const pageId = $scope.uploadedTo = $attrs.pageId;
537 let currentOrder = ''; 537 let currentOrder = '';
538 $scope.files = []; 538 $scope.files = [];
539 + $scope.editFile = false;
540 + $scope.file = getCleanFile();
541 +
542 + function getCleanFile() {
543 + return {
544 + page_id: pageId
545 + };
546 + }
539 547
540 // Angular-UI-Sort options 548 // Angular-UI-Sort options
541 $scope.sortOptions = { 549 $scope.sortOptions = {
...@@ -559,15 +567,16 @@ module.exports = function (ngApp, events) { ...@@ -559,15 +567,16 @@ module.exports = function (ngApp, events) {
559 currentOrder = newOrder; 567 currentOrder = newOrder;
560 $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => { 568 $http.put(`/files/sort/page/${pageId}`, {files: $scope.files}).then(resp => {
561 events.emit('success', resp.data.message); 569 events.emit('success', resp.data.message);
562 - }); 570 + }, checkError);
563 } 571 }
564 572
565 /** 573 /**
566 * Used by dropzone to get the endpoint to upload to. 574 * Used by dropzone to get the endpoint to upload to.
567 * @returns {string} 575 * @returns {string}
568 */ 576 */
569 - $scope.getUploadUrl = function () { 577 + $scope.getUploadUrl = function (file) {
570 - return window.baseUrl('/files/upload'); 578 + let suffix = (typeof file !== 'undefined') ? `/${file.id}` : '';
579 + return window.baseUrl(`/files/upload${suffix}`);
571 }; 580 };
572 581
573 /** 582 /**
...@@ -578,7 +587,7 @@ module.exports = function (ngApp, events) { ...@@ -578,7 +587,7 @@ module.exports = function (ngApp, events) {
578 $http.get(url).then(resp => { 587 $http.get(url).then(resp => {
579 $scope.files = resp.data; 588 $scope.files = resp.data;
580 currentOrder = resp.data.map(file => {return file.id}).join(':'); 589 currentOrder = resp.data.map(file => {return file.id}).join(':');
581 - }); 590 + }, checkError);
582 } 591 }
583 getFiles(); 592 getFiles();
584 593
...@@ -596,6 +605,24 @@ module.exports = function (ngApp, events) { ...@@ -596,6 +605,24 @@ module.exports = function (ngApp, events) {
596 }; 605 };
597 606
598 /** 607 /**
608 + * Upload and overwrite an existing file.
609 + * @param file
610 + * @param data
611 + */
612 + $scope.uploadSuccessUpdate = function (file, data) {
613 + $scope.$apply(() => {
614 + let search = filesIndexOf(data);
615 + if (search !== -1) $scope.files[search] = file;
616 +
617 + if ($scope.editFile) {
618 + $scope.editFile = data;
619 + data.link = '';
620 + }
621 + });
622 + events.emit('success', 'File updated');
623 + };
624 +
625 + /**
599 * Delete a file from the server and, on success, the local listing. 626 * Delete a file from the server and, on success, the local listing.
600 * @param file 627 * @param file
601 */ 628 */
...@@ -603,21 +630,77 @@ module.exports = function (ngApp, events) { ...@@ -603,21 +630,77 @@ module.exports = function (ngApp, events) {
603 $http.delete(`/files/${file.id}`).then(resp => { 630 $http.delete(`/files/${file.id}`).then(resp => {
604 events.emit('success', resp.data.message); 631 events.emit('success', resp.data.message);
605 $scope.files.splice($scope.files.indexOf(file), 1); 632 $scope.files.splice($scope.files.indexOf(file), 1);
606 - }); 633 + }, checkError);
607 }; 634 };
608 635
609 - $scope.attachLinkSubmit = function(fileName, fileLink) { 636 + /**
610 - $http.post('/files/link', { 637 + * Attach a link to a page.
611 - uploaded_to: pageId, 638 + * @param fileName
612 - name: fileName, 639 + * @param fileLink
613 - link: fileLink 640 + */
614 - }).then(resp => { 641 + $scope.attachLinkSubmit = function(file) {
642 + $http.post('/files/link', file).then(resp => {
615 $scope.files.unshift(resp.data); 643 $scope.files.unshift(resp.data);
616 events.emit('success', 'Link attached'); 644 events.emit('success', 'Link attached');
645 + $scope.file = getCleanFile();
646 + }, checkError);
647 + };
648 +
649 + /**
650 + * Start the edit mode for a file.
651 + * @param fileId
652 + */
653 + $scope.startEdit = function(file) {
654 + $scope.editFile = angular.copy(file);
655 + if (!file.external) $scope.editFile.link = '';
656 + };
657 +
658 + /**
659 + * Cancel edit mode
660 + */
661 + $scope.cancelEdit = function() {
662 + $scope.editFile = false;
663 + };
664 +
665 + /**
666 + * Update the name and link of a file.
667 + * @param file
668 + */
669 + $scope.updateFile = function(file) {
670 + $http.put(`/files/${file.id}`, file).then(resp => {
671 + let search = filesIndexOf(resp.data);
672 + if (search !== -1) $scope.files[search] = file;
673 +
674 + if ($scope.editFile && !file.external) {
675 + $scope.editFile.link = '';
676 + }
677 + events.emit('success', 'Attachment details updated');
617 }); 678 });
618 - $scope.fileName = $scope.fileLink = '';
619 }; 679 };
620 680
681 + /**
682 + * Search the local files via another file object.
683 + * Used to search via object copies.
684 + * @param file
685 + * @returns int
686 + */
687 + function filesIndexOf(file) {
688 + for (let i = 0; i < $scope.files.length; i++) {
689 + if ($scope.files[i].id == file.id) return file.id;
690 + }
691 + return -1;
692 + }
693 +
694 + /**
695 + * Check for an error response in a ajax request.
696 + * @param response
697 + */
698 + function checkError(response) {
699 + if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') {
700 + events.emit('error', response.data.error);
701 + }
702 + }
703 +
621 }]); 704 }]);
622 705
623 }; 706 };
......
...@@ -116,6 +116,7 @@ module.exports = function (ngApp, events) { ...@@ -116,6 +116,7 @@ module.exports = function (ngApp, events) {
116 uploadedTo: '@' 116 uploadedTo: '@'
117 }, 117 },
118 link: function (scope, element, attrs) { 118 link: function (scope, element, attrs) {
119 + if (attrs.placeholder) element[0].querySelector('.dz-message').textContent = attrs.placeholder;
119 var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { 120 var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), {
120 url: scope.uploadUrl, 121 url: scope.uploadUrl,
121 init: function () { 122 init: function () {
......
...@@ -228,21 +228,6 @@ ...@@ -228,21 +228,6 @@
228 padding-top: $-s; 228 padding-top: $-s;
229 position: relative; 229 position: relative;
230 } 230 }
231 - button.pos {
232 - position: absolute;
233 - bottom: 0;
234 - display: block;
235 - width: 100%;
236 - padding: $-s;
237 - height: 45px;
238 - border: 0;
239 - margin: 0;
240 - box-shadow: none;
241 - border-radius: 0;
242 - &:hover{
243 - box-shadow: none;
244 - }
245 - }
246 .handle { 231 .handle {
247 user-select: none; 232 user-select: none;
248 cursor: move; 233 cursor: move;
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
4 <div class="tabs primary-background-light"> 4 <div class="tabs primary-background-light">
5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span> 5 <span toolbox-toggle><i class="zmdi zmdi-caret-left-circle"></i></span>
6 <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span> 6 <span tab-button="tags" title="Page Tags" class="active"><i class="zmdi zmdi-tag"></i></span>
7 - <span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span> 7 + @if(userCan('file-create-all'))
8 + <span tab-button="files" title="Attachments"><i class="zmdi zmdi-attachment"></i></span>
9 + @endif
8 </div> 10 </div>
9 11
10 <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}"> 12 <div tab-content="tags" ng-controller="PageTagController" page-id="{{ $page->id or 0 }}">
...@@ -35,35 +37,60 @@ ...@@ -35,35 +37,60 @@
35 </div> 37 </div>
36 </div> 38 </div>
37 39
38 - <div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}"> 40 + @if(userCan('file-create-all'))
39 - <h4>Attached Files</h4> 41 + <div tab-content="files" ng-controller="PageAttachmentController" page-id="{{ $page->id or 0 }}">
40 - <div class="padded files"> 42 + <h4>Attached Files</h4>
41 - <p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p> 43 + <div class="padded files">
42 - <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
43 44
44 - <hr class="even"> 45 + <div id="file-list" ng-show="!editFile">
46 + <p class="muted small">Upload some files to display on your page. This are visible in the page sidebar.</p>
47 + <drop-zone upload-url="@{{getUploadUrl()}}" uploaded-to="@{{uploadedTo}}" event-success="uploadSuccess"></drop-zone>
45 48
46 - <div class="form-group"> 49 + <hr class="even">
47 - <label for="attachment-via-link">File Name</label>
48 - <input type="text" placeholder="File name" ng-model="fileName">
49 - </div>
50 - <div class="form-group">
51 - <label for="attachment-via-link">Link to file</label>
52 - <input type="text" placeholder="File url" ng-model="fileLink">
53 - </div>
54 - <button type="button" ng-click="attachLinkSubmit(fileName, fileLink)" class="button pos">Attach</button>
55 50
51 + <div class="form-group">
52 + <label for="attachment-via-link">File Name</label>
53 + <input type="text" placeholder="File name" ng-model="file.name">
54 + </div>
55 + <div class="form-group">
56 + <label for="attachment-via-link">Link to file</label>
57 + <input type="text" placeholder="File url" ng-model="file.link">
58 + </div>
59 + <button type="button" ng-click="attachLinkSubmit(file)" class="button pos">Attach</button>
56 60
57 - <table class="no-style" tag-autosuggestions style="width: 100%;"> 61 +
58 - <tbody ui-sortable="sortOptions" ng-model="files" > 62 + <table class="no-style" tag-autosuggestions style="width: 100%;">
59 - <tr ng-repeat="file in files track by $index"> 63 + <tbody ui-sortable="sortOptions" ng-model="files" >
60 - <td width="20" ><i class="handle zmdi zmdi-menu"></i></td> 64 + <tr ng-repeat="file in files track by $index">
61 - <td ng-bind="file.name"></td> 65 + <td width="20" ><i class="handle zmdi zmdi-menu"></i></td>
62 - <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td> 66 + <td ng-bind="file.name"></td>
63 - </tr> 67 + <td width="10" ng-click="deleteFile(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-close"></i></td>
64 - </tbody> 68 + <td width="10" ng-click="startEdit(file)" class="text-center text-neg" style="padding: 0;"><i class="zmdi zmdi-edit"></i></td>
65 - </table> 69 + </tr>
70 + </tbody>
71 + </table>
72 + </div>
73 +
74 + <div id="file-edit" ng-if="editFile">
75 + <h5>Edit File</h5>
76 + <div class="form-group">
77 + <label for="attachment-name-edit">File Name</label>
78 + <input type="text" id="attachment-name-edit" placeholder="File name" ng-model="editFile.name">
79 + </div>
80 + <hr class="even">
81 + <drop-zone upload-url="@{{getUploadUrl(editFile)}}" uploaded-to="@{{uploadedTo}}" placeholder="Drop files or click here to upload and overwrite" event-success="uploadSuccessUpdate"></drop-zone>
82 + <hr class="even">
83 + <div class="form-group">
84 + <label for="attachment-link-edit">Link to file</label>
85 + <input type="text" id="attachment-link-edit" placeholder="File url" ng-model="editFile.link">
86 + </div>
87 +
88 + <button type="button" class="button" ng-click="cancelEdit()">Back</button>
89 + <button type="button" class="button pos" ng-click="updateFile(editFile)">Save</button>
90 + </div>
91 +
92 + </div>
66 </div> 93 </div>
67 - </div> 94 + @endif
68 95
69 </div> 96 </div>
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -90,7 +90,9 @@ Route::group(['middleware' => 'auth'], function () { ...@@ -90,7 +90,9 @@ Route::group(['middleware' => 'auth'], function () {
90 // File routes 90 // File routes
91 Route::get('/files/{id}', 'FileController@get'); 91 Route::get('/files/{id}', 'FileController@get');
92 Route::post('/files/upload', 'FileController@upload'); 92 Route::post('/files/upload', 'FileController@upload');
93 + Route::post('/files/upload/{id}', 'FileController@uploadUpdate');
93 Route::post('/files/link', 'FileController@attachLink'); 94 Route::post('/files/link', 'FileController@attachLink');
95 + Route::put('/files/{id}', 'FileController@update');
94 Route::get('/files/get/page/{pageId}', 'FileController@listForPage'); 96 Route::get('/files/get/page/{pageId}', 'FileController@listForPage');
95 Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage'); 97 Route::put('/files/sort/page/{pageId}', 'FileController@sortForPage');
96 Route::delete('/files/{id}', 'FileController@delete'); 98 Route::delete('/files/{id}', 'FileController@delete');
......