Skip to content

Commit

Permalink
#785 Add APIs to access images
Browse files Browse the repository at this point in the history
  • Loading branch information
schoicsiro committed Sep 26, 2023
1 parent 9e5f5a6 commit ac05c49
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 0 deletions.
174 changes: 174 additions & 0 deletions grails-app/controllers/au/org/ala/profile/api/ApiController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package au.org.ala.profile.api

import au.ala.org.ws.security.RequireApiKey
import au.org.ala.profile.hub.BaseController
import au.org.ala.profile.hub.ImageType
import au.org.ala.profile.hub.MapService
import au.org.ala.profile.hub.ProfileService
import au.org.ala.profile.hub.Utils
import au.org.ala.profile.security.GrantAccess
import au.org.ala.profile.security.RequiresAccessToken
import grails.converters.JSON

Expand Down Expand Up @@ -510,6 +513,177 @@ class ApiController extends BaseController {
}
}

@GrantAccess
@Path("/api/opus/{opusId}/profile/{profileId}/image/{imageId}")
@Operation(
summary = "Get images associated with a profile",
operationId = "/api/opus/{opusId}/profile/{profileId}/image/{imageId}",
method = "GET",
responses = [
@ApiResponse(
responseCode = "200",
content = @Content(
mediaType = "application/json",
schema = @Schema(
implementation = ImageListResponse.class
)
),
headers = [
@Header(name = 'X-Total-Count', description = "Total number of images", schema = @Schema(type = "integer")),
@Header(name = 'Access-Control-Allow-Headers', description = "CORS header", schema = @Schema(type = "String")),
@Header(name = 'Access-Control-Allow-Methods', description = "CORS header", schema = @Schema(type = "String")),
@Header(name = 'Access-Control-Allow-Origin', description = "CORS header", schema = @Schema(type = "String"))
]
),
@ApiResponse(responseCode = "400",
description = "opusId and profileId are required parameters"),
@ApiResponse(responseCode = "403",
description = "You do not have the necessary permissions to perform this action."),
@ApiResponse(responseCode = "405",
description = "An unexpected error has occurred while processing your request."),
@ApiResponse(responseCode = "404",
description = "Opus or profile not found"),
@ApiResponse(responseCode = "500",
description = "An unexpected error has occurred while processing your request.")
],
parameters = [
@Parameter(name = "opusId",
in = ParameterIn.PATH,
required = true,
description = "Collection id - UUID or short name"),
@Parameter(name = "profileId",
in = ParameterIn.PATH,
required = true,
description = "Profile id - UUID or Scientific name"),
@Parameter(name = "type",
in = ParameterIn.PATH,
required = true,
description = "type - private or open"),
@Parameter(name = "Access-Token",
in = ParameterIn.HEADER,
required = false,
description = "Access token to read private collection"),
@Parameter(name = "Accept-Version",
in = ParameterIn.HEADER,
required = true,
description = "The API version",
schema = @Schema(
name = "Accept-Version",
type = "string",
defaultValue = '1.0',
allowableValues = ["1.0"]
)
)
],
security = [@SecurityRequirement(name="auth"), @SecurityRequirement(name = "oauth")]
)
def getLocalImage () {
if (!params.type) {
badRequest "type is a required parameter"
} else {
try {
ImageType type = params.type as ImageType
if (type == ImageType.PRIVATE) {
def result = apiService.displayLocalImage("${grailsApplication.config.image.private.dir}/", params.opusId, params.profileId, params.imageId, false)
if (result) {
response.setHeader("Content-disposition", "attachment;filename=${params.imageId}")
response.setContentType(Utils.getContentType(result))
result.withInputStream { response.outputStream << it }
}
}
} catch (IllegalArgumentException e) {
log.warn(e)
badRequest "Invalid image type ${params.type}"
}
}
}

