Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does Form support file upload? #6027

Open
dodyg opened this issue Apr 23, 2020 · 26 comments
Open

Does Form support file upload? #6027

dodyg opened this issue Apr 23, 2020 · 26 comments
Milestone

Comments

@dodyg
Copy link

dodyg commented Apr 23, 2020

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

@dodyg
Copy link
Author

dodyg commented Apr 24, 2020

Or more importantly does the workflow support file upload?

@deanmarcussen
Copy link
Member

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

Related issues #4896

@dodyg
Copy link
Author

dodyg commented Apr 24, 2020

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

@dodyg
Copy link
Author

dodyg commented Apr 25, 2020

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.

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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;
        }
    }

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

You will have to fill the rest of the views.

layout

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

test

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

This is just a ContentItem with FlowPart
form-2

This is how it renders
form-1

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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

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

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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

@dodyg
Copy link
Author

dodyg commented Apr 26, 2020

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

@amira-elbatal
Copy link

@dodyg
can upload your solution ?

@dodyg
Copy link
Author

dodyg commented Apr 27, 2020

There you go.
Disk Writer.zip

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

@amira-elbatal
Copy link

@dodyg
thanks.

@hishamco
Copy link
Member

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

@duncanhoggan
Copy link
Contributor

duncanhoggan commented Apr 28, 2020

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

DiskWriterTask.cs

@Flarescape
Copy link

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.

@stevetayloruk
Copy link
Member

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.

@dodyg
Copy link
Author

dodyg commented Aug 17, 2020

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

@sebastienros
Copy link
Member

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.

@Flarescape
Copy link

@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.

@stevetayloruk
Copy link
Member

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

What would your approach be to achieve this?

@sebastienros
Copy link
Member

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.

@Piedone
Copy link
Member

Piedone commented Mar 13, 2024

The Form widget now supports multipart/form-data. If #12218 is merged, we'll have file uploads in Workflows too.

The most recent conversation here changed over to authorized Media (file storage) access though. For that, we have this other issue: #3590.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants