Orchardcore: Does Form support file upload?

Created on 23 Apr 2020  路  25Comments  路  Source: OrchardCMS/OrchardCore

I didn't see anything in the Workflow nor the Form widget support for "multipart/form-data"

Most helpful comment

@sebastienros This would be really great.
I mean, I'm currently working with my own solution as described, but it's far from perfect and not for everyones needs.
In addition to that, there is a very common scenario, where people are attaching files to a form: a job application.
With that, you have two Options to handle the files:
1.) Store them inside the media/file system
2.) Send them by mail with a workflow

In case of option 2, I've extended the OrchardCore.Email module to support attachments which would be really helpfull as default functionality.
My file upload task sets the paths as workflow variables which the extended email module reads and attaches them to the mail.
This could be solved way better, but it works for now.

The ability to choose wheter a media folder is public or private would be awesome, because I would really like to store files inside the media, that are only available through the admin panel, so I don't have to save my files outside of the media folder with a hardcoded path anymore.

All 25 comments

Or more importantly does the workflow support file upload?

There is no support for file upload through workflows right now.

Related issues https://github.com/OrchardCMS/OrchardCore/issues/4896

OK - let me try to implement a task for the workflow that handle a file upload.

I am trying to write a simple Activity first just to get a hang on this custom activity creation thing. This one will write a message to the disk when it is done.

disk-writer

This thumbnail is handled by DiskWriterTask.Fields.Thumbnail.cshtml.

This is a good simple activity task to learn from https://github.com/EtchUK/Etch.OrchardCore.Workflows/tree/master/FormOutput

If anyone need similar functionality, this code will work

  public class DiskWriterTask : TaskActivity
    {
        readonly IOptions<ShellOptions> _shellOptions;
        readonly IStringLocalizer S;
        readonly ShellSettings _shellSettings;
        private readonly IHttpContextAccessor _http;

        public DiskWriterTask(IStringLocalizer<DiskWriterTask> s, 
            IOptions<ShellOptions> shellOptions, 
            ShellSettings shellSettings,
            IHttpContextAccessor httpContextAccessor)
        {
            _shellOptions = shellOptions;
            _shellSettings = shellSettings;
            S = s;
            _http = httpContextAccessor;
        }


        public override string Name => nameof(DiskWriterTask);

        public override LocalizedString DisplayText => S["Disk Writer Task"];

        public override LocalizedString Category => S["UI"];

        public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            return Outcomes(S["Done"]);
        }

        public override bool CanExecute(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            return _http.HttpContext?.Request?.Form != null;
        }

        public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
        {
            var shell = _shellOptions.Value;
            var directory = PathExtensions.Combine(shell.ShellsApplicationDataPath, shell.ShellsContainerName, _shellSettings.Name, Folder);

            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            foreach(var file in _http.HttpContext.Request.Form.Files)
            {
                var toSave = PathExtensions.Combine(directory, file.FileName);
                using (var stream = System.IO.File.Create(toSave))
                {
                    await file.CopyToAsync(stream); 
                    workflowContext.Properties[file.Name] = toSave;
                }
            }

            return Outcomes("Done");
        }

        public string Folder
        {
            get => GetProperty<string>();
            set => SetProperty(value ?? string.Empty);
        }
    }

    public class DiskWriterViewModel
    {
        public string Folder { get; set; }
    }

    public class DiskWriterTaskDisplay: ActivityDisplayDriver<DiskWriterTask, DiskWriterViewModel>
    {
        protected override void EditActivity(DiskWriterTask activity, DiskWriterViewModel model)
        {
            model.Folder = activity.Folder;
        }

        protected override void UpdateActivity(DiskWriterViewModel model, DiskWriterTask activity)
        {
            activity.Folder = model.Folder;
        }
    }

You will have to fill the rest of the views.

layout

test

This is just a ContentItem with FlowPart
form-2

This is how it renders
form-1

Because the Form widget doesn't support multiple, you have to modify the generated html with Javascript

$(function(){
    $('form').attr("enctype","multipart/form-data");
});

This is the property page of the DiskWriter activity. The one I added is just Folder. The Tittle input comes by default.

disk-writer-edit

This is my first time writing a custom activity task so this one is gonna be rough at the edges.

Simple worklflow that will add data from the form

property-task-3


Pay attention to this one. There are older samples that uses JSON.parse(readBody()) but that stopped working a while ago. The name of the property here will be accessible as Workflow.Properties.YourPropertyName. So in this case, this property can be access as Workflow.Properties.ApplicationDetails in Liquid.

property-task


This part is the one that insert the data from the form to your content.
property-task-2

@dodyg
can upload your solution ?

There you go.
Disk Writer.zip

You need to modify the code to make it more robust but all the necessary ingredients are there.

@dodyg
thanks.

Thanks a lot @dodyg .. very detailed and useful explanation as usual 馃

@dodyg I updated your Task to include liquid support on the folder.

DiskWriterTask.cs

I've done something similar around two months ago and maybe I'm allowed to add some thoughts about security and validation.
In my case, I've added some more fields to the file upload activity and some more output options:

Fields:

  • Allowed extensions: Validate file extensions = Output: Invalid file extension
  • Allowed file size: Validate file size = Output: Invalid file size
  • Save file to: Saves the file to a specific location e.g. some media folder.

Optional fields:

  • Invalid file extension message: Integrated Notify and/or Form validation error
  • Invalid file size message: Integrated Notify and/or Form validation error
  • Default failed message: Integrated Notify and/or Form validation error
  • Default success message: Integrated Notify and/or Form validation error

Feature field:

  • File name fields: A comma separed list of other form fields which values are added to the file name to prevent dublicates or for better identification.

I think the save part needs to change so if someone is using cloud blob storage then it would also work. Seems like its hardcoded to the local filesystem.

@stevetayloruk Yup. This is just a quick and dirty implementation for my local project purpose.

What about having such a task by default in the project?
I assume we would need to use a custom abstraction of the FS, such that we could write to a custom folder or store (blob). These files should not be servable by default. Or have an option to store them in a specific media folder, if we want them to be servable.

Maybe it should only use the media service. And we'd need to have a way to use a custom folder that can't serve files publicly. But we'd be able to browse them from the admin.

@sebastienros This would be really great.
I mean, I'm currently working with my own solution as described, but it's far from perfect and not for everyones needs.
In addition to that, there is a very common scenario, where people are attaching files to a form: a job application.
With that, you have two Options to handle the files:
1.) Store them inside the media/file system
2.) Send them by mail with a workflow

In case of option 2, I've extended the OrchardCore.Email module to support attachments which would be really helpfull as default functionality.
My file upload task sets the paths as workflow variables which the extended email module reads and attaches them to the mail.
This could be solved way better, but it works for now.

The ability to choose wheter a media folder is public or private would be awesome, because I would really like to store files inside the media, that are only available through the admin panel, so I don't have to save my files outside of the media folder with a hardcoded path anymore.

@sebastienros Is there any plans to have media folders/files securable through the permission system?

What would your approach be to achieve this?

Per role, per folder:

  • Write in folder
  • List files in folder

In the attached media field, we already do that, by forcing a custom folder, and not showing other ones.

Bonus feature (harder):

  • Serve files in folder, by permission. Only serve the file if the current user's role (even anonymous) is allowed to be served the file.

I think it should be done at the folder level to simplify the UI.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jardg picture jardg  路  3Comments

JanSichula picture JanSichula  路  3Comments

jeffolmstead picture jeffolmstead  路  4Comments

khoshroomahdi picture khoshroomahdi  路  4Comments

aghili371 picture aghili371  路  3Comments