]> BookStack Code Mirror - bookstack/commitdiff
Added basic attachment editing functionality
authorDan Brown <redacted>
Tue, 11 Oct 2016 19:39:11 +0000 (20:39 +0100)
committerDan Brown <redacted>
Tue, 11 Oct 2016 19:39:11 +0000 (20:39 +0100)
app/Http/Controllers/FileController.php
app/Services/FileService.php
resources/assets/js/controllers.js
resources/assets/js/directives.js
resources/assets/sass/_pages.scss
resources/views/pages/form-toolbox.blade.php
routes/web.php

index 9486298b2b5ad00ddd4de014e9bb99929eb9c6e2..4cdcf66dceb8dafac6fd31d8f864b79b8593c4dd 100644 (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');
index 3674209a82055ef72bfb5624c93d1bf39f88cf84..a04d840018a92ce1bcf4ce6416ec9ff4083ac530 100644 (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
         }
     }
 
+
+    /**
+     * 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 and any empty folders the deletion leaves.
+     * 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;
     }
 
 }
\ No newline at end of file
index bc2d43fc8eb0e5e933cd46637b31821480de3b18..40466876822539661ab769f67f75700c5e0a7437 100644 (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.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');
                 });
-                $scope.fileName = $scope.fileLink = '';
             };
 
+            /**
+             * 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);
+                }
+            }
+
         }]);
 
 };
index 933bbf5ff5e96c0ad9631a39f72d5f558e880b49..82cb128f34a3a999476e667184cdacbb98cd3f2f 100644 (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 () {
index e608295a806756b084f1ad7e12867933c2f74d6d..1f79c38c821325a264e321997813c0586b1213ad 100755 (executable)
     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;
index e1481f50013f938382e02daa56c2767acf801d71..e6b761c28b16043a0a048d772d23b5abe27c56d1 100644 (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 }}">
         </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>
 
-            <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>
+                    <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>
-                </tr>
-                </tbody>
-            </table>
+
+                    <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>
         </div>
-    </div>
+    @endif
 
 </div>
\ No newline at end of file
index a55eb224f3143ec0767a5177a17b65d3e42508f7..45957ac626e2e1cb34bdb71ddefbe2278b24bcb5 100644 (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');
Morty Proxy This is a proxified and sanitized view of the page, visit original site.