Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Bendik Engebretsen 105 posts 202 karma points
    Nov 02, 2016 @ 20:41
    Bendik Engebretsen
    6

    File upload in backoffice custom section

    While trying to implement a file upload form in a backoffice custom section, I stumbled onto quite a few obstacles and outdated/incorrect information in various forums (not only in Our;-) So, after I finally got it to work, I thought I'd share my solution and code here for anyone else interested.

    First of all, you need to have your backoffice custom section up and running. There's a good article about how to do one here. From here, I will assume that you have one called "MySection".

    The next thing you'll need is the ng-file-upload AngularJS directive from danialfarid. If you're in Visual Studio, there's a NuGet package called "angular-file-upload" which will install the .js files. Otherwise, get them from GitHub here. Ensure that you have the ng-file-upload-all.js file in your ~/Scripts folder.

    ng-file-upload has a lot of functionality (drag-drop, etc.), some of which I may explore later, but for now I opted for a very simple file input control and an upload button. So in your edit.html markup for your custom section something like this:

                    <div class="umb-pane">
    
    
                    <umb-control-group label="File" description="File to upload">
                        <input type="file" class="umb-editor umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
                    </umb-control-group>
    
                    <div class="umb-tab-buttons" detect-fold>
                        <div class="btn-group">
                            <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="uploadFile()">
                                Upload
                            </button>
                        </div>
                    </div>
                </div>
    

    Note the use of the ng- and ngf- attributes. When you select a file (only one in my case), fileSelected(files) is called to store the local path and filename in the $scope. So that when the Upload button is clicked, and fileUpload() is called, it knows what file(s) to upload. I chose to put these two in the edit.controller.js for my section, so add this code:

        $scope.fileSelected = function (files) {
            // In this case, files is just a single path/filename
            $scope.file = files;
        };
    
        $scope.uploadFile = function () {
            if (!$scope.isUploading) {
                if ($scope.file) {
                    $scope.isUploading = true;
                    fileUploadService.uploadFileToServer($scope.file)
                        .then(function (response) {
                            if (response) {
                                notificationsService.success("Success", "Saved to server with the filename " + response);
                            }
                            $scope.isUploading = false;
                        }, function (reason) {
                        notificationsService.error("Error", "File import failed: " + reason.message);
                        $scope.isUploading = false;
                    });
                } else {
                    notificationsService.error("Error", "You must select a file to upload");
                    $scope.isUploading = false;
                }
            }
        };
    
        $scope.file = false;
        $scope.isUploading = false;
    

    Now we need the fileUploadService, which I chose to put in a separate file called file.upload.api.service.js:

    angular.module("umbraco.resources")
    .factory("fileUploadService", function ($http) {
        return {
            uploadFileToServer: function (file) {
                var request = {
                    file: file
                };
                return $http({
                    method: 'POST',
                    url: "backoffice/MySection/MySectionApi/UploadFileToServer",
                    headers: { 'Content-Type': undefined },
                    transformRequest: function (data) {
                        var formData = new FormData();
                        formData.append("file", data.file);
                        return formData;
                    },
                    data: request
                }).then(function (response) {
                    if (response) {
                        var fileName = response.data;
                        return fileName;
                    } else {
                        return false;
                    }
                });
            }
        };
    });
    

    To make sure the ng-file-upload-all.js and file.upload.api.service.js are loaded, we need to list them in the package.manifest for our section:

    {
    javascript: [
        '~/App_Plugins/MySection/backoffice/mySectionTree/edit.controller.js',
        '~/App_Plugins/MySection/mySection.resource.js',
        '~/App_Plugins/MySection/file.upload.api.service.js',
        '~/Scripts/ng-file-upload-all.js'
    ]}
    

    Next thing is to implement the actual uploading. I put it in my api controller for the section, i.e. MySectionApiController.cs. Add the following method to your MySectionApiController class:

        public async Task<HttpResponseMessage> UploadFileToServer()
        {
            if (!Request.Content.IsMimeMultipartContent())
            {
                throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
            }
    
            string uploadFolder = HttpContext.Current.Server.MapPath("~/App_Data/FileUploads");
            Directory.CreateDirectory(uploadFolder);
            var provider = new CustomMultipartFormDataStreamProvider(uploadFolder);
            var result = await Request.Content.ReadAsMultipartAsync(provider);
            var fileName = result.FileData.First().LocalFileName;
            HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
            response.Content = new StringContent(fileName);
            return response;
        }
    

    To override the generated filename, I also added this class (yes, I know this may have a security impact):

    public class CustomMultipartFormDataStreamProvider : MultipartFormDataStreamProvider
    {
        public CustomMultipartFormDataStreamProvider(string path) : base(path) { }
    
        public override string GetLocalFileName(HttpContentHeaders headers)
        {
            return headers.ContentDisposition.FileName.Replace("\"", string.Empty);
        }
    }
    

    And that's about it! (Hope I didn't forget anything. If someone tests it and finds that I did, please don't hesitate to let me know!)

  • Comment author was deleted

    Nov 02, 2016 @ 20:46

    Thanks for sharing :)

  • Sean Maloney 5 posts 79 karma points
    Nov 07, 2016 @ 10:03
    Sean Maloney
    0

    Hi Bendik,

    Thanks for posting this - had been trying to figure out how to approach it and now got it working after following this :-)

    I think there's a typo in the view:

    <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="**fileUpload**()">
    

    Should be:

    <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="**uploadFile**()">
    

    Thanks again, Sean.

  • Bendik Engebretsen 105 posts 202 karma points
    Nov 09, 2016 @ 09:31
    Bendik Engebretsen
    0

    Thanks, Sean! I have now updated the code in the original post. Glad to hear that my post was helpful.

  • djscorch 67 posts 106 karma points
    Jan 19, 2017 @ 11:26
    djscorch
    0

    We get the following error when selecting the file:

    Uncaught InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string
    

    Our implementation pretty much mirrors what you have above.

    Any ideas?

  • Bendik Engebretsen 105 posts 202 karma points
    Feb 07, 2017 @ 00:27
    Bendik Engebretsen
    0

    Tough one, but I think the error message indicates that your code (or someone else's) is somehow trying to modify the value of the <input type="file"...> element, which is not allowed. And this happens just after you've selected a file? In that case, it could seem that the offending code is

    $scope.fileSelected = function (files) {
        // In this case, files is just a single path/filename
        $scope.file = files;
    };
    

    But I fail to see what could be wrong with that simple function. This is probably a shot in the dark, but how about changing the name of the $scope.file variable in edit.controller.js to say $scope.theFile?

    Otherwise it could be something in the ng-file-upload directive that isn't cooperating well with your code/markup. But that's a huge chunk of js code to understand/debug, and it does seem to be very well tested and up-to-date. So in that case I guess you should try to work your way around it. Try various mods to your markup and controller and see if anything changes. There is also a forum for issues in ng-file-upload: https://github.com/danialfarid/ng-file-upload/issues. If you're really stuck it might be worth trying a post there.

  • hank 2 posts 92 karma points
    Feb 24, 2017 @ 18:20
    hank
    0

    Hi Bendik,

    I am trying to implement your version of this file upload functionality.

    When testing I can see the file input and the upload button but I get a javascript error saying:

    Uncaught ReferenceError: $scope is not defined
    

    Any chance you can help me out here or provide me a download link for the files implemented by you? I'm working on v7.5.6.

  • hank 2 posts 92 karma points
    Feb 26, 2017 @ 22:43
    hank
    100

    You can ignore the $scope error I posted before. I just copied the controller code as is in the controller.js file. But that was obvisously not enough, I forgot to put the controller declaration around it. So it should be something like this:

    angular.module("umbraco").controller("MyControllerName",
    function MyImportController($scope, $routeParams, $http, fileUploadService) {
        $scope.fileSelected = function (files) {
            // In this case, files is just a single path/filename
            $scope.file = files;
        };
        $scope.uploadFile = function () {
            if (!$scope.isUploading) {
                if ($scope.file) {
                    $scope.isUploading = true;
                    fileUploadService.uploadFileToServer($scope.file)
                        .then(function (response) {
                            if (response) {
                                notificationsService.success("Success", "Saved to server with the filename " + response);
                            }
                            $scope.isUploading = false;
                        }, function (reason) {
                            notificationsService.error("Error", "File import failed: " + reason.message);
                            $scope.isUploading = false;
                        });
                } else {
                    notificationsService.error("Error", "You must select a file to upload");
                    $scope.isUploading = false;
                }
            }
        };
        $scope.file = false;
        $scope.isUploading = false;
    });
    

    Now it works as it should be. Thank you!

  • Bendik Engebretsen 105 posts 202 karma points
    Mar 16, 2017 @ 13:09
    Bendik Engebretsen
    0

    Sorry for being late following up here, but you solved it already, great! I'm sure this will be helpful to others.

  • eugenewo 1 post 71 karma points
    Jan 12, 2020 @ 15:06
    eugenewo
    0

    do you have the full working demo? cant make it work

  • Sean Hynes 10 posts 142 karma points
    Mar 25, 2017 @ 16:03
    Sean Hynes
    0

    Hi

    New to umbraco and also angular js - but this post has been useful. However, struggling with getting it to work if anyone can help

    I've using virtually an exact copy of the code above.

    1) When I bind the controller to my edit.html file I seem to be losing the "choose file" section - the code in umb-control-group. It just disappears and I'm left with the upload button and the code I inserted stating "start of loading section". Here's my edit.html code for my backoffice section:

    <form name="contentForm"
      data-ng-controller="MyControllerName"
      ng-show="loaded"
      val-form-manager>
    <div>
            <div class="spare">Start of loading section</div>
            <div class="umb-pane">
                <umb-control-group label="File" description="File to upload">
                    <input type="file" class="umb-editor umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
                </umb-control-group>
    
                <div class="umb-tab-buttons" detect-fold>
                    <div class="btn-group">
                        <button type="button" data-hotkey="ctrl+s" class="btn btn-success" ng-click="uploadFile()">
                            Upload here
                        </button>
                    </div>
                </div>
            </div>
    </div>
    

    Unbinding the controller - just deleting the data-ng-controller brings back the choose section.

    Hoping someone can help as no idea why I can't seem to bind the controller?

    Regards Sean

  • Pagggy 28 posts 71 karma points
    Jan 24, 2019 @ 14:24
    Pagggy
    0

    Thanks for sharing. Saved me tons of time.

  • Arjan H. 226 posts 463 karma points c-trib
    May 31, 2020 @ 17:33
    Arjan H.
    1

    For those of you who are struggling with file uploads in custom sections in Umbraco 8 here's my solution:

    View:

    <input type="file" class="umb-textstring" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" required />
    <umb-button action="vm.clickUploadButton()"
                type="button"
                button-style="info"
                label="Upload">
    </umb-button>
    

    Controller:

    function Controller($scope, Upload) {
        var vm = this;
        vm.clickUploadButton = clickUploadButton;
    
        $scope.fileSelected = function (files) {
            $scope.file = files;
        };
    
        function clickUploadButton() {
            if (!$scope.file)
                return false;
    
            Upload.upload({
                url: "backoffice/api/Upload/UploadFile",
                fields: {
                    "field1": "value1",
                    "field2": "value2"
                },
                file: $scope.file
            }).success(function (data, status, headers, config) {
                console.log(status);
                console.log(data);
            }).error(function (evt, status, headers, config) {
                console.log(evt.Message);
            });
        }
    }
    

    API controller:

    public class UploadController : UmbracoAuthorizedApiController
    {
        [HttpPost]
        public HttpResponseMessage UploadFile()
        {
            var files = HttpContext.Current.Request.Files;
            var field1 = HttpContext.Current.Request.Form["field1"];
            var field2 = HttpContext.Current.Request.Form["field2"];
    
            return Request.CreateResponse(HttpStatusCode.OK, new { fileCount = files.Count, field1, field2 }) ;
        }
    }
    
  • Anthony Southworth 48 posts 175 karma points
    Dec 14, 2022 @ 17:28
    Anthony Southworth
    0

    This served me well in Umbraco 7 but now I have to port it to Umbraco 10, has anyone got it working in .Net Core?

    Thanks Tony

  • Arjan H. 226 posts 463 karma points c-trib
    Dec 14, 2022 @ 22:11
    Arjan H.
    1

    I haven't used an upload in a custom section in Umbraco 10 yet, but maybe this code will point you in the right direction:

    JavaScript:

    let fileInput = document.querySelector('input[type="file"]');
    
    fileInput.addEventListener('change', (e) => {
    
      // Upload file
      let formData = new FormData();
      formData.append('file', fileInput.files[0]);
    
      fetch('/api/attachments/upload', {
        method: 'POST',
        body: formData,
      })
        .then((response) => {
          // Check if response status is 200
          if (!response.ok) throw new Error('API response was not OK');
          return response.json();
        })
        .then((result) => {
          if (result.success === false) {
            throw new Error(result.message);
          }
    
          // File has been successfully uploaded
          console.log('Success:', result.message);
        })
        .catch((error) => {
          // Something went wrong
          console.log('Error:', error);
        });
    });
    

    C#:

    public class AttachmentResult
    {
        public bool Success { get; set; } = false;
        public string? Message { get; set; }
    }
    
    [Route("api/attachments/[action]")]
    public class AttachmentsController : UmbracoApiController
    {
        [HttpPost]
        [Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
        public async Task<IActionResult> Upload()
        {
            var result = new AttachmentResult();
    
            try
            {
                var file = Request.Form.Files[0];
                var fileName = file.FileName;
                var fileExtension = Path.GetExtension(fileName);
                var fileSize = file.Length;
    
                // Save physical file
                var filePath = Path.Combine(Environment.CurrentDirectory, "uploads", fileName);
                using var fileStream = new FileStream(filePath, FileMode.Create);
                await file.CopyToAsync(fileStream);
    
                result.Success = true;
                result.Message = $"Uploaded file has been saved to {filePath}";
            }
            catch (Exception ex)
            {
                result.Message = ex.Message;
            }
    
            return Ok(result);
        }
    }
    
  • Anthony Southworth 48 posts 175 karma points
    Dec 15, 2022 @ 12:51
    Anthony Southworth
    0

    Thanks Arjan!

    I managed to get it working with a combination of the old code and your example.

  • Arjan H. 226 posts 463 karma points c-trib
    Dec 15, 2022 @ 15:14
    Arjan H.
    0

    Glad to hear you got it working. Would you be able to share (parts) of your code for the backoffice section so other people could benefit from it as well?

  • Anthony Southworth 48 posts 175 karma points
    Dec 15, 2022 @ 15:53
    Anthony Southworth
    2

    It's a mess but hopefully it helps someone:

    HTML:

       <div ng-controller="SSOAdminDashboardController" class="umb-pane">
            <umb-control-group label="File" description="File to upload">
                <input type="file" class="" ngf-select="" ng-model="files" ng-multiple="false" ngf-change="fileSelected(files)" />
            </umb-control-group>
    
            <button type="button" class="btn btn-success" ng-click="uploadFile()">
                Upload
            </button>
        </div>
    

    JS - ssoadmindashboard.controller.js:

    angular.module("umbraco").controller("SSOAdminDashboardController", function ($scope, notificationsService, fileUploadService) {
    
    $scope.fileSelected = function (files) {
        // In this case, files is just a single path/filename
        $scope.file = files;
    };
    
    $scope.uploadFile = function () {
        if (!$scope.isUploading) {
            if ($scope.file) {
                $scope.isUploading = true;
                fileUploadService.uploadFileToServer($scope.file)
                    .then(function (response) {
                        if (response) {
                            notificationsService.success("Success", "Users added.");
                        }
                        $scope.isUploading = false;
                    }, function (reason) {
                        notificationsService.error("Error", "File import failed: " + reason.message);
                        $scope.isUploading = false;
                    });
            } else {
                notificationsService.error("Error", "You must select a file to upload");
                $scope.isUploading = false;
            }
        }
    };
    
    $scope.file = false;
    $scope.isUploading = false;
    });
    

    file.upload.api.service.js

    angular.module("umbraco.resources").factory("fileUploadService", function ($http) {
    return {
        uploadFileToServer: function (file) {
            var request = {
                file: file
            };
            let fileInput = document.querySelector('input[type="file"]');
            return $http({
                method: 'POST',
                url: "backoffice/api/AddUserAPI/UploadFileToServer",
                headers: { 'Content-Type': undefined },
                transformRequest: function (data) {
                    var formData = new FormData();
                    formData.append('file', fileInput.files[0]);
                    return formData;
                },
                data: request
            }).then(function (response) {
                if (response) {
                    var fileName = response.data;
                    return fileName;
                } else {
                    return false;
                }
            });
        }
    };
    });
    

    C#:

        public class AddUserAPIController : UmbracoAuthorizedApiController
    {
        [HttpPost]
        public async Task<HttpResponseMessage> UploadFileToServer()
        {
            HttpResponseMessage response = new HttpResponseMessage() { StatusCode = HttpStatusCode.OK };
    
            var file = Request.Form.Files[0];
            if (file == null)
            {
                response = new HttpResponseMessage() { StatusCode = HttpStatusCode.NotAcceptable };
                response.Content = new StringContent("\r\n" + "File not valid.");
                return response;
            }
    
            var fileName = file.FileName;
            var filePath = Path.Combine(Environment.CurrentDirectory, "Uploads", fileName);
    
            using (var fileStream = System.IO.File.Open(filePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
                fileStream.Flush(true);
            }
    
            response = new HttpResponseMessage() { StatusCode = HttpStatusCode.OK };
            response.Content = new StringContent(fileName + " added.");
            return response;
        }
    }
    
  • This forum is in read-only mode while we transition to the new forum.

    You can continue this topic on the new forum by tapping the "Continue discussion" link below.

Please Sign in or register to post replies