Fork me on Github

How to upload a file with Grails 3

Learn how to upload files with Grails 3; transfer them to a folder, save them as byte[] in the database or upload them to AWS S3.

Authors: Sergio del Amo

Grails Version: 3.3.0

1 Grails Training

Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.

2 Getting Started

In this guide, you are going to write three different domain classes. Moreover, you are going to write controllers, services, and views associated with them.

We are going to explore different ways to save the uploaded files; byte[] in the database, local folder or remote server (AWS S3).

We are going to use a Command Object to allow only images ( JPG or PNG ) uploads.

2.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.7 or greater installed with JAVA_HOME configured appropriately

2.2 How to complete the guide

To get started do the following:

or

The Grails guides repositories contain two folders:

  • initial Initial project. Often a simple Grails app with additional some code to give you a head-start.

  • complete A completed example. It is the result of working through the steps presented by the guide and applying those changes to the initial folder.

To complete the guide, go to the initial folder

  • cd into grails-guides/grails-upload-file/initial

and follow the instructions in the next sections.

You can go right to the completed example if you cd into grails-guides/grails-upload-file/complete

3 Writing the Application

We are going to create an app to list tourism resources; hotels, restaurants, and points of interest.

In the next sections, you are going to see in more detail those domain classes.

3.1 Featured Image Command Object

Each of our application entities ( hotels, restaurants and point of interest ) will have a featured image.

To encapsulate the validation of the file being upload; we use a Grails Command Object

/grails-app/controllers/demo/FeaturedImageCommand.groovy
package demo

import grails.validation.Validateable
import org.springframework.web.multipart.MultipartFile

class FeaturedImageCommand implements Validateable {
    MultipartFile featuredImageFile
    Long id
    Integer version

    static constraints = {
        id nullable: false
        version nullable: false
        featuredImageFile  validator: { val, obj ->
            if ( val == null ) {
                return false
            }
            if ( val.empty ) {
                return false
            }

            ['jpeg', 'jpg', 'png'].any { extension -> (1)
                 val.originalFilename?.toLowerCase()?.endsWith(extension)
            }
        }
    }
}
1 Only files ending with JPG or PNG are allowed

3.2 Increase allowed file size

Grails 3’s default file size is 128000 (~128KB).

We are going to allow 25MB files uploads.

25 * 1024 * 1024 = 26.214.400 bytes

/grails-app/conf/application.yml
grails:
 controllers:
  upload:
    maxFileSize: 26214400
    maxRequestSize: 26214400

4 Restaurant - Upload and save as byte[]

This part of the Guide illustrates how to upload a file to a Grails server app and store the file’s bytes in the database.

bytearray

4.1 Restaurant Domain Class

Create a domain class for Restaurant

./grailsw create-domain-class Restaurant
| Created grails-app/domain/demo/Restaurant.groovy
| Created src/test/groovy/demo/RestaurantSpec.groovy
/grails-app/domain/demo/Restaurant.groovy
package demo

class Restaurant {
    String name
    byte[] featuredImageBytes (1)
    String featuredImageContentType (2)

    static constraints = {
        featuredImageBytes nullable: true
        featuredImageContentType nullable: true
    }

    static mapping = {
        featuredImageBytes column: 'featured_image_bytes', sqlType: 'longblob' (3)
    }
}
1 Use a domain class property to store the bytes of the image in the database
2 Content-Type of the image. E.g. 'image/jpeg'
3 Images may be big files; we use the mapping closure to configure the sqlType

4.2 Restaurant Static Scaffolding

We have used static scaffolding to generate the CRUD ( Create, Read, Update, Delete) functionality.

./grailsw generate-all Restaurant

Moreover, we have done a code refactoring of the generated controller RestaurantController and we have extracted the transactional GORM behavior to a service RestaurantGormService.

In order to keep this guide code listings short, we don’t include those code snippets. However, you can download and unzip the source or clone this guide repository Git: git clone https://github.com/grails-guides/grails-upload-file.git You can find the finished guide with all the project code at grails-guides/grails-upload-file/complete

4.3 Restaurant Views

We are going to modify the generated views slightly to include functionality which enables to upload a featured image.

In the restaurant’s listing, display only the restaurant’s name in the table. Checkout the Fields Plugin f:table documentation.

/grails-app/views/restaurant/index.gsp
<f:table collection="${restaurantList}" properties="['name']"/>

Edit and create form will not allow users to set featured image’s content type or byte[]

