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. 221 posts 457 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 46 posts 173 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. 221 posts 457 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 46 posts 173 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. 221 posts 457 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 46 posts 173 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;
        }
    }
    
Please Sign in or register to post replies

Write your reply to:

Draft