Show Navigation

How to upload a file with Grails 4

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

Authors: Sergio del Amo

Grails Version: 4.0.1

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.8 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 some additional 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

The initial folder contains a Grails 4 application created with the web profile. It has several Controllers, GORM Data Services, Domain classes and a simple CRUD interface built with GSPs. It is an app to list tourism resources; hotels, restaurants, and points of interest.

In the next sections, you are going to add a feature to the app. Each of those resources may include a featured image.

  • For restaurants, we are going to store the image bytes directly in the domain class.

  • For points of interest, we transfer the file to a folder.

  • For hotels, we upload the file to AWS S3.

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 uploaded; we use a Grails Command Object

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

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 4’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

Modify Restaurant domain class. Add two properties featuredImageBytes and featuredImageContentType.

/grails-app/domain/example/grails/Restaurant.groovy
package example.grails

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 Views

We are going to modify the 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>

Modify RestaurantController. Add editFeaturedImage controller’s action:

/grails-app/controllers/example/grails/RestaurantController.groovy
def editFeaturedImage(Long id) {
    Restaurant restaurant = restaurantDataService.get(id)
    if (!restaurant) {
        notFound()
    }
    [restaurant: 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.3 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 Gorm Data Service.

/grails-app/controllers/example/grails/RestaurantController.groovy
    def uploadFeaturedImage(FeaturedImageCommand cmd) {
        if (cmd == null) {
            notFound()
            return
        }

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

        Restaurant restaurant = restaurantDataService.update(cmd.id,
                cmd.version,
                cmd.featuredImageFile.bytes,
                cmd.featuredImageFile.contentType)

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

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

        Locale locale = request.locale
        flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), cmd.id, locale)
        redirect restaurant
    }

The complete GORM Data Service is shown in the following code snippet:

/grails-app/services/example/grails/RestaurantDataService.groovy
package example.grails

import grails.gorm.services.Service

@Service(Restaurant)
interface RestaurantDataService {
    Restaurant get(Long id)
    List<Restaurant> list(Map args)
    Number count()
    void delete(Serializable id)
    Restaurant update(Serializable id, Long version, String name)
    Restaurant update(Serializable id, Long version, byte[] featuredImageBytes, String featuredImageContentType) (1)
    Restaurant save(String name)
}
1 The first argument should be the id of the object to update. If any of the parameters don’t match up to a property on the domain class then a compilation error will occur.

4.4 Load byte array Image

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

/grails-app/controllers/example/grails/RestaurantController.groovy
def featuredImage(Long id) {
    Restaurant restaurant = restaurantDataService.get(id)
    if (!restaurant || restaurant.featuredImageBytes == null) {
        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

Modify PointOfInterest domain class. Add a property featuredImageUrl

/grails-app/domain/example/grails/PointOfInterest.groovy
package example.grails

class PointOfInterest {
    String name
    String featuredImageUrl (1)

    static constraints = {
        featuredImageUrl nullable: true
    }
}
1 Use this property to save the url where the image can be retrieved.

5.2 Point of Interest Views

We are going to modify the GSP 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>

Modify PointOfInterestController. Add a controller’s action named editFeaturedImage:

/grails-app/controllers/example/grails/PointOfInterestController.groovy
def editFeaturedImage(Long id) {
    PointOfInterest pointOfInterest = pointOfInterestDataService.get(id)
    if (!pointOfInterest) {
        notFound()
        return
    }
    [pointOfInterest: 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.3 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/example/grails/PointOfInterestController.groovy
def uploadFeaturedImage(FeaturedImageCommand cmd) {

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

    PointOfInterest pointOfInterest = uploadPointOfInterestFeaturedImageService.uploadFeatureImage(cmd)

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

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

    Locale locale = request.locale
    flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), pointOfInterest.id, locale)
    redirect pointOfInterest
}

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/example/grails/UploadPointOfInterestFeaturedImageService.groovy
package example.grails

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

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

    PointOfInterestDataService pointOfInterestDataService

    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
        String folderPath = "${cdnFolder}/pointOfInterest/${cmd.id}"
        File folder = new File(folderPath)
        if ( !folder.exists() ) {
            folder.mkdirs()
        }
        String path = "${folderPath}/${filename}"
        cmd.featuredImageFile.transferTo(new File(path))

        String featuredImageUrl = "${cdnRootUrl}//pointOfInterest/${cmd.id}/${filename}"
        PointOfInterest poi = pointOfInterestDataService.updateFeaturedImageUrl(cmd.id, cmd.version, featuredImageUrl)

        if ( !poi || poi.hasErrors() ) {
            File f = new File(path)
            f.delete()
        }
        poi
    }
}