/grails-app/views/restaurant/create.gsp
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>
/grails-app/views/restaurant/edit.gsp
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>

In the show restaurant page, add a button which takes the user to a featured image edit page.

/grails-app/views/restaurant/show.gsp
<g:link class="edit" action="editFeaturedImage" resource="${this.restaurant}"><g:message code="restaurant.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>

editFeaturedImage Controller’s action is identical to edit action:

/grails-app/controllers/demo/RestaurantController.groovy
@Transactional(readOnly = true)
def editFeaturedImage(Restaurant restaurant) {
    respond restaurant
}

editFeaturedImage GSP is identical to edit GSP but it uses an g:uploadForm instead of a g:form

/grails-app/views/restaurant/editFeaturedImage.gsp
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
    <g:hiddenField name="id" value="${this.restaurant?.id}" />
    <g:hiddenField name="version" value="${this.restaurant?.version}" />
    <input type="file" name="featuredImageFile" />
    <fieldset class="buttons">
        <input class="save" type="submit" value="${message(code: 'restaurant.featuredImage.upload.label', default: 'Upload')}" />
    </fieldset>
</g:uploadForm>

4.4 Upload Featured Image Service

The uploadFeatureImage controller’s action uses the previously described command object to validate the upload form submission. If it does not find validation errors, it invokes a service.

/grails-app/controllers/demo/RestaurantController.groovy
package demo

import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.NO_CONTENT
import static org.springframework.http.HttpStatus.CREATED
import grails.transaction.Transactional

@SuppressWarnings('LineLength')
class RestaurantController {

    static allowedMethods = [
            save: 'POST',
            update: 'PUT',
            uploadFeaturedImage: 'POST',
            delete: 'DELETE',
    def uploadFeaturedImage(FeaturedImageCommand cmd) {
        if (cmd == null) {
            notFound()
            return
        }

        if (cmd.hasErrors()) {
            respond(cmd.errors, model: [restaurant: cmd], view: 'editFeaturedImage')
            return
        }

        def restaurant = uploadRestaurantFeaturedImageService.uploadFeatureImage(cmd)

        if (restaurant == null) {
            notFound()
            return
        }

        if (restaurant.hasErrors()) {
            respond(restaurant.errors, model: [restaurant: restaurant], view: 'editFeaturedImage')
            return
        }

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'restaurant.label', default: 'Restaurant'), restaurant.id])
                redirect restaurant
            }
            '*' { respond restaurant, [status: OK] }
        }
    }

                redirect action: 'index', method: 'GET'

Service extracts the content type and byte[] and saves them in the database.

/grails-app/services/demo/UploadRestaurantFeaturedImageService.groovy
package demo

class UploadRestaurantFeaturedImageService {

    def restaurantGormService

    Restaurant uploadFeatureImage(FeaturedImageCommand cmd) {
        byte[] bytes = cmd.featuredImageFile.bytes
        String contentType = cmd.featuredImageFile.contentType
        restaurantGormService.updateRestaurantFeaturedImage(cmd.id, cmd.version, bytes, contentType)
    }
}
/grails-app/services/demo/RestaurantGormService.groovy
    @Transactional
    Restaurant updateRestaurantFeaturedImage(Long restaurantId, Integer version, byte[] bytes, String contentType) {
        Restaurant restaurant = Restaurant.get(restaurantId)
        if ( !restaurant ) {
          return null
        }
        restaurant.version = version
        restaurant.featuredImageBytes = bytes
        restaurant.featuredImageContentType = contentType
        restaurant.save()
        restaurant
    }

4.5 Load byte array Image

Create an action which renders the restaurant’s featured image:

/grails-app/controllers/demo/RestaurantController.groovy
    @Transactional(readOnly = true)
    def featuredImage(Restaurant restaurant) {
        if (restaurant == null || restaurant.featuredImageBytes == null) {
            transactionStatus.setRollbackOnly()
            notFound()
            return
        }
        render file: restaurant.featuredImageBytes, contentType: restaurant.featuredImageContentType
    }

Use the g:createLink GSP tag to reference the previous controller action in the img src attribute.

/grails-app/views/restaurant/show.gsp
            <h1><f:display bean="restaurant" property="name" /></h1>
            <g:if test="${this.restaurant.featuredImageBytes}">
                <img src="<g:createLink controller="restaurant" action="featuredImage" id="${this.restaurant.id}"/>" width="400"/>
            </g:if>

5 Point of Interest - Upload and transfer

