Easy File Upload Using DropzoneJS AngularJs And Spring

Saving user files in a web application is pretty much a necessity in many cases be it images, videos, or documents. This post will go over how to easily implement both the back and frontend components to facilitate the storage of files to a database. Additionally, we will be using DropzoneJS to prettify and make the front end uploading process more smooth. First we will lay down the backend framework to facilitate the persisting of user files to the database. We will start by going over the backend implementation by creating a basic REST server, using Java Spring, with endpoints to both accept and send files. This will then be followed up with a frontend implementation using AngularJS as the app component and DropzoneJS as the library to upload files.

You can follow along by downloading the complete source found on GitHub.

Backend Setup

We will start with setting up our REST server to accept file uploads. First we will create the base application:


            @ComponentScan
            @EnableAutoConfiguration
            @RestController
            public class EasyUploadApplication {

                public static void main(String[] args) {
                    SpringApplication.run(EasyUploadApplication.class, args);
                }

            }
            

Next we will setup the configuration for this application:


            @Configuration
            @EnableTransactionManagement
            public class AppConfiguration {

                @Bean
                public DataSource dataSource() {
                    BasicDataSource dataSource = new BasicDataSource();
                    dataSource.setTestOnBorrow(true);
                    dataSource.setValidationQuery("SELECT 1");
                    dataSource.setUrl("jdbc:mysql://localhost:3306/database");
                    dataSource.setUsername("USERNAME");
                    dataSource.setPassword("PASSWORD");
                    return dataSource;
                }

                @Bean
                public LocalContainerEntityManagerFactoryBean entityManagerFactory(
                        DataSource dataSource) {
                    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
                    emf.setPackagesToScan("easyupload.entity");
                    emf.setPersistenceProvider(new HibernatePersistenceProvider());
                    Properties jpaProperties = new Properties();
                    jpaProperties.setProperty("hibernate.hbm2ddl.auto", "update");
                    jpaProperties.setProperty("hibernate.show_sql", "true");
                    emf.setJpaProperties(jpaProperties);
                    emf.setDataSource(dataSource);
                    return emf;
                }

                @Bean
                public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
                    JpaTransactionManager transactionManager = new JpaTransactionManager();
                    transactionManager.setEntityManagerFactory(emf);
                    return transactionManager;
                }
            }
            

Here we have setup 3 Java Beans for the application configuration. The first one configures our database layer. In this example, we have decided to use a MySQL database, but this can easily be changed to another SQL variant.

The 2nd Bean sets up the entity manager and configures the persistent layer to communicate with our MySQL datasource. We have also indicated where our Entity classes will be located so the application can wire them up on startup.

The 3rd and final Bean sets up the Transaction Manager. This will be the middle layer between our Entity classes and the database tables. These will be used to persist and transact the data back and forth. In this example we have opted to use the JpaTransactionManager.

At this stage, we have a very basic backend application which will listen on default port 8080, but currently lacks any functionality. We will go ahead and create an Entity for the uploaded file along with its properties. The Entity will reflect the table structure in the database.


            @Entity
            public class FileUpload {

                public FileUpload(String filename, byte[] file, String mimeType) {

                    this.file = file;
                    this.filename = filename;
                    this.mimeType = mimeType;
                }

                public FileUpload() {
                    // Default Constructor
                }

                @Id
                private String filename;

                @Lob
                private byte[] file;

                private String mimeType;


                public String getFilename() {
                    return filename;
                }

                public void setFilename(String filename) {
                    this.filename = filename;
                }

                public byte[] getFile() {
                    return file;
                }

                public void setFile(byte[] file) {
                    this.file = file;
                }

                public String getMimeType() {
                    return mimeType;
                }

                public void setMimeType(String mimeType) {
                    this.mimeType = mimeType;
                }
            }
            

In this entity we have created 3 fields:

  1. filename - the name of the file supplied by the frontend. This will also serve as id to allow only unique filenames
  2. file - a byte array of the file contents. It is annotated with @Lob, telling the database that it will be of type blob
  3. mimeType - file format which will assist the browser in suggesting a means of opening the file upon download

With the entity created, we will go ahead and create both the repository and service to persist the objects to the database.


            public interface FileUploadRepository extends JpaRepository<FileUpload, Long> {
                FileUpload findByFilename(String filename);
            }
            

            @Service
            public class FileUploadService {

                @Autowired
                FileUploadRepository fileUploadRepository;

                // Retrieve file
                public FileUpload findByFilename(String filename) {
                    return fileUploadRepository.findByFilename(filename);
                }

                // Upload the file
                public void uploadFile(FileUpload doc) {
                    fileUploadRepository.saveAndFlush(doc);
                }
            }
            

Above, we have extended the JpaRepository to include the findByFilename method which will retrieve the file object based on the filename string. The great thing about the Jpa repository, and all other Spring repositories is that it allows us to simply name a method with certain keywords which will be translated into a SQL query. These are called Query Methods, and based on our example, the method will be automatically interpreted as: SELECT * FROM FileUpload WHERE filename = "FILENAME".

