显示导航

如何使用 Grails 3 上传文件

了解如何使用 Grails 3 上传文件;将其传输到一个文件夹,将其保存在数据库中的 byte[] 中,或将其上传到 AWS S3。

作者:Sergio del Amo

Grails 版本 3.3.6

1 Grails 培训

Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供的!。

2 开始使用

在此指南中,您将编写三个不同的域类。此外,您将编写与之关联的控制器、服务和视图。

我们将探讨保存上传文件这种方法;数据库中的 byte[]、本地文件夹或远程服务器(AWS S3)。

我们将使用命令对象只允许图片(JPG 或 PNG)上传。

2.1 需要什么

要完成此指南,您需要以下内容

  • 一些闲暇时间

  • 一款不错的文本编辑器或 IDE

  • 安装了 JDK 1.7 或更高版本,并已适当地配置了 JAVA_HOME

2.2 如何完成指南

要开始操作,请执行以下操作

Grails 指南仓库包含两个文件夹

  • initial 初始项目。通常是一个带有附加代码的简单 Grails 应用,以此为您提供一个良好的开端。

  • complete 完成的示例。这是贯穿指南提供的步骤并对 initial 文件夹应用这些更改的结果。

要完成指南,请转至 initial 文件夹

  • cd 转至 grails-guides/grails-upload-file/initial

并按照下一部分中的说明进行操作。

如果您 cd 转至 grails-guides/grails-upload-file/complete,可以转到已完成的示例

3 编写应用程序

initial 文件夹包含使用 web 概要文件创建的 Grails 3 应用程序。它有多个控制器、GORM 数据服务、域名类以及用 GSP 构建的简单 CRUD 界面。它是一个用于列出酒店、餐厅和重要景点等旅游资源的应用。

在接下来的部分中,您将为应用添加一项功能。这些资源中的每个资源可以包含一个精选图片。

  • 对于餐厅,我们将直接将图片字节存储在域名类中。

  • 对于重要景点,我们将文件传输至一个文件夹。

  • 对于酒店,我们将文件上传至 AWS S3。

3.1 精选图片命令对象

我们应用程序中的每个实体(酒店、餐厅和重要景点)都将有精选图片。

为封装上传文件的验证,我们将使用 Grails 命令对象

/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 仅允许 JPG 或 PNG 结尾的文件

3.2 增加允许的文件大小

Grails 3 的默认文件大小为 128000(约 128KB)。

我们将允许 25MB 的文件上传。

25 * 1024 * 1024 = 26.214.400 字节

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

4 餐厅 - 上传并保存为 byte[]

本指南部分展示了如何将文件上传至 Grails 服务器应用以及将文件的字节存储在数据库中。

bytearray

4.1 餐厅域名类

修改 Restaurant 域名类。添加两个属性 featuredImageBytesfeaturedImageContentType

/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 使用域名类属性来在数据库中存储图像的字节
2 图像的内容类型。例如“image/jpeg”
3 图像可能是大文件,我们使用映射闭包来配置 sqlType

4.2 餐厅视图

我们将稍微修改视图,以包括可用于上传精选图片的功能。

在餐厅列表中,表中只显示餐厅名称。查看 Fields 插件 f:table 文档。

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

编辑和创建表单不允许用户设置精选图片的内容类型或 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"/>

在显示餐厅页面中添加一个按钮,该按钮将用户带到精选图片编辑页面。

/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>

修改 `RestaurantController`。添加 `editFeaturedImage` 控制器操作。

/grails-app/controllers/example/grails/RestaurantController.groovy
def editFeaturedImage(Long id) {
    Restaurant restaurant = restaurantDataService.get(id)
    if (!restaurant) {
        notFound()
    }
    [restaurant: restaurant]
}