In this part of this guide, we have separated the Grails application from the uploaded images. We use MAMP to run a local apache server at port 8888 pointing to the folder data.

transfer

5.1 Point of Interest Domain Class

./grailsw create-domain-class PointOfInterest
| Created grails-app/domain/demo/PointOfInterest.groovy
| Created src/test/groovy/demo/PointOfInterestSpec.groovy
/grails-app/domain/demo/PointOfInterest.groovy
package demo

class PointOfInterest {
    String name
    String featuredImageUrl

    static constraints = {
        featuredImageUrl nullable: true
    }
}

5.2 Point of Interest Static Scaffolding

We have used static scaffolding to generate the CRUD ( Create, Read, Update, Delete) functionality.

./grailsw generate-all PointOfInterest

Moreover, we have done a code refactoring of the generated controller PointOfInterestController and we have extracted the transactional GORM behavior to a service PointOfInterestGormService.

In order to keep this guide code listings short, we don’t include those code snippets. However, you can download and unzip the source or clone this guide repository Git: git clone https://github.com/grails-guides/grails-upload-file.git You can find the finished guide with all the project code at grails-guides/grails-upload-file/complete

5.3 Point of Interest Views

We are going to modify the generated views slightly to include functionality which enables to upload a featured image.

In Point of Interest’s listing, display only the hotel’s name in the table. Checkout the Fields Plugin f:table documentation.

/grails-app/views/pointOfInterest/index.gsp
<f:table collection="${pointOfInterestList}" properties="['name']"/>

Edit and create form will not allow users to set featured image’s url

/grails-app/views/pointOfInterest/create.gsp
<f:all bean="pointOfInterest" except="featuredImageUrl"/>
/grails-app/views/pointOfInterest/edit.gsp
<f:all bean="pointOfInterest" except="featuredImageUrl"/>

Instead we add a button which takes the user to a featured image edit page.

/grails-app/views/pointOfInterest/show.gsp
<g:link class="edit" action="editFeaturedImage" resource="${this.pointOfInterest}"><g:message code="pointOfInterest.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>

editFeaturedImage Controller’s action is identical to edit action:

/grails-app/controllers/demo/PointOfInterestController.groovy
@Transactional(readOnly = true)
def editFeaturedImage(PointOfInterest pointOfInterest) {
    respond pointOfInterest
}

editFeaturedImage GSP is identical to edit GSP but it uses an g:uploadForm instead of a g:form

/grails-app/views/pointOfInterest/editFeaturedImage.gsp
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
    <g:hiddenField name="id" value="${this.pointOfInterest?.id}" />
    <g:hiddenField name="version" value="${this.pointOfInterest?.version}" />
    <input type="file" name="featuredImageFile" />
    <fieldset class="buttons">
        <input class="save" type="submit" value="${message(code: 'pointOfInterest.featuredImage.upload.label', default: 'Upload')}" />
    </fieldset>
</g:uploadForm>

5.4 Upload Featured Image Service

The uploadFeatureImage controller’s action uses the previously described command object to validate the upload form submission. If it does not find validation errors, it invokes a service.

/grails-app/controllers/demo/PointOfInterestController.groovy
package demo

import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.NO_CONTENT
import static org.springframework.http.HttpStatus.CREATED
import grails.transaction.Transactional

@SuppressWarnings('LineLength')
class PointOfInterestController {

    static allowedMethods = [
            save: 'POST',
            update: 'PUT',
            uploadFeaturedImage: 'POST',
            delete: 'DELETE',
    def uploadFeaturedImage(FeaturedImageCommand cmd) {

        if (cmd.hasErrors()) {
            respond(cmd, model: [pointOfInterest: cmd], view: 'editFeaturedImage')
            return
        }

        def pointOfInterest = uploadPointOfInterestFeaturedImageService.uploadFeatureImage(cmd)
        if (pointOfInterest == null) {
            notFound()
            return
        }

        if (pointOfInterest.hasErrors()) {
            respond(pointOfInterest, model: [pointOfInterest: pointOfInterest], view: 'editFeaturedImage')
            return
        }

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'pointOfInterest.label', default: 'Point of Interest'), pointOfInterest.id])
                redirect pointOfInterest
            }
            '*' { respond pointOfInterest, [status: OK] }
        }
    }
}

We configure the local server url which will host the uploaded images and the local folder path where we are going to save the images to.

/grails-app/conf/application.yml
grails:
    guides:
        cdnFolder: /Users/sdelamo/Sites/GrailsGuides/UploadFiles
        cdnRootUrl: http://localhost:8888