The service updates featuredImageUrl with the help of a GORM Data Service:

/grails-app/services/example/grails/PointOfInterestDataService.groovy
package example.grails

import grails.gorm.services.Service

@SuppressWarnings(['LineLength', 'UnusedVariable', 'SpaceAfterOpeningBrace', 'SpaceBeforeClosingBrace'])
@Service(PointOfInterest)
interface PointOfInterestDataService {

    PointOfInterest get(Long id)

    List<PointOfInterest> list(Map args)

    Number count()

    void delete(Serializable id)

    PointOfInterest save(String name)

    PointOfInterest updateName(Serializable id, Long version, String name)

    PointOfInterest updateFeaturedImageUrl(Serializable id, Long version, String featuredImageUrl)
}

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
repositories {
...
..
    maven { url 'http://dl.bintray.com/agorapulse/libs' }
}
dependencies {
...
..
.
    compile "org.grails.plugins:aws-sdk-s3:$awsSdkS3Version"
    compile "com.amazonaws:aws-java-sdk-s3:1.11.375"
}

Define awsSdkS3Version in gradle.properties

gradle.properties
grailsVersion=4.0.1
gormVersion=7.0.2.RELEASE
hibernateCoreVersion=5.4.0.Final
gebVersion=3.2
htmlunitDriverVersion=2.47.1
htmlunitVersion=2.35.0
assetPipelineVersion=3.0.11
awsSdkS3Version=2.2.4

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

Modify Hotel domain class. Add two properties featuredImageKey and featuredImageUrl`

/grails-app/domain/example/grails/Hotel.groovy
package example.grails

class Hotel {

    String name
    String featuredImageUrl (1)
    String featuredImageKey (2)

    static constraints = {
        featuredImageUrl nullable: true
        featuredImageKey nullable: true
    }
}
1 We store the AWS S3 url of the featured image
2 We store the file path. Useful to delete the file if necessary.

6.2 Hotel Views

We are going to modify the 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,featuredImageKey"/>
/grails-app/views/hotel/edit.gsp
<f:all bean="hotel" except="featuredImageUrl,featuredImageKey"/>

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>

Modify HotelController. Add a controller’s action named editFeaturedImage:

/grails-app/controllers/example/grails/HotelController.groovy
def editFeaturedImage(Long id) {
    Hotel hotel = hotelDataService.get(id)
    if (!hotel) {
        notFound()
        return
    }
    [hotel: 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.3 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/example/grails/HotelController.groovy
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
    }

    Locale locale = request.locale
    flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), hotel.id, locale)
    redirect hotel
}

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/example/grails/UploadHotelFeaturedImageService.groovy
package example.grails

import grails.gorm.transactions.Transactional
import grails.plugin.awssdk.s3.AmazonS3Service
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j

@CompileStatic
@Slf4j
@Transactional
class UploadHotelFeaturedImageService {

    AmazonS3Service amazonS3Service

    HotelDataService hotelDataService

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

        Hotel hotel = hotelDataService.update(cmd.id, cmd.version, path, s3FileUrl)
        if ( !hotel || hotel.hasErrors() ) {
            deleteFileByPath(path)
        }
        hotel
    }

    boolean deleteFileByPath(String path) {
        boolean result = amazonS3Service.deleteFile(path)
        if (!result) {
            log.warn 'could not remove file {}', path
        }
        result
    }
}

The domain class instance is updated with the help of GORM Data Service:

/grails-app/services/example/grails/HotelDataService.groovy
package example.grails

import grails.gorm.services.Service

@Service(Hotel)
interface HotelDataService {
    Hotel get(Long id)
    List<Hotel> list(Map args)
    Number count()
    void delete(Serializable id)
    Hotel save(String name)
    Hotel update(Serializable id, Long version, String name)
    Hotel update(Serializable id, Long version, String featuredImageKey, String featuredImageUrl)
}

6.4 Run the App with AWS Access and Secret

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 Do you need help with Grails?

Object Computing, Inc. (OCI) sponsored the creation of this Guide. A variety of consulting and support services are available.

OCI is Home to Grails

Meet the Team