`editFeaturedImage` GSP 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `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 上传精选图片服务

`uploadFeatureImage` 控制器操作使用之前描述的命令对象来验证上传表单提交。如果它未找到验证错误,则调用 Gorm 数据服务

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

完整 GORM 数据服务在以下代码段中显示

/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 第一个参数应该是要更新的对象的 ID。如果任何参数与域类上的属性不匹配,则会发生编译错误。

4.4 加载字节数组图像

`RestaurantController` 中创建一个操作来呈现餐厅的精选图片

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

使用 `g:createLink` GSP 标记来引用 `img` `src` 属性中的前一个控制器操作。

/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 兴趣点 - 上传和转让

在本指南的这一部分中,我们已经将 Grails 应用程序与上传的图片分开了。我们使用 MAMP 在端口 8888 处运行本地 Apache 服务器,指向 data 文件夹。

transfer

5.1 兴趣点域类

修改 `PointOfInterest` 域类。添加一个属性 `featuredImageUrl`

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

class PointOfInterest {
    String name
    String featuredImageUrl (1)

    static constraints = {
        featuredImageUrl nullable: true
    }
}
1 使用此属性来保存可以检索图像的 url。

5.2 兴趣点视图

我们将稍微修改 GSP 视图,以包括启用上传精选图片的功能。

在兴趣点列表中,表中只显示酒店的名称。查看 Fields 插件 f:table 文档。

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

编辑和创建表单不允许用户设置精选图片的 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"/>

相反,我们添加一个按钮,将用户带到精选图片编辑页面。

/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>

修改 `PointOfInterestController`。添加一个名为 `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 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `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 上传特色图像服务

uploadFeatureImage 控制器操作使用先前描述的命令对象来验证上传表单提交。如果它找不到验证错误,它将调用服务。

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

我们配置将托管上传图像的本地服务器 URL 和我们将用于保存图像的本地文件夹路径。

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

服务使用 transferTo 方法将文件传输到本地文件路径。如果发生错误,该服务将删除它先前传输的文件

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

该服务借助 GORM 数据服务来更新 featuredImageUrl

/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 酒店 - 上传到 AWS S3

对于 Hotel 域类,我们将把文件上传到 Amazon AWS S3

s3

首先,安装 Grails AWS SDK S3 插件

修改 your build.grade 文件

build.gradle
repositories {
...
..
    maven { url 'http://dl.bintray.com/agorapulse/libs' }
}
dependencies {
...
..
.
    compile "org.grails.plugins:aws-sdk-s3:$awsSdkS3Version"
}

gradle.properties 中定义 awsSdkS3Version

gradle.properties
grailsVersion=3.3.6
gormVersion=6.1.10.RELEASE
hibernateCoreVersion=5.1.5.Final
gebVersion=1.1.2
htmlunitDriverVersion=2.47.1
htmlunitVersion=2.18
gradleWrapperVersion=3.5
assetPipelineVersion=2.14.8
awsSdkS3Version=2.2.4

我们将把文件上传到一个已存在的存储桶。

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

6.1 酒店域类

修改 Hotel 域类。添加两个属性 featuredImageKeyfeaturedImageUrl`

/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 我们存储特色图像的 AWS S3 url
2 我们存储文件路径。如有必要,则可用于删除文件。

6.2 酒店视图

我们将稍微修改视图,以包括可用于上传精选图片的功能。

在酒店列表中,仅在表中显示酒店名称。查看 Fields 插件 f:table 文档。

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

编辑和创建表单不允许用户设置精选图片的 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"/>

相反,我们添加一个按钮,将用户带到精选图片编辑页面。

/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>

修改 HotelController。添加名为 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 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `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 上传图像到 S3

uploadFeatureImage 控制器操作使用先前描述的命令对象来验证上传表单提交。如果它找不到验证错误,它将调用服务。

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

该服务使用插件提供的 amazonS3Service 将文件上传到 AWS S3。如果发生错误,该服务将删除它先前上传的文件。

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

借助 GORM 数据服务更新域类实例

/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 使用 AWS 访问密钥和密钥运行应用程序

如果你不提供凭据,则将使用凭据提供程序链,按以下顺序搜索凭据

  • 环境变量 - AWS_ACCESS_KEY_ID 和 AWS_SECRET_KEY

  • Java 系统属性 - aws.accessKeyId andaws.secretKey`

  • 通过 Amazon EC2 元数据服务(IAM 角色)传递的实例配置文件凭据

因此,我们可以通过运行应用程序

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

7 您是否需要 Grails 方面的帮助?

Object Computing, Inc.(OCI)赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的家

认识团队