Service uses the transferTo method to transfer the file to a local file path. In case of error, the service deletes the file it has previously transfered

/grails-app/services/demo/UploadPointOfInterestFeaturedImageService.groovy
package demo

import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic

@SuppressWarnings('GrailsStatelessService')
@CompileStatic
class UploadPointOfInterestFeaturedImageService implements GrailsConfigurationAware {

    PointOfInterestGormService pointOfInterestGormService

    String cdnFolder
    String cdnRootUrl

    @Override
    void setConfiguration(Config co) {
        cdnFolder = co.getRequiredProperty('grails.guides.cdnFolder')
        cdnRootUrl = co.getRequiredProperty('grails.guides.cdnRootUrl')
    }

    @SuppressWarnings('JavaIoPackageAccess')
    PointOfInterest uploadFeatureImage(FeaturedImageCommand cmd) {

        String filename = cmd.featuredImageFile.originalFilename
        def folderPath = "${cdnFolder}/pointOfInterest/${cmd.id}" as String
        def folder = new File(folderPath)
        if ( !folder.exists() ) {
            folder.mkdirs()
        }
        def path = "${folderPath}/${filename}" as String
        cmd.featuredImageFile.transferTo(new File(path))

        String featuredImageUrl = "${cdnRootUrl}//pointOfInterest/${cmd.id}/${filename}"

        def poi = pointOfInterestGormService.updateFeaturedImageUrl(cmd.id, cmd.version, featuredImageUrl)

        if ( !poi || poi.hasErrors() ) {
            def f = new File(path)
            f.delete()
        }
        poi
    }
}
/grails-app/services/demo/PointOfInterestGormService.groovy
    @Transactional
    PointOfInterest updateFeaturedImageUrl(Long pointOfInterestId, Integer version, String featuredImageUrl) {
        PointOfInterest poi = PointOfInterest.get(pointOfInterestId)
        if ( !poi ) {
            return null
        }
        poi.version = version
        poi.featuredImageUrl = featuredImageUrl
        poi.save()
    }

6 Hotel - Upload to AWS S3

For Hotel domain class we are going to upload the file to Amazon AWS S3.

s3

First, Install Grails AWS SDK S3 Plugin

Modify you build.grade file

build.gradle
    compile 'org.grails.plugins:aws-sdk-s3:2.1.5'

We are going to upload our files to an already existing bucket.

/grails-app/conf/application.yml
grails:
    plugins:
        awssdk:
            s3:
                region: eu-west-1
                bucket: grails-guides

6.1 Hotel Domain Class

./grailsw create-domain-class Hotel
| Created grails-app/domain/demo/Hotel.groovy
| Created src/test/groovy/demo/HotelSpec.groovy
/grails-app/domain/demo/Hotel.groovy
package demo

class Hotel {

    String name
    String featuredImageUrl (1)

    static constraints = {
        featuredImageUrl nullable: true
    }
}
1 We store the AWS S3 url of the featured image

6.2 Hotel Static Scaffolding

We have used static scaffolding to generate the CRUD ( Create, Read, Update, Delete) functionality.

./grailsw generate-all Hotel

Moreover, we have done a code refactoring of the generated controller HotelController and we have extracted the transactional GORM behavior to a service HotelGormService.

In order to keep this guide code listings short, we don’t include those code snippets. However, you can download and unzip the source or clone this guide repository Git: git clone https://github.com/grails-guides/grails-upload-file.git You can find the finished guide with all the project code at grails-guides/grails-upload-file/complete

6.3 Hotel Views

We are going to modify the generated views slightly to include functionality which enables to upload a featured image.

In Hotel’s listing, display only the hotel’s name in the table. Checkout the Fields Plugin f:table documentation.

/grails-app/views/hotel/index.gsp
<f:table collection="${hotelList}" properties="['name']"/>

Edit and create form will not allow users to set featured image’s url

/grails-app/views/hotel/create.gsp
<f:all bean="hotel" except="featuredImageUrl"/>
/grails-app/views/hotel/edit.gsp
<f:all bean="hotel" except="featuredImageUrl"/>

Instead we add a button which takes the user to a featured image edit page.

/grails-app/views/hotel/show.gsp
<g:link class="edit" action="edit" resource="${this.hotel}"><g:message code="default.button.edit.label" default="Edit" /></g:link>

editFeaturedImage Controller’s action is identical to edit action:

