FileStorageUploadField with myStorage

Hi All

I use myStorage to store Files on specific directories on my server.

With Jmix 1.x I did use

    @Install(to = "attachment", subject = "contentProvider")
    private InputStream attachmentContentProvider()
    {
        //  clicked on the filename
        try {
            return new FileInputStream(getPath() + File.separator + attachment.getFileName() );
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

How to do this in FlowUI 2.x ?

Jmix version: 2.1.2
Jmix Studio plugin version: 2.1.2-233
IntelliJ version: IntelliJ IDEA 2023.3.2 (Ultimate Edition)

Thank you for any hint

Felix

Hello Felix,

In FlowUI method onFileNameClick has to be overridden in order to do the same thing:

  1. Override FileStorageUploadField:
public class AppFileStorageUploadField extends FileStorageUploadField {

    @Override
    protected void onFileNameClick(ClickEvent<?> clickEvent) {
        if (!isEnabled()) {
            return;
        }

        // here we can get custom content, e.g.
        //FileRef value = getValue();
        //if (value != null) {
        //    downloader.download(() ->
        //                    new FileInputStream(getPath() + File.separator + value.getFileName()),
        //            generateFileName());
        //}

        // default behavior
        FileRef value = getValue();
        if (value != null) {
            downloader.download(value);
        }
    }
}
  1. Replace FileStorageUploadField with AppFileStorageUploadField in Configuration:
@Bean
public ComponentRegistration componentRegistration() {
    return ComponentRegistrationBuilder.create(AppFileStorageUploadField.class)
            .replaceComponent(FileStorageUploadField.class)
            .build();
}

Regards,
Dmitry

Hi Dmitry

There is no working sample for the myStorage in flowUI() available.

Attached you find a small project; unfortunatly it still does not work ;/

What did I miss ?

Best regards

Felix
myStorage.zip (1.0 MB)

Hello Felix,

ComponentRegistration should be added to Configuration class (the one annotated with org.springframework.boot.SpringBootConfiguration which aggregates @Configuration annotation too) in order to extend some UI component:

//...
@SpringBootApplication
public class MyStorageApplication implements AppShellConfigurator {
    //...
    @Bean
    public ComponentRegistration componentRegistration() {
        return ComponentRegistrationBuilder.create(AppFileStorageUploadField.class)
                .replaceComponent(FileStorageUploadField.class)
                .build();
    }
}

Here is the fixed project:
myStorage_fixed.zip (120.8 KB)

Regards,
Dmitry

Hello Dmitry

Thank you a lot for pointing out the wrong place of my ComponentRegistration.

As I take care myself of the path where the file is stored, I want to store in the FileStorageUploadField the filename only.

attachmentField.setValue(FileRef.fromString(event.getFileName()));

If i do this, i get an error
IllegalArgumentException: Cannot convert BOV.gif to FileRef

It does not make sense to show to the user the place where the file is stored.

Additionally, the

downloader.download(value);

Does not support any windows drive letters ( means the : in the filename ).
IllegalArgumentException: NTFS ADS separator (’:’) in file name is forbidden.

JMix 1.x doesn’t have any of these problems ;/ Is there a smart way to solve this ?

Best regards

Felix

Hello Felix,

Sorry for the late reply.

If you want to set custom file name to display in FileStorageUploadField you can override method generateFileName in com.company.mystorage.view.AppFileStorageUploadField e.g. like this:

 @Override
    protected String generateFileName() {
        if (getValue() != null) {
            return Paths.get(getValue().getFileName()).getFileName().toString();
        }
        return super.generateFileName();
    }

It is also better to define MyFilestorage as bean (see docs, @Bean definition should also be added to MyStorageApplication).

Maybe it make sense to try to implement com.company.mystorage.MyFileStorage slightly different overriding e.g. io.jmix.localfs.LocalFileStorage#saveStream(java.lang.String, java.io.InputStream, java.util.Map<java.lang.String,java.lang.Object>) and making it build FileRef correctly with filename only in FileRef#fileName field.

As for second question I will check a few things and will return as soon as I find the answer. Could you please provide a full stacktrace of the exception?

Regards,
Dmitry

Hello Dmitry

Thank you for your suggestions. I will check.

Attached you find the project with the error ( just upload a file and try to download after that on a windows PC ).

And here the stack

handleInvocationData:464, ServerRpcHandler (com.vaadin.flow.server.communication)
lambda$handleInvocations$2:440, ServerRpcHandler (com.vaadin.flow.server.communication)
accept:-1, ServerRpcHandler$$Lambda$1781/0x0000023df4bf7558 (com.vaadin.flow.server.communication)
forEach:1511, ArrayList (java.util)
handleInvocations:440, ServerRpcHandler (com.vaadin.flow.server.communication)
handleRpc:323, ServerRpcHandler (com.vaadin.flow.server.communication)
synchronizedHandleRequest:114, UidlRequestHandler (com.vaadin.flow.server.communication)
handleRequest:40, SynchronizedRequestHandler (com.vaadin.flow.server)
handleRequest:1529, VaadinService (com.vaadin.flow.server)
service:398, VaadinServlet (com.vaadin.flow.server)
service:106, SpringServlet (com.vaadin.flow.spring)
service:614, HttpServlet (jakarta.servlet.http)
internalDoFilter:205, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
invoke:642, ApplicationDispatcher (org.apache.catalina.core)
processRequest:408, ApplicationDispatcher (org.apache.catalina.core)
doForward:313, ApplicationDispatcher (org.apache.catalina.core)
forward:277, ApplicationDispatcher (org.apache.catalina.core)
handleRequestInternal:141, ServletForwardingController (org.springframework.web.servlet.mvc)
handleRequest:178, AbstractController (org.springframework.web.servlet.mvc)
handle:51, SimpleControllerHandlerAdapter (org.springframework.web.servlet.mvc)
doDispatch:1081, DispatcherServlet (org.springframework.web.servlet)
doService:974, DispatcherServlet (org.springframework.web.servlet)
processRequest:1014, FrameworkServlet (org.springframework.web.servlet)
doPost:914, FrameworkServlet (org.springframework.web.servlet)
service:547, HttpServlet (jakarta.servlet.http)
service:885, FrameworkServlet (org.springframework.web.servlet)
service:614, HttpServlet (jakarta.servlet.http)
internalDoFilter:205, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilter:51, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:28, LogMdcFilter (io.jmix.core.impl.logging)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilter:108, CompositeFilter$VirtualFilterChain (org.springframework.web.filter)
lambda$doFilterInternal$3:231, FilterChainProxy (org.springframework.security.web)
doFilter:-1, FilterChainProxy$$Lambda$1535/0x0000023df4b198c0 (org.springframework.security.web)
doFilter:365, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:100, AuthorizationFilter (org.springframework.security.web.access.intercept)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:126, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:120, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:131, SessionManagementFilter (org.springframework.security.web.session)
doFilter:85, SessionManagementFilter (org.springframework.security.web.session)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:100, AnonymousAuthenticationFilter (org.springframework.security.web.authentication)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:110, RememberMeAuthenticationFilter (org.springframework.security.web.authentication.rememberme)
doFilter:101, RememberMeAuthenticationFilter (org.springframework.security.web.authentication.rememberme)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:179, SecurityContextHolderAwareRequestFilter (org.springframework.security.web.servletapi)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:63, RequestCacheAwareFilter (org.springframework.security.web.savedrequest)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:151, ConcurrentSessionFilter (org.springframework.security.web.session)
doFilter:129, ConcurrentSessionFilter (org.springframework.security.web.session)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:227, AbstractAuthenticationProcessingFilter (org.springframework.security.web.authentication)
doFilter:221, AbstractAuthenticationProcessingFilter (org.springframework.security.web.authentication)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:107, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:93, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:117, CsrfFilter (org.springframework.security.web.csrf)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doHeadersAfter:90, HeaderWriterFilter (org.springframework.security.web.header)
doFilterInternal:75, HeaderWriterFilter (org.springframework.security.web.header)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:117, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:87, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:62, WebAsyncManagerIntegrationFilter (org.springframework.security.web.context.request.async)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:42, DisableEncodeUrlFilter (org.springframework.security.web.session)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
doFilter:374, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:233, FilterChainProxy (org.springframework.security.web)
doFilter:191, FilterChainProxy (org.springframework.security.web)
doFilter:113, CompositeFilter$VirtualFilterChain (org.springframework.web.filter)
lambda$createCacheFilter$3:195, HandlerMappingIntrospector (org.springframework.web.servlet.handler)
doFilter:-1, HandlerMappingIntrospector$$Lambda$1390/0x0000023df4a71a60 (org.springframework.web.servlet.handler)
doFilter:113, CompositeFilter$VirtualFilterChain (org.springframework.web.filter)
doFilter:74, CompositeFilter (org.springframework.web.filter)
doFilter:225, WebMvcSecurityConfiguration$CompositeFilterChainProxy (org.springframework.security.config.annotation.web.configuration)
invokeDelegate:352, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:268, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:116, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:174, ApplicationFilterChain (org.apache.catalina.core)
doFilter:149, ApplicationFilterChain (org.apache.catalina.core)
invoke:167, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:482, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:115, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:340, CoyoteAdapter (org.apache.catalina.connector)
service:391, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:896, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1744, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:52, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:840, Thread (java.lang)

myStorage.zip (581.6 KB)

Hello Felix,

The problem with “IllegalArgumentException: NTFS ADS separator (’:’) in file name is forbidden” is caused by MyFileStorage implementation:
MyFileStorage receives and stores path to file in fileName, but fileName is not designed to contain such symbols.

Please use standard LocalFileStorage with custom storage dir, e.g. like this:

// ----- MyStorageApplication.java ---
//...
@SpringBootApplication
public class MyStorageApplication implements AppShellConfigurator {
    //...
    // register LocalFileStorage with custom folder
    @Bean
    public FileStorage customFileStorage() {
        return new LocalFileStorage("myfs", "C:/path/to/custom/folder");
    }
}
//------ application.properties -----
# use 'myfs' as default filestorage
jmix.core.default-file-storage=myfs

Here is the example:
myStorage_fixedStorage.zip (341.8 KB)

If you want to save file to fully custom directory structure inside the file storage root dir, please note that standard LocalFileStorage implementation is designed to store files in directory structure built by date: YYY/MM/DD/.

If you want to implement MyFileStorage using LocalFileStorage as a base class, full reimplementation of some methods is needed (including io.jmix.localfs.LocalFileStorage#createRelativeFilePath and maybe another methods to make it work, return correct fileName without path in it and do not throw any exceptions).

Regards,
Dmitry