@GrantAccess
@Path("/api/opus/{opusId}/profile/{profileId}/image/{imageId}")
@Operation(
summary = "Get images associated with a profile",
operationId = "/api/opus/{opusId}/profile/{profileId}/image/{imageId}",
method = "GET",
responses = [
@ApiResponse(
responseCode = "200",
content = @Content(
mediaType = "application/json",
schema = @Schema(
implementation = ImageListResponse.class
)
),
headers = [
@Header(name = 'X-Total-Count', description = "Total number of images", schema = @Schema(type = "integer")),
@Header(name = 'Access-Control-Allow-Headers', description = "CORS header", schema = @Schema(type = "String")),
@Header(name = 'Access-Control-Allow-Methods', description = "CORS header", schema = @Schema(type = "String")),
@Header(name = 'Access-Control-Allow-Origin', description = "CORS header", schema = @Schema(type = "String"))
]
),
@ApiResponse(responseCode = "400",
description = "opusId and profileId are required parameters"),
@ApiResponse(responseCode = "403",
description = "You do not have the necessary permissions to perform this action."),
@ApiResponse(responseCode = "405",
description = "An unexpected error has occurred while processing your request."),
@ApiResponse(responseCode = "404",
description = "Opus or profile not found"),
@ApiResponse(responseCode = "500",
description = "An unexpected error has occurred while processing your request.")
],
parameters = [
@Parameter(name = "opusId",
in = ParameterIn.PATH,
required = true,
description = "Collection id - UUID or short name"),
@Parameter(name = "profileId",
in = ParameterIn.PATH,
required = true,
description = "Profile id - UUID or Scientific name"),
@Parameter(name = "type",
in = ParameterIn.PATH,
required = true,
description = "type - private or open"),
@Parameter(name = "Access-Token",
in = ParameterIn.HEADER,
required = false,
description = "Access token to read private collection"),
@Parameter(name = "Accept-Version",
in = ParameterIn.HEADER,
required = true,
description = "The API version",
schema = @Schema(
name = "Accept-Version",
type = "string",
defaultValue = '1.0',
allowableValues = ["1.0"]
)
)
],
security = [@SecurityRequirement(name="auth"), @SecurityRequirement(name = "oauth")]
)

def retrieveLocalThumbnailImage () {
if (!params.type) {
badRequest "type is a required parameter"
} else {
try {
ImageType type = params.type as ImageType
if (type == ImageType.PRIVATE) {
def result = apiService.displayLocalImage("${grailsApplication.config.image.private.dir}/", params.opusId, params.profileId, params.imageId, true)
if (result) {
response.setHeader("Content-disposition", "attachment;filename=${params.imageId}")
response.setContentType(Utils.getContentType(result))
result.withInputStream { response.outputStream << it }
}
}
} catch (IllegalArgumentException e) {
log.warn(e)
badRequest "Invalid image type ${params.type}"
}
}
}