The Service class utilizes the methods provided by the repository interface to persist and retrieve data from the datasource. In our example we are using the method we defined in the repository to retrieve a file and also have another method to persist a file object to the database. The saveAndFlush method is already defined by the JpaRepository which simultaneously persists the object and flushes the data from memory into the DB.

We are almost complete with setting up our backend server component, all that is needed now is to setup a controller to accept the routes from the frontend.


            @CrossOrigin
            @RestController
            public class FileController {

                @Autowired
                FileUploadService fileUploadService;

                // Download a file
                @RequestMapping(
                    value = "/download",
                    method = RequestMethod.GET
                )
                public ResponseEntity downloadFile(@RequestParam("filename") String filename) {

                    FileUpload fileUpload = fileUploadService.findByFilename(filename);

                    // No file found based on the supplied filename
                    if (fileUpload == null) {
                        return new ResponseEntity<>("{}", HttpStatus.NOT_FOUND);
                    }

                    // Generate the http headers with the file properties
                    HttpHeaders headers = new HttpHeaders();
                    headers.add("content-disposition", "attachment; filename=" + fileUpload.getFilename());

                    // Split the mimeType into primary and sub types
                    String primaryType, subType;
                    try {
                        primaryType = fileUpload.getMimeType().split("/")[0];
                        subType = fileUpload.getMimeType().split("/")[1];
                    }
                        catch (IndexOutOfBoundsException | NullPointerException ex) {
                        return new ResponseEntity<>("{}", HttpStatus.INTERNAL_SERVER_ERROR);
                    }

                    headers.setContentType( new MediaType(primaryType, subType) );

                    return new ResponseEntity<>(fileUpload.getFile(), headers, HttpStatus.OK);
                }
            }
            

We'll start with the download method. Here we set the path /download which will accept one query parameter of string called filename (as annotated by @RequestParam). The file object is first retrieved from the database by the DocumentService we created earlier. Next, we use the file properties to generate the HTTP headers for the response and finish it off with returning the ResponseEntity which includes the file data. One important item to note here is the @CrossOrigin annotation. This has only been added here so that this example functions correctly with the backend and frontend being segregated. Due to the Single Origin Policy (SOP), the frontend component would not be able to access the /upload route without some sort of CORS filter. As of Spring Boot 1.3.0 M2, this annotation now comes standard and lets us easily set it up. In a production environment, the configuration for this filter would need to adjusted as needed.


            @RequestMapping(
                value = "/upload",
                method = RequestMethod.POST
            )
            public ResponseEntity uploadFile(MultipartHttpServletRequest request) {

                try {
                    Iterator<String> itr = request.getFileNames();

                    while (itr.hasNext()) {
                        String uploadedFile = itr.next();
                        MultipartFile file = request.getFile(uploadedFile);
                        String mimeType = file.getContentType();
                        String filename = file.getOriginalFilename();
                        byte[] bytes = file.getBytes();

                        FileUpload newFile = new FileUpload(filename, bytes, mimeType);

                        fileUploadService.uploadFile(newFile);
                    }
                }
                catch (Exception e) {
                    return new ResponseEntity<>("{}", HttpStatus.INTERNAL_SERVER_ERROR);
                }

                return new ResponseEntity<>("{}", HttpStatus.OK);
            }
            

The upload method is quite simple in that it all it accepts is the the MultipartHttpServletRequest, which contains all the necessary data for us to persist it to the backend. One thing to note about the method is that it can accept multiple files in a single request. This is done because DropzoneJS has an option of uploading multiple files in a single request. As a result, this method iterates through the request, creates the file object based on the properties supplied, and saves it to the datasource via the service method.

This now wraps up the backend component, leaving us with a way to upload a file by sending a multipart request to http://localhost:8080/upload as well as the ability to download a file by passing in a filename query parameter to http://localhost:8080/download?filename=. Next we will be moving onto the frontend component where we will setup a basic AngularJS application and use the DropzoneJS library to facilitate the file upload process.

Frontend Setup

For the frontend setup, we will begin by creating a simple AngularJS app.


            /* js/app.js */

            angular.module('fileApp', []);
            

            /* js/fileAppControllers.js */

            function fileCtrl ($scope) {
            }

            angular.module('fileApp').controller('fileCtrl', fileCtrl)
            

            /* js/fileAppDirectives */

            function dropzone() {
            }

            angular.module('fileApp').directive('dropzone', dropzone);
            

This provides us with a skeleton application which can now be linked to our example page index.html. We have also included bootstrap to add some styling to our example.


            <!DOCTYPE html>
            <html ng-app="fileApp">

            <head>

                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">

                
                <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">

            </head>

            <body ng-controller="FileCtrl" ng-cloak>

                <!-- Load Angular scripts-->

                <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.17/angular.min.js"></script>

                <script src="js/app.js"></script>

                <script src="js/fileAppControllers.js"></script>

                <script src="js/fileAppDirectives.js"></script>

            </body>
            </html>
            