/grails-app/controllers/demo/HotelController.groovy
@Transactional(readOnly = true)
def editFeaturedImage(Hotel hotel) {
    respond hotel
}

editFeaturedImage GSP is identical to edit GSP but it uses an g:uploadForm instead of a g:form

/grails-app/views/hotel/editFeaturedImage.gsp
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
    <g:hiddenField name="id" value="${this.hotel?.id}" />
    <g:hiddenField name="version" value="${this.hotel?.version}" />
    <input type="file" name="featuredImageFile" />
    <fieldset class="buttons">
        <input class="save" type="submit" value="${message(code: 'hotel.featuredImage.upload.label', default: 'Upload')}" />
    </fieldset>
</g:uploadForm>

6.4 Upload image to S3

The uploadFeatureImage controller’s action uses the previously described command object to validate the upload form submission. If it does not find validation errors, it invokes a service.

/grails-app/controllers/demo/HotelController.groovy
package demo

import static org.springframework.http.HttpStatus.OK
import static org.springframework.http.HttpStatus.NOT_FOUND
import static org.springframework.http.HttpStatus.NO_CONTENT
import static org.springframework.http.HttpStatus.CREATED
import grails.transaction.Transactional

@SuppressWarnings('LineLength')
class HotelController {

    static allowedMethods = [
            save: 'POST',
            update: 'PUT',
            uploadFeaturedImage: 'POST',
            delete: 'DELETE',
    def uploadFeaturedImage(FeaturedImageCommand cmd) {

        if (cmd.hasErrors()) {
            respond(cmd.errors, model: [hotel: cmd], view: 'editFeaturedImage')
            return
        }

        def hotel = uploadHotelFeaturedImageService.uploadFeatureImage(cmd)
        if (hotel == null) {
            notFound()
            return
        }

        if (hotel.hasErrors()) {
            respond(hotel.errors, model: [hotel: hotel], view: 'editFeaturedImage')
            return
        }

        request.withFormat {
            form multipartForm {
                flash.message = message(code: 'default.updated.message', args: [message(code: 'hotel.label', default: 'Hotel'), hotel.id])
                redirect hotel
            }
            '*' { respond hotel, [status: OK] }
        }
    }
                redirect action: 'index', method: 'GET'

Service uses the amazonS3Service provided by the plugin to upload the file to AWS S3. In case of error, the service deletes the file it has previously uploaded.

/grails-app/services/demo/UploadHotelFeaturedImageService.groovy
package demo

import grails.transaction.Transactional

@Transactional
class UploadHotelFeaturedImageService {

    def amazonS3Service

    def hotelGormService

    Hotel uploadFeatureImage(FeaturedImageCommand cmd) {

        def path = "hotel/${cmd.id}/" + cmd.featuredImageFile.originalFilename
        String s3FileUrl = amazonS3Service.storeMultipartFile(path, cmd.featuredImageFile)

        def hotel = hotelGormService.updateFeaturedImageUrl(cmd.id, cmd.version, s3FileUrl)
        if ( !hotel || hotel.hasErrors() ) {
            amazonS3Service.deleteFile(path)
        }
        hotel
    }
}
/grails-app/services/demo/HotelGormService.groovy
    @Transactional
    Hotel updateFeaturedImageUrl(Long id, Integer version, String featuredImageUrl) {
        Hotel hotel = Hotel.get(id)
        if ( !hotel ) {
            return null
        }
        hotel.version = version
        hotel.featuredImageUrl = featuredImageUrl
        hotel.save()
    }

6.5 Run the App with AWS Access and Secret

Modify build.gradle, we want System properties to be passed to bootRun Gradle task.

/build.gradle
bootRun {
    systemProperties System.properties
}

If you do not provide credentials, a credentials provider chain will be used that searches for credentials in this order:

Environment Variables - AWS_ACCESS_KEY_ID and AWS_SECRET_KEY Java System Properties - aws.accessKeyId andaws.secretKey` Instance profile credentials delivered through the Amazon EC2 metadata service (IAM role)

Thus, we can run the app with:

./gradlew bootRun -Daws.accessKeyId=XXXXX -Daws.secretKey=XXXXX

7 Running the Application

To run the application use the ./gradlew bootRun command which will start the application on port 8080.

8 Do you need help with Grails?

OCI sponsored the creation of this Guide. OCI offers several Grails services:

Free consultation

The OCI Grails Team includes Grails co-founders, Jeff Scott Brown and Graeme Rocher. Check our Grails courses and learn from the engineers who developed, matured and maintain Grails.

Grails OCI Team