@Path("/api/opus/{opusId}/profile/{profileId}/attribute/{attributeId}")
@Operation(
summary = "Get attributes of a profile in a collection",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class ApiInterceptor {
} else {
authorised = true
}
} else if (method?.isAnnotationPresent(GrantAccess)) {
authorised = true
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ class UrlMappings {
get "/opus/$opusId/profile" (version: "1.0", controller: "api", action: "getProfiles", namespace: "v1")
get "/opus/$opusId/profile/$profileId" (version: "1.0", controller: "api", action: "get", namespace: "v1")
get "/opus/$opusId/profile/$profileId/image" (version: "1.0", controller: "api", action: "getImages", namespace: "v1")
get "/opus/$opusId/profile/$profileId/image/$imageId" (version: "1.0", controller: "api", action: "getLocalImage", namespace: "v1")
get "/opus/$opusId/profile/$profileId/image/thumbnail/$imageId" (version: "1.0", controller: "api", action: "retrieveLocalThumbnailImage", namespace: "v1")
get "/opus/$opusId/profile/$profileId/attribute/$attributeId" (version: "1.0", controller: "api", action: "getAttributes", namespace: "v1")
get "/opus/$opusId/profile/$profileId/draft" (version: "1.0", controller: "api", action: "getDraftProfile", namespace: "v1")
}
Expand Down
34 changes: 34 additions & 0 deletions grails-app/services/au/org/ala/profile/api/ApiService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package au.org.ala.profile.api
import au.org.ala.profile.hub.ImageService
import au.org.ala.profile.hub.NslService
import au.org.ala.profile.hub.ProfileService
import au.org.ala.profile.hub.Utils
import grails.web.mapping.LinkGenerator

import static au.org.ala.profile.hub.Utils.encPath
Expand Down Expand Up @@ -138,4 +139,37 @@ class ApiService {
protected isSuccessful(int statusCode) {
return statusCode >= SC_OK && statusCode <= 299
}

def displayLocalImage(String path, String collectionId, String profileId, String fileName, Boolean thumbnail) {
if (!fileName) {
badRequest "fileId is a required parameter"
} else {
File file
String imageId = fileName.substring(0, fileName.lastIndexOf("."))
if (thumbnail) {
String thumbnailName = makeThumbnailName(fileName)
file = new File("${path}/${collectionId}/${profileId}/${imageId}/${imageId}_thumbnails/${thumbnailName}")
if (!file.exists()) { //use the image if there is no thumbnail
file = new File("${path}/${collectionId}/${profileId}/${imageId}/${fileName}")
}
} else {
file = new File("${path}/${collectionId}/${profileId}/${imageId}/${fileName}")
}

if (!file.exists()) { //support for version 1 file locations
file = new File("${path}/${profileId}/${fileName}")
}
if (!file.exists()) {
notFound "The requested file could not be found"
} else {
return file
}
}
}

String makeThumbnailName(String fileName) {
String extension = Utils.getExtension(fileName)
String imageId = fileName.substring(0, fileName.lastIndexOf('.'))
"${imageId}_thumbnail${extension}"
}
}
55 changes: 55 additions & 0 deletions src/test/groovy/au/org/ala/profile/api/ApiControllerSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,59 @@ class ApiControllerSpec extends Specification implements ControllerUnitTest<ApiC
'opus1' | '123' | null | 404
'opus1' | '123' | 'a,b' | 200
}

void "getLocalImage should be provided with opus id and profile id parameters"() {
when:
params.opusId = opusId
params.profileId = profileId
params.imageId = imageId
apiService.displayLocalImage('path', params.opusId, params.profileId, params.imageId, false)

then:
response.status == responseCode

where:
opusId | profileId | imageId | responseCode
'opus1' | '123' | '1.png' | 200
}

void "getLocalImage should be provided with opus id and profile id and imageId parameters"() {
when:
params.type = type
params.opusId = opusId
params.profileId = profileId
params.imageId = imageId
controller.getLocalImage()

then:
response.status == responseCode

where:
type | opusId | profileId | imageId | responseCode
null |'opus1' | '123' | '1.png' | 400
null | null | '123' | '1.png' | 400
null |'opus1' | null | '1.png' | 400
null |'opus1' | '123' | null | 400
'PRIVATE' |'opus1' | '123' | '1.png' | 200
}

void "retrieveLocalThumbnailImage should be provided with opus id and profile id and imageId parameters with thumbnail"() {
when:
params.type = type
params.opusId = opusId
params.profileId = profileId
params.imageId = imageId
controller.retrieveLocalThumbnailImage()

then:
response.status == responseCode

where:
type | opusId | profileId | imageId | responseCode
null |'opus1' | '123' | '1.png' | 400
null | null | '123' | '1.png' | 400
null |'opus1' | null | '1.png' | 400
null |'opus1' | '123' | null | 400
'PRIVATE' |'opus1' | '123' | '1.png' | 200
}
}

0 comments on commit ac05c49

Please sign in to comment.