We can now download and attach the dropzone library as a dependency and include the element in the body tag.


            <!DOCTYPE html>
            <html ng-app="fileApp">

            <head>

                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">

                <!-- Load Dropzone CSS -->
                <link href="css/dropzone/basic.css" rel="stylesheet" />
                <link href="css/dropzone/dropzone.css" rel="stylesheet" />

            </head>

            <body ng-controller="FileCtrl" ng-cloak>

                <div class="col-xs-6 col-xs-offset-3">
                    <div class="well">
                        <form action="" class="dropzone" dropzone="" id="dropzone">
                            <div class="dz-default dz-message">
                            </div>
                        <form>
                    </div>
                </div>

                <!-- Load Angular scripts-->
                <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.17/angular.min.js"></script>
                <script src="js/app.js"></script>
                <script src="js/fileAppControllers.js"></script>
                <script src="js/fileAppDirectives.js"></script>

                <!-- Load Dropzone JS -->
                <script src="js/dropzone/dropzone.js"></script>

            </body>
            </html>
            

One thing to note above, in the form element we have added an attribute dropzone, this will be the angular directive used to wrap this element and allow it to be accessible by our app. We will go ahead and create the directive next. The directive is mostly based on This Github Project with a couple of tweaks.


            /* js/fileAppDirectives */

            var fileAppDirectives = angular.module('fileAppDirectives', []);

            fileAppDirectives.directive('dropzone', function() {
                return {
                    restrict: 'C',
                    link: function(scope, element, attrs) {

                        var config = {
                            url: 'http://localhost:8080/upload',
                            maxFilesize: 100,
                            paramName: "uploadfile",
                            maxThumbnailFilesize: 10,
                            parallelUploads: 1,
                            autoProcessQueue: false
                        };

                        var eventHandlers = {
                            'addedfile': function(file) {
                                scope.file = file;
                                if (this.files[1]!=null) {
                                    this.removeFile(this.files[0]);
                                }
                                scope.$apply(function() {
                                    scope.fileAdded = true;
                                });
                            },

                            'success': function (file, response) {
                            }

                        };

                        dropzone = new Dropzone(element[0], config);

                        angular.forEach(eventHandlers, function(handler, event) {
                            dropzone.on(event, handler);
                        });

                        scope.processDropzone = function() {
                            dropzone.processQueue();
                        };

                        scope.resetDropzone = function() {
                            dropzone.removeAllFiles();
                        }
                    }
                }
            });
            

In this directive, we have setup the configuration for dropzone, and in this specific example, we have limited the number of parallelUploads to 1 (we are only doing one file at a time), and have disabled the autoProcessQueue so that we can control the file uploading from within the app. The directive is also listening on the addedFile and success events from Dropzone, as we require a number of flags to change based on the outcome of these events. In the addedfile event we have included a check on the files array, which enforces the one file policy, overwriting old files to be uploaded with a new one. Finally, we have exposed the processQueue() and removeAllFiles() functions to our application's scope.

We can now create our controller which will allow us to trigger the file upload via the directive.


            /* js/fileAppControllers.js */

            ...

            fileAppControllers.controller('FileCtrl', ['scope',
                function ($scope) {

                    $scope.partialDownloadLink = 'http://localhost:8080/download?filename=';
                    $scope.filename = '';

                    $scope.uploadFile = function() {
                        $scope.processQueue();
                    };

                    $scope.reset = function() {
                        $scope.resetDropzone();
                    };
                }

            ]);
            

This controller is pretty straight forward. The two methods from the directive have been wrapped and will be linked to an upload button and a reset button on the page. Additionally we have included the template URL from which the file will be downloaded from. The user input in the form will be appended as the filename parameter.


            <body ng-controller="FileCtrl" ng-cloak>

                <div class="col-xs-6 col-xs-offset-3">
                    <div class="well">
                        <form action="" class="dropzone" dropzone="" id="dropzone">
                            <div class="dz-default dz-message">
                            </div>
                        <form>
                    </div>
                </div>
                <div class="pull-right">
                    <button class="btn btn-success" ng-click="uploadFile()">Upload File</button>
                    <button class="btn btn-danger" ng-click="reset()">Reset Dropzone</button>
                </div>
                <div>
                    <form>
                        <label>File to download</label>
                        <input ng-model="filename" type="text" placeholder="Filename" />
                        <a class="btn btn-primary" href="" ng-href="{{ partialDownloadLink + filename }}">Download File</a>
                    </form>
                </div>

                ...

                
                <script type="text/javascript">
                    Dropzone.autoDiscover = false;
                </script>

            </body>
            </html>
            

The page is now linked up with the buttons to process the file upload queue manually. A second form has been included for the user to input a filename which will be appended to the download link. In this case ng-href was needed to access the scope variables for generating a URL. Finally, the autoDiscover property of the Dropzone component had to be disabled to prevent the component from being loaded twice by our directive.

At this point, you should have a fully functioning application. The backend can be launched and will be listening on port 8080, while index.html can simply be opened in a web browser and will be able to talk with the backend via the /upload and /download routes. This can further be expanded to have the frontend resources run on another webserver (such as ExpressJS), segregated from the backend. Or the frontend components can be integrated into the Spring project and be used with a ViewResolver to render index.html when a user navigates to port 8080 of the hosted URL.