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:
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:
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!)
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.
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.
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;
});
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:
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);
}
}
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?
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:
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:
Now we need the fileUploadService, which I chose to put in a separate file called file.upload.api.service.js:
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:
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:
To override the generated filename, I also added this class (yes, I know this may have a security impact):
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
Thanks for sharing :)
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:
Should be:
Thanks again, Sean.
Thanks, Sean! I have now updated the code in the original post. Glad to hear that my post was helpful.
We get the following error when selecting the file:
Our implementation pretty much mirrors what you have above.
Any ideas?
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 isBut 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.
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:
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.
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:
Now it works as it should be. Thank you!
Sorry for being late following up here, but you solved it already, great! I'm sure this will be helpful to others.
do you have the full working demo? cant make it work
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:
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
Thanks for sharing. Saved me tons of time.
For those of you who are struggling with file uploads in custom sections in Umbraco 8 here's my solution:
View:
Controller:
API controller:
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
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:
C#:
Thanks Arjan!
I managed to get it working with a combination of the old code and your example.
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?
It's a mess but hopefully it helps someone:
HTML:
JS - ssoadmindashboard.controller.js:
file.upload.api.service.js
C#:
is working